Oliver Drotbohm opened SPR-14414 and commented

When a controller returns a Resource implementation to be served to clients and the client didn't send any preferred media type (either through the URI or via the Accept header).

The culprit is that the content negotiation calculates all compatible media types first and for that consults the HttpMessageConverter's supported media types. The one handling Resource instances returns */*. The converter backed by Jackson returns a more concrete media type and thus the converter handling Resource instances is called with a media type. That in turn causes the media type detection to be turned off as it's only triggered as fallback, i.e. when no media type is given in the first place — which is never the case in the server side use case.

As discussed with Rossen Stoyanchev, a potential solution could be to hand the value (or at least the type of the value) to an overload of getSupportedMediaTypes() so that the converter can return a more concrete media type for the object about to be rendered.


Affects: 4.3 GA

Issue Links: - #18980 Allow defining custom file extension to media type mappings in ResourceHttpMessageConverter - #19603 BufferedImageHttpMessageConverter cannot convert to a media type it claims it can

Comment From: spring-projects-issues

Rossen Stoyanchev commented

Indeed at present it seems the ability of the ResourceHttpMessageConverter to determine the content type of a resource through JAF is highly unlikely to ever get used on the server side. It's an internal-only mechanism, i.e. at the HttpMessageConverter level it always returns */* and never something more concrete. The only way it can be used is as a fallback mechanism via getDefaultContentType in case we failed to find a concrete compatible media type, which is highly unlikely.

The only way this probably works in practice is when the URL contains an extension, which results in a concrete "acceptable" media type. That's a good solution but it can't always be used. For example, as in Oliver's case when the path is an id used to load a resource. Also the combination of extension pattern matching + URI variables can be a challenge in its own right. You could rely on explicitly registered extensions to determine reliably if a dot signifies an extension or not but the range of expected extensions may be large or unknown.

Ideally we need to give HttpMessageConverter implementations a chance to return "producible" media types taking into account the Object to be written. So just like canWrite takes the Object as input, so should getSupportedMediaTypes.

This applies also to the Spring 5 reactive Encoder which currently has a similar getEncodableMimeTypes() method. The change should be considered for both places.

/cc Arjen Poutsma, Sébastien Deleuze

Comment From: spring-projects-issues

Rossen Stoyanchev commented

Oliver Drotbohm, on second thought the proposed fix could be problematic. At present the ResourceHttpMessageConverter is designed to render a Resource as whatever media type is requested. I'm afraid making the converter proactively "propose" the media type of the underlying resource can cause regressions. Take for example an XML resource. At present if the client asks for "text/plain" they would get a 200 response with "text/plain" as expected. If the converter is changed to return "application/xml" instead of MediaType.ALL as the producible type, the response would be 406.

When an @RequestMapping method is picked based on its producible condition, the producible media type is enforced through a request attribute. In your case however the producible type depends on the actual resource. That means you also can't use ContentNegotiationConfigurer#defaultContentTypeStrategy which only gives you access to the request -- well, technically you could but you would have to retrieve the resource first. For your case, after retrieving the resource in the controller method you could set the request attribute RequestMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, which is what the ProducesRequestCondition does.

Comment From: spring-projects-issues

Oliver Drotbohm commented

I might be missing a detail here but my original premise was that the client doesn't send any Accept header at all, indicating that it requests the resource no matter what representation it serves by default.

Take for example an XML resource. At present if the client asks for "text/plain" they would get a 200 response with "text/plain" as expected.

I don't necessarily agree on the "as expected" part. Is sending an XML document back for a request with an explicit Accept: text/plain a good (even valid?) response? If so, based on what criteria? We (as humans) know that XML is a textual format but on what criteria do you decide that? Is there a formally maintained mapping of "compatible" media types beyond the media type compatibility rules (in which text/plain is not compatible with application/xml) of media types in general? Assume it's rather a PNG file that's requested, would we still serve it and use text/plain as Content-Type header?

If the converter is changed to return "application/xml" instead of MediaType.ALL as the producible type, the response would be 406.

I'd argue that that's the more appropriate response. It's not that text/plain is a catch-all for "everything that can be interpreted as text". If the client wants to express that, It would either add more options (i.e. Accept: text/plain, */*) or use a more liberal version (i.e. text/* — as that'd map text/xml, too) in the first place, wouldn't it?

Comment From: spring-projects-issues

Rossen Stoyanchev commented

I was trying to make a larger point that ResourceHttpMessageConverter is designed to render a Resource as whatever content type it is asked to (the same is also true for the String and byte array message converters). Whether that flexibility is a good thing or not we can debate but my point is that if we make it proactively suggest the content type to use, we're almost guaranteed to cause a regression where someone is relying on that flexibility, or where the content type we determine is close enough but not exactly the same as the requested content type.

I suppose the key here is that the client has not specified any content type and we could check whether the client has not specified any content type but this is not so easy since there are different ways to specify a content type. We would probably use the ContentNegotiationManager which could return a default content type for example making it difficult to know that the client did not specify anything.

This is why I was going back to where we started and trying to take a fresh look. It strikes me that your use case is somewhat unusual in that it is a generic URL that resolves to some resource where neither the URL nor the Accept header reveal the content type of the resource. You actually are in a position to know that the content type is and should be used, so it seems alright to fill in the producible type. I would of course prefer a different solution but I don't see one yet.