Fix InvalidMimeTypeException for compatible media types

The `AbstractMessageConverterMethodProcessor` is in charge of handling
controller method return values and to write those as HTTP response
messages. The content negotiation process is an important part.

The `MimeTypeUtils#sortBySpecificity` is in charge of sorting inbound
"Accept" media types by their specificity and reject them if the list
is too large, in order to protect the application from ddos attacks.

Prior to this commit, the content negotiation process would first get
the sorted "Accept" media types, the producible media types as
advertized by message converters - and collect the intersection of both
in a new list (also sorted by specificity). If the "Accept" list is
large enough (but under the limit), the list of compatible media types
could exceed that limit because duplicates could be introduced in that
list: several converters can produce the same content type.

This commit ensures that compatible media types are collected in a set
to avoid duplicates. Without that, exceeding the limit at this point
will throw an `InvalidMimeTypeException` that's not handled by the
processor and result in a server error.

Fixes gh-36300
This commit is contained in:
Brian Clozel
2026-02-20 18:21:45 +01:00
parent f103af4982
commit 2691489381
2 changed files with 27 additions and 6 deletions
@@ -272,12 +272,11 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
"No converter found for return value of type: " + valueType);
}
List<MediaType> compatibleMediaTypes = new ArrayList<>();
determineCompatibleMediaTypes(acceptableTypes, producibleTypes, compatibleMediaTypes);
List<MediaType> compatibleMediaTypes = determineCompatibleMediaTypes(acceptableTypes, producibleTypes);
// For ProblemDetail, fall back on RFC 9457 format
if (compatibleMediaTypes.isEmpty() && ProblemDetail.class.isAssignableFrom(valueType)) {
determineCompatibleMediaTypes(PROBLEM_MEDIA_TYPES, producibleTypes, compatibleMediaTypes);
compatibleMediaTypes = determineCompatibleMediaTypes(PROBLEM_MEDIA_TYPES, producibleTypes);
}
if (compatibleMediaTypes.isEmpty()) {
@@ -452,16 +451,18 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
return this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request));
}
private void determineCompatibleMediaTypes(
List<MediaType> acceptableTypes, List<MediaType> producibleTypes, List<MediaType> mediaTypesToUse) {
private List<MediaType> determineCompatibleMediaTypes(
List<MediaType> acceptableTypes, List<MediaType> producibleTypes) {
Set<MediaType> compatibleTypes = new LinkedHashSet<>();
for (MediaType requestedType : acceptableTypes) {
for (MediaType producibleType : producibleTypes) {
if (requestedType.isCompatibleWith(producibleType)) {
mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));
compatibleTypes.add(getMostSpecificMediaType(requestedType, producibleType));
}
}
}
return new ArrayList<>(compatibleTypes);
}
/**
@@ -24,6 +24,8 @@ import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeName;
@@ -1022,6 +1024,24 @@ class RequestResponseBodyMethodProcessorTests {
assertThat(value).isEqualTo("foo");
}
@Test // gh-36300
void shouldNotDuplicateInCompatibleMediaTypes() throws Exception {
Method method = TestRestController.class.getMethod("handle");
MethodParameter returnType = new MethodParameter(method, -1);
List<HttpMessageConverter<?>> converters = List.of(new StringHttpMessageConverter(), new JacksonJsonHttpMessageConverter());
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
String accept = Stream.iterate(1, i -> i + 1)
.limit(48).map(i -> "application/" + i)
.collect(Collectors.joining(","));
accept = accept + ", application/json";
this.servletRequest.addHeader("Accept", accept);
processor.writeWithMessageConverters("spring framework", returnType, this.request);
}
private void assertContentDisposition(RequestResponseBodyMethodProcessor processor,
boolean expectContentDisposition, String requestURI, String comment) throws Exception {