Skip to content

RequestedContentTypeResolver does not ignore quality factor when filtering */* media types #29915

@nbaars

Description

@nbaars

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.

Metadata

Metadata

Assignees

Labels

in: webIssues in web modules (web, webmvc, webflux, websocket)status: backportedAn issue that has been backported to maintenance branchestype: bugA general bug

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions