When you have two content type resolvers:
HeaderContentTypeResolver
FixedContentTypeResolver
And suppose the FixedContentTypeResolver returns application/json
.
When you have the following controller definitions:
@RestController("/users")
public class MyController {
@GetMapping()
public Flux<String> f() {
...
}
@GetMapping(produces = "text/csv")
public Mono<ResponseEntity<String>> g() {
..
}
}
So basically, we have two mappings one produces JSON and one a CSV file.
When you call this controller with:
curl -H 'Accept: */*' http://localhost:8080/users
It produces a JSON message. However, when you call this controller with:
curl -H 'Accept: */*;q=0.8' http://localhost:8080/users
All browsers do add a quality factor by default.
It defaults to whatever HttpMessageWriter can produce the type, in our case, a CSV mapper HttpMessageEncoder and it writes a CSV file with content type text/csv
. One could argue the problem lies here, however looking at the code in RequestedContentTypeResolverBuilder
:
public RequestedContentTypeResolver build() {
List<RequestedContentTypeResolver> resolvers = (!this.candidates.isEmpty() ?
this.candidates.stream().map(Supplier::get).collect(Collectors.toList()) :
Collections.singletonList(new HeaderContentTypeResolver()));
return exchange -> {
for (RequestedContentTypeResolver resolver : resolvers) {
List<MediaType> mediaTypes = resolver.resolveMediaTypes(exchange);
if (mediaTypes.equals(RequestedContentTypeResolver.MEDIA_TYPE_ALL_LIST)) {
continue;
}
return mediaTypes;
}
return RequestedContentTypeResolver.MEDIA_TYPE_ALL_LIST;
};
}
There is a special case for */*
in which it tries the next content type resolver. But when it encounters */*;q=0.8
it does not work. The problem is that:
HeaderContentTypeResolver:
public List<MediaType> resolveMediaTypes(ServerWebExchange exchange) throws NotAcceptableStatusException {
try {
List<MediaType> mediaTypes = exchange.getRequest().getHeaders().getAccept();
MediaType.sortBySpecificityAndQuality(mediaTypes);
return (!CollectionUtils.isEmpty(mediaTypes) ? mediaTypes : MEDIA_TYPE_ALL_LIST);
}
catch (InvalidMediaTypeException ex) {
String value = exchange.getRequest().getHeaders().getFirst("Accept");
throw new NotAcceptableStatusException(
"Could not parse 'Accept' header [" + value + "]: " + ex.getMessage());
}
}
does not apply the method MediaType#removeQualityValue
, which is necessary to fire the continue
block in the build
method above.
The workaround for this issue is to add produces
to the first controller method explicitly.
Comment From: bclozel
Thanks for the analysis, but you're several steps ahead of us here. A sample application showing the behavior and a short note explaining what you would expect instead would be really helpful. Could you provide a minimal sample please?
Comment From: nbaars
@bclozel simplified it to two test cases:
@Test
public void allMediaTypeWithTwoResolvers() {
RequestedContentTypeResolverBuilder builder = new RequestedContentTypeResolverBuilder();
builder.resolver(new HeaderContentTypeResolver());
builder.resolver(new FixedContentTypeResolver(MediaType.APPLICATION_JSON));
RequestedContentTypeResolver resolver = builder.build();
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/").accept(MediaType.ALL));
List<MediaType> mediaTypes = resolver.resolveMediaTypes(exchange);
assertThat(mediaTypes).isEqualTo(Collections.singletonList(MediaType.APPLICATION_JSON));
}
@Test
public void allMediaTypeWithQualityFactorAndTwoResolvers() {
RequestedContentTypeResolverBuilder builder = new RequestedContentTypeResolverBuilder();
builder.resolver(new HeaderContentTypeResolver());
builder.resolver(new FixedContentTypeResolver(MediaType.APPLICATION_JSON));
RequestedContentTypeResolver resolver = builder.build();
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/").accept(MediaType.valueOf("*/*;q=0.8")));
List<MediaType> mediaTypes = resolver.resolveMediaTypes(exchange);
assertThat(mediaTypes).isEqualTo(Collections.singletonList(MediaType.APPLICATION_JSON));
}
The last one fails it returns:
expected: [application/json]
but was: [*/*;q=0.8]
org.opentest4j.AssertionFailedError:
expected: [application/json]
but was: [*/*;q=0.8]
The code in RequestedContentTypeResolver build()
should use removeQualityValue
from MediaType
before applying the equals check.
Comment From: bclozel
Thanks @nbaars for the report, this has been fixed in 6.0.x and 5.3.x and will be released tomorrow.