From 54aae9182241e29c2a071c2df69a57277d070d71 Mon Sep 17 00:00:00 2001 From: Artyom Tsvirko <36863599+lArtiquel@users.noreply.github.com> Date: Sun, 23 Aug 2026 00:40:02 +0200 Subject: [PATCH] Handle MIME type parameter names case-insensitively MIME type parameter names are case-insensitive, and MimeType already stores them in a LinkedCaseInsensitiveMap. Several code paths, however, still compared them with case-sensitive String.equals(). As a result, MimeType.hashCode() disagreed with MimeType.equals() for parameter names that differ only in case, breaking the equals/hashCode contract: text/plain;FOO=bar and text/plain;foo=bar are equal but hash differently, so one is not found in a hash-based collection holding the other. MimeType.compareTo() had the same blind spot for the charset parameter. MediaType was affected in two further ways: an out-of-range quality value escaped validation when spelled Q=, and removeQualityValue() left a Q= parameter in place. Signed-off-by: Artyom Tsvirko <36863599+lArtiquel@users.noreply.github.com> --- .../org/springframework/util/MimeType.java | 18 +++++++-------- .../springframework/util/MimeTypeTests.java | 22 +++++++++++++++++++ .../org/springframework/http/MediaType.java | 4 ++-- .../springframework/http/MediaTypeTests.java | 18 +++++++++++++++ 4 files changed, 51 insertions(+), 11 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/util/MimeType.java b/spring-core/src/main/java/org/springframework/util/MimeType.java index 3c96f445e2ab..91373194ee8a 100644 --- a/spring-core/src/main/java/org/springframework/util/MimeType.java +++ b/spring-core/src/main/java/org/springframework/util/MimeType.java @@ -486,9 +486,9 @@ public boolean equals(@Nullable Object other) { /** * Determine if the parameters in this {@code MimeType} and the supplied * {@code MimeType} are equal, performing case-insensitive comparisons - * for {@link Charset Charsets} and disregarding quoting of parameter - * values, so that, for example, {@code spring="framework"} and - * {@code spring=framework} are considered equal. + * for parameter names and {@link Charset Charsets}, and disregarding + * quoting of parameter values, so that, for example, {@code spring="framework"} + * and {@code spring=framework} are considered equal. * @since 4.2 */ private boolean parametersAreEqual(MimeType other) { @@ -501,7 +501,7 @@ private boolean parametersAreEqual(MimeType other) { if (!other.parameters.containsKey(key)) { return false; } - if (PARAM_CHARSET.equals(key)) { + if (PARAM_CHARSET.equalsIgnoreCase(key)) { if (!ObjectUtils.nullSafeEquals(getCharset(), other.getCharset())) { return false; } @@ -528,15 +528,15 @@ public int hashCode() { /** * Compute a hash code for the parameters map, consistent with - * {@link #parametersAreEqual}: normalizing {@link Charset Charsets} and - * disregarding quoting of parameter values. + * {@link #parametersAreEqual}: normalizing parameter names and + * {@link Charset Charsets}, and disregarding quoting of parameter values. */ private int parametersHashCode() { int result = 0; for (Map.Entry entry : this.parameters.entrySet()) { String key = entry.getKey(); - Object value = (PARAM_CHARSET.equals(key) ? getCharset() : unquote(entry.getValue())); - result += key.hashCode() ^ ObjectUtils.nullSafeHashCode(value); + Object value = (PARAM_CHARSET.equalsIgnoreCase(key) ? getCharset() : unquote(entry.getValue())); + result += key.toLowerCase(Locale.ROOT).hashCode() ^ ObjectUtils.nullSafeHashCode(value); } return result; } @@ -602,7 +602,7 @@ public int compareTo(MimeType other) { if (comp != 0) { return comp; } - if (PARAM_CHARSET.equals(thisAttribute)) { + if (PARAM_CHARSET.equalsIgnoreCase(thisAttribute)) { Charset thisCharset = getCharset(); Charset otherCharset = other.getCharset(); if (thisCharset != otherCharset) { diff --git a/spring-core/src/test/java/org/springframework/util/MimeTypeTests.java b/spring-core/src/test/java/org/springframework/util/MimeTypeTests.java index bef9ebfde422..801794b1dfbd 100644 --- a/spring-core/src/test/java/org/springframework/util/MimeTypeTests.java +++ b/spring-core/src/test/java/org/springframework/util/MimeTypeTests.java @@ -539,6 +539,28 @@ void equalsIsCaseInsensitiveForCharsets() { assertThat(m2.compareTo(m1)).isEqualTo(0); } + @Test + void equalsIsCaseInsensitiveForParameterNames() { + MimeType m1 = new MimeType("text", "plain", singletonMap("Spring", "framework")); + MimeType m2 = new MimeType("text", "plain", singletonMap("spring", "framework")); + assertThat(m1).isEqualTo(m2); + assertThat(m2).isEqualTo(m1); + assertThat(m1).hasSameHashCodeAs(m2); + assertThat(m1.compareTo(m2)).isEqualTo(0); + assertThat(m2.compareTo(m1)).isEqualTo(0); + } + + @Test + void equalsIsCaseInsensitiveForCharsetParameterName() { + MimeType m1 = new MimeType("text", "plain", singletonMap("Charset", "UTF-8")); + MimeType m2 = new MimeType("text", "plain", singletonMap("charset", "utf-8")); + assertThat(m1).isEqualTo(m2); + assertThat(m2).isEqualTo(m1); + assertThat(m1).hasSameHashCodeAs(m2); + assertThat(m1.compareTo(m2)).isEqualTo(0); + assertThat(m2.compareTo(m1)).isEqualTo(0); + } + @Test // gh-36729 void equalsIgnoresParameterValueQuoting() { MimeType m1 = MimeTypeUtils.parseMimeType("text/plain; spring=\"framework\""); diff --git a/spring-web/src/main/java/org/springframework/http/MediaType.java b/spring-web/src/main/java/org/springframework/http/MediaType.java index b65e8053c944..72599932b634 100644 --- a/spring-web/src/main/java/org/springframework/http/MediaType.java +++ b/spring-web/src/main/java/org/springframework/http/MediaType.java @@ -503,7 +503,7 @@ public MediaType(MimeType mimeType) { @Override protected void checkParameters(String parameter, String value) { super.checkParameters(parameter, value); - if (PARAM_QUALITY_FACTOR.equals(parameter)) { + if (PARAM_QUALITY_FACTOR.equalsIgnoreCase(parameter)) { String unquotedValue = unquote(value); double d = Double.parseDouble(unquotedValue); Assert.isTrue(d >= 0D && d <= 1D, @@ -651,7 +651,7 @@ public MediaType removeQualityValue() { return this; } Map params = new LinkedHashMap<>(getParameters()); - params.remove(PARAM_QUALITY_FACTOR); + params.keySet().removeIf(PARAM_QUALITY_FACTOR::equalsIgnoreCase); return new MediaType(this, params); } diff --git a/spring-web/src/test/java/org/springframework/http/MediaTypeTests.java b/spring-web/src/test/java/org/springframework/http/MediaTypeTests.java index a7c9c090fb5a..6640b725a831 100644 --- a/spring-web/src/test/java/org/springframework/http/MediaTypeTests.java +++ b/spring-web/src/test/java/org/springframework/http/MediaTypeTests.java @@ -135,6 +135,12 @@ void parseMediaTypeIllegalQualityFactor() { MediaType.parseMediaType("audio/basic;q=1.1")); } + @Test + void parseMediaTypeIllegalQualityFactorWithUpperCaseParameterName() { + assertThatExceptionOfType(InvalidMediaTypeException.class).isThrownBy(() -> + MediaType.parseMediaType("audio/basic;Q=1.1")); + } + @Test void parseMediaTypeIllegalCharset() { assertThatExceptionOfType(InvalidMediaTypeException.class).isThrownBy(() -> @@ -293,6 +299,18 @@ void isConcrete() { assertThat(new MediaType("text", "*").isConcrete()).as("text/* concrete").isFalse(); } + @Test + void removeQualityValue() { + assertThat(MediaType.parseMediaType("audio/basic;q=0.8").removeQualityValue()) + .isEqualTo(MediaType.parseMediaType("audio/basic")); + } + + @Test + void removeQualityValueWithUpperCaseParameterName() { + assertThat(MediaType.parseMediaType("audio/basic;Q=0.8").removeQualityValue()) + .isEqualTo(MediaType.parseMediaType("audio/basic")); + } + @Test // gh-26127 void serialize() throws Exception { MediaType original = new MediaType("text", "plain", StandardCharsets.UTF_8);