Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
nbaars opened this issue Feb 1, 2023 · 3 comments
Assignees
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) status: backported An issue that has been backported to maintenance branches type: bug A general bug
Milestone

Comments

@nbaars
Copy link

nbaars commented Feb 1, 2023

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.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Feb 1, 2023
@bclozel
Copy link
Member

bclozel commented Feb 1, 2023

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?

@bclozel bclozel added status: waiting-for-feedback We need additional information before we can continue in: web Issues in web modules (web, webmvc, webflux, websocket) labels Feb 1, 2023
@nbaars
Copy link
Author

nbaars commented Feb 3, 2023

@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.

@bclozel bclozel self-assigned this Feb 3, 2023
@bclozel bclozel added type: bug A general bug and removed status: waiting-for-feedback We need additional information before we can continue status: waiting-for-triage An issue we've not yet triaged or decided on labels Feb 3, 2023
@bclozel bclozel added this to the 6.0.x milestone Feb 3, 2023
@bclozel bclozel removed this from the 6.0.x milestone Mar 7, 2023
@bclozel bclozel modified the milestones: 6.0.8, 6.0.7 Mar 15, 2023
@bclozel bclozel changed the title WebFlux: HeaderContentTypeResolver takes into account the quality factor and fails to detect */* correctly HeaderContentTypeResolver does not ignore quality factor when filtering */* media types Mar 15, 2023
@bclozel bclozel changed the title HeaderContentTypeResolver does not ignore quality factor when filtering */* media types RequestedContentTypeResolver does not ignore quality factor when filtering */* media types Mar 15, 2023
@github-actions github-actions bot added status: backported An issue that has been backported to maintenance branches and removed for: backport-to-5.3.x labels Mar 15, 2023
@bclozel
Copy link
Member

bclozel commented Mar 15, 2023

Thanks @nbaars for the report, this has been fixed in 6.0.x and 5.3.x and will be released tomorrow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) status: backported An issue that has been backported to maintenance branches type: bug A general bug
Projects
None yet
Development

No branches or pull requests

3 participants