Spring

 How do you get a Spring MVC service to pick a less specific Accept MediaType?

Greg Lee's profile image
Greg Lee posted Jan 16, 2019 09:21 PM

I have a use case where the client sends either:

Accept=application/json Accept=application/json;fmt=avro

The response should have slightly different format based on the accept header.

 

I have a two HttpMessageConverters, both of them know that they specifically only handle exactly their media types with exactly the right parameters (no more, no less).

 

When a request is sent with application/json, the AbstractMessageConverterMethodProcessor#writeWithMessageConverters method asks each converter if the converter canWrite the object type. The canWrite method does have a MediaType parameter, but it is always sent as null in this case.

 

Of course, both of the converters know they can handle the object type, so both MediaTypes are added to the producible media types. Then the producible media types are sorted by specificity and the most specific one is selected. What this means is that application/json;fmt=avro will always be selected, regardless of what the client sends.

 

How can I get the AbstractMessageConverterMethodProcessor to select a less specific MediaType?

 

If canWrite was called with the MediaType, I could do what I need by saying "No" based on the MediaType.

 

If the converter's write method was called with the MediaType, I could have one converter that internally knows when to format the response based on the Media Type.

 

Neither of these seem to be true.

Daniel Mikusa's profile image
Daniel Mikusa

Any chance you have a demo app or something you could share to replicate the situation you're seeing? I'm surprised you're not getting a MediaType in `canWrite()`. If I can replicate what you're seeing, I'd be happy to take a closer look at what's going on.

Greg Lee's profile image
Greg Lee

Daniel, thanks for the quick response.

 

Here is a demo of the problem, see the tests.

Attachment  View in library
Daniel Mikusa's profile image
Daniel Mikusa

I'm not sure I understand why it's picking the most specific first. It is clearly the intent from the code, but if I ask for `application/json`, and there's a converter that can return exactly `application/json` and nothing more, then I'd expect that to be what's picked. I don't profess to be an expert on mime types though, so there could certainly be something I'm missing here & a good reason for what it's doing.

 

What I've been able to find is I can make your example work if I make the following change to your controller.

@GetMapping(value = "/person", produces=MediaType.APPLICATION_JSON_VALUE) public Person getPerson(@RequestHeader("Accept") final String accept) { return new Person("test-" + accept, 1.23); }

Note the addition of `produces` to the annotation.

 

When you add this, if you send a request with `Accept: application/json` then you will get a response converted by your `com.sample.converter.JsonHttpMessageConverter`, and if you send a request with `Accept: application/json; fmt=tertiary; foo=bar` (or something more specific) it will use the converter registered for that specific media type. I believe this is the behavior you're going for, in that you generally want `application/json` unless the client specifically asks for `application/json;fmt=avro`.

 

This seems to side step some of the logic related to sorting. With this annotation, the code does not build up a complete list of all media types from all registered converters (it skips the first call to `canWrite()`), instead it just uses a list of one type, what you put in `produces`. It then sorts in the same manner, by specificity, but there's only one media type in the list so it doesn't matter. It will then pick `application/json` and use the corresponding registered message converter.

 

In the case where you request something more specific, that works because the code will use whichever is more specific, what you set in `produces` or what's in the Accept header. Since the Accept header is more specific, that gets used. What would fail is if you set `produces` to `application/json` but have an Accept header of `plain/text`. Since that's not under the "application/json" hierarchy, it doesn't match and you get an 406.

 

Anyway, you might give that a try for a workaround. If you think there's a bug in the way it's handling media type selection, like the way it's sorting, I would suggest opening a JIRA issue here -> https://github.com/spring-projects/spring-framework/issues

 

Hope that helps!

Nigel Longton's profile image
Nigel Longton

Looking at the spring code it seems 'wrong' to change the callers Accept media type based on the available message converters for the java class type.

Nigel Longton's profile image
Nigel Longton

Daniel,

This works unless we add two produces entries like

@GetMapping(value = "/person", produces = {"application/json", "application/json;fmt=secondary"}

then the same behavior emerges.

We tried to subclass and override this method but there are too many private methods/fields for that to be easy.

A way to inject the decision logic would be nice :-). I still think the code is wrong to mutate the clients request.

We'll raise an issue at the link you provided and see what happens.

 

Nigel

Nigel Longton's profile image
Nigel Longton

Daniel

This issue https://github.com/spring-projects/spring-framework/issues/21670 looks related to ours.

Should we create a new one or just comment on this one?

Nigel

 

Daniel Mikusa's profile image
Daniel Mikusa

I'd suggest joining that discussion. It's already got some attention, so probably good to keep going there.