Skip to content

Commit 59eede4

Browse files
committed
fix: Case-insensitive Content-Type matching
Per RFC 9110/2045 media types are case-insensitive. Lower-case the result of ContentTypeHeader.subtype() and lower-case spec content-type keys at parse time so headers like "Application/JSON" or "Text/Plain" match requestBody.content entries regardless of casing. Also fixes a compile error in FormUrlEncodedParser.decodeComponent which referenced a non-existent ValidationException constructor; the URLDecoder failure path now wraps the IllegalArgumentException as a proper ValidationError(/body, decode, ...).
1 parent 04c5043 commit 59eede4

4 files changed

Lines changed: 20 additions & 5 deletions

File tree

src/main/java/com/retailsvc/http/internal/ContentTypeHeader.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,17 @@ public final class ContentTypeHeader {
88

99
private ContentTypeHeader() {}
1010

11-
/** Returns the bare media type, stripping parameters. {@code null} → {@code application/json}. */
11+
/**
12+
* Returns the bare media type, stripping parameters and lower-casing for case-insensitive
13+
* matching (RFC 9110 / 2045). {@code null} → {@code application/json}.
14+
*/
1215
public static String subtype(String header) {
1316
if (header == null) {
1417
return "application/json";
1518
}
1619
int semi = header.indexOf(';');
1720
String bare = (semi < 0 ? header : header.substring(0, semi));
18-
return bare.trim();
21+
return bare.trim().toLowerCase(Locale.ROOT);
1922
}
2023

2124
/** Returns the named parameter value (e.g. {@code charset}), or empty if absent. */

src/main/java/com/retailsvc/http/internal/FormUrlEncodedParser.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package com.retailsvc.http.internal;
22

3+
import com.retailsvc.http.ValidationException;
34
import com.retailsvc.http.spec.schema.ArraySchema;
45
import com.retailsvc.http.spec.schema.ObjectSchema;
56
import com.retailsvc.http.spec.schema.Schema;
7+
import com.retailsvc.http.validate.ValidationError;
68
import java.net.URLDecoder;
79
import java.nio.charset.Charset;
810
import java.nio.charset.IllegalCharsetNameException;
@@ -42,7 +44,9 @@ private static String decodeComponent(String value, Charset charset) {
4244
try {
4345
return URLDecoder.decode(value, charset);
4446
} catch (IllegalArgumentException ex) {
45-
throw new ValidationException("/body", "decode", "Malformed form URL encoding", ex);
47+
throw new ValidationException(
48+
new ValidationError(
49+
"/body", "decode", "malformed form URL encoding: " + ex.getMessage(), value));
4650
}
4751
}
4852

src/main/java/com/retailsvc/http/spec/Spec.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ private static RequestBody parseRequestBody(Map<String, Object> raw) {
203203
for (var e : contentRaw.entrySet()) {
204204
Map<String, Object> mt = (Map<String, Object>) e.getValue();
205205
content.put(
206-
e.getKey(),
206+
e.getKey().toLowerCase(java.util.Locale.ROOT),
207207
new MediaType(SchemaParser.parse(mt.getOrDefault(SCHEMA_KEY, Map.of("type", "object")))));
208208
}
209209
return new RequestBody(Boolean.TRUE.equals(raw.get("required")), Map.copyOf(content));
@@ -219,7 +219,9 @@ private static Map<String, Response> parseResponses(Map<String, Object> raw) {
219219
for (var ce : contentRaw.entrySet()) {
220220
Map<String, Object> mt = (Map<String, Object>) ce.getValue();
221221
if (mt.containsKey(SCHEMA_KEY)) {
222-
content.put(ce.getKey(), new MediaType(SchemaParser.parse(mt.get(SCHEMA_KEY))));
222+
content.put(
223+
ce.getKey().toLowerCase(java.util.Locale.ROOT),
224+
new MediaType(SchemaParser.parse(mt.get(SCHEMA_KEY))));
223225
}
224226
}
225227
out.put(e.getKey(), new Response(Map.copyOf(content)));

src/test/java/com/retailsvc/http/internal/ContentTypeHeaderTest.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ void subtypeDefaultsToApplicationJsonWhenNull() {
2626
assertThat(ContentTypeHeader.subtype(null)).isEqualTo("application/json");
2727
}
2828

29+
@Test
30+
void subtypeLowerCasesMediaType() {
31+
assertThat(ContentTypeHeader.subtype("Application/JSON")).isEqualTo("application/json");
32+
assertThat(ContentTypeHeader.subtype("Text/Plain; charset=UTF-8")).isEqualTo("text/plain");
33+
}
34+
2935
@Test
3036
void parameterReturnsValue() {
3137
assertThat(ContentTypeHeader.parameter("text/plain; charset=iso-8859-1", "charset"))

0 commit comments

Comments
 (0)