diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/CHANGELOG.md b/sdk/spring/spring-cloud-azure-appconfiguration-config/CHANGELOG.md index 16944b197b61..c7b463921ac1 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/CHANGELOG.md +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/CHANGELOG.md @@ -8,6 +8,8 @@ ### Bugs Fixed +- Fixes a bug where ';' was ignored in JSON content type checking. + ### Other Changes ## 7.1.0 (2026-03-11) diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/JsonConfigurationParser.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/JsonConfigurationParser.java index 824f111ae238..9a9ae1a44300 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/JsonConfigurationParser.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/JsonConfigurationParser.java @@ -2,10 +2,8 @@ // Licensed under the MIT License. package com.azure.spring.cloud.appconfiguration.config.implementation; -import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; -import java.util.List; import java.util.Map; import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException; @@ -30,21 +28,47 @@ static boolean isJsonContentType(String contentType) { return false; } - if (contentType.contains("/")) { - String mainType = contentType.split("/")[0]; - String subType = contentType.split("/")[1]; + // Remove parameters like "; charset=utf-8" if present, without using regex-based split + String cleanContentType; + int semicolonIndex = contentType.indexOf(';'); + if (semicolonIndex >= 0) { + cleanContentType = contentType.substring(0, semicolonIndex).trim(); + } else { + cleanContentType = contentType.trim(); + } - if (mainType.equalsIgnoreCase(acceptedMainType)) { - if (subType.contains("+")) { - List subtypes = Arrays.asList(subType.split("\\+")); - return subtypes.contains(acceptedSubType); - } else { - return subType.equalsIgnoreCase(acceptedSubType); - } - } + if (cleanContentType.isEmpty()) { + return false; + } + + int slashIndex = cleanContentType.indexOf('/'); + if (slashIndex <= 0 || slashIndex == cleanContentType.length() - 1) { + // No slash, slash at start, or no subtype + return false; + } + + String mainType = cleanContentType.substring(0, slashIndex); + String subType = cleanContentType.substring(slashIndex + 1); + + // RFC 7231/6838: tokens cannot contain whitespace + // Check for internal whitespace (after initial trim of the whole content type) + if (mainType.contains(" ") || mainType.contains("\t") || subType.contains(" ") || subType.contains("\t")) { + return false; } - return false; + if (!mainType.equalsIgnoreCase(acceptedMainType) || subType.isEmpty()) { + return false; + } + + // Handle structured syntax suffixes like "application/vnd.api+json". + // According to RFC 6839, the suffix is the part after the last '+'. + int plusIndex = subType.lastIndexOf('+'); + if (plusIndex >= 0 && plusIndex < subType.length() - 1) { + String suffix = subType.substring(plusIndex + 1); + return suffix.equalsIgnoreCase(acceptedSubType); + } else { + return subType.equalsIgnoreCase(acceptedSubType); + } } static Map parseJsonSetting(ConfigurationSetting setting) diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/JsonConfigurationParserTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/JsonConfigurationParserTest.java index 51cc010bb88a..52641e43abc7 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/JsonConfigurationParserTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/JsonConfigurationParserTest.java @@ -25,17 +25,77 @@ public class JsonConfigurationParserTest { @Test public void isJsonContentType() { + // Basic valid JSON content types assertTrue(JsonConfigurationParser.isJsonContentType("application/json")); assertTrue(JsonConfigurationParser.isJsonContentType("application/api+json")); - assertTrue(JsonConfigurationParser.isJsonContentType("application/json+activity")); assertTrue(JsonConfigurationParser.isJsonContentType("application/vnd.xxxx+json")); assertTrue(JsonConfigurationParser.isJsonContentType("application/vnd.microsoft.appconfig.document+json")); + + // Invalid content types assertFalse(JsonConfigurationParser.isJsonContentType("application")); assertFalse(JsonConfigurationParser.isJsonContentType("app/json")); assertFalse(JsonConfigurationParser.isJsonContentType("app/config")); assertFalse(JsonConfigurationParser.isJsonContentType("application/config")); assertFalse(JsonConfigurationParser.isJsonContentType("")); assertFalse(JsonConfigurationParser.isJsonContentType(null)); + assertFalse(JsonConfigurationParser.isJsonContentType("application/json+activity")); // activity is the suffix, not json + } + + @Test + public void isJsonContentTypeWithParameters() { + // Content types with charset and other parameters + assertTrue(JsonConfigurationParser.isJsonContentType("application/json; charset=utf-8")); + assertTrue(JsonConfigurationParser.isJsonContentType("application/json;charset=utf-8")); + assertTrue(JsonConfigurationParser.isJsonContentType("application/json ; charset=utf-8")); + assertTrue(JsonConfigurationParser.isJsonContentType("application/vnd.api+json; charset=utf-8")); + assertTrue(JsonConfigurationParser.isJsonContentType("application/json; charset=ISO-8859-1")); + assertTrue(JsonConfigurationParser.isJsonContentType("application/json;boundary=something")); + } + + @Test + public void isJsonContentTypeWithWhitespace() { + // Content types with various whitespace at the boundaries + assertTrue(JsonConfigurationParser.isJsonContentType(" application/json")); + assertTrue(JsonConfigurationParser.isJsonContentType("application/json ")); + assertTrue(JsonConfigurationParser.isJsonContentType(" application/json ")); + + // Internal whitespace around '/' or '+' is not allowed by RFC 7231/6838 token rules + assertFalse(JsonConfigurationParser.isJsonContentType("application / json")); + assertFalse(JsonConfigurationParser.isJsonContentType("application/vnd.api + json")); + } + + @Test + public void isJsonContentTypeCaseInsensitive() { + // Case variations + assertTrue(JsonConfigurationParser.isJsonContentType("Application/Json")); + assertTrue(JsonConfigurationParser.isJsonContentType("APPLICATION/JSON")); + assertTrue(JsonConfigurationParser.isJsonContentType("application/JSON")); + assertTrue(JsonConfigurationParser.isJsonContentType("Application/vnd.api+JSON")); + assertTrue(JsonConfigurationParser.isJsonContentType("application/API+JSON")); + } + + @Test + public void isJsonContentTypeEdgeCases() { + // Edge cases and boundary conditions + assertFalse(JsonConfigurationParser.isJsonContentType("application/")); // Empty subtype + assertFalse(JsonConfigurationParser.isJsonContentType("/json")); // Empty main type + assertFalse(JsonConfigurationParser.isJsonContentType("/")); // Both empty + assertFalse(JsonConfigurationParser.isJsonContentType("application/xml")); + assertFalse(JsonConfigurationParser.isJsonContentType("text/json")); // Wrong main type + assertFalse(JsonConfigurationParser.isJsonContentType("application/json+xml")); // json not as suffix + assertFalse(JsonConfigurationParser.isJsonContentType("application/xml+html")); // No json at all + assertFalse(JsonConfigurationParser.isJsonContentType(" ")); // Only whitespace + } + + @Test + public void isJsonContentTypeComplexStructuredSyntax() { + // Complex structured syntax suffixes (RFC 6839) + assertTrue(JsonConfigurationParser.isJsonContentType("application/problem+json")); + assertTrue(JsonConfigurationParser.isJsonContentType("application/merge-patch+json")); + assertTrue(JsonConfigurationParser.isJsonContentType("application/json-patch+json")); + assertTrue(JsonConfigurationParser.isJsonContentType("application/ld+json")); // JSON-LD + assertTrue(JsonConfigurationParser.isJsonContentType("application/hal+json")); + assertTrue(JsonConfigurationParser.isJsonContentType("application/vnd.geo+json")); } @Test