I'm working on an API that produces application/hal+json documents that use the media type profile parameter to support versioning. The structure of the controller looks like this:

@RestController
@RequestMapping("/hal-documents")
class MyController {
    @GetMapping(produces = ["application/hal+json;charset=UTF-8"])
    fun getWithoutProfile() : ResponseEntity<MyResource> {...}

    @GetMapping(produces = ["""application/hal+json;profile="my-resource-v1""""])
    fun getV1() : ResponseEntity<MyResource> {...}

    @GetMapping(produces = ["""application/hal+json;profile="my-resource-v2""""])
    fun getV2() : ResponseEntity<MyResource> {...}
}

If I execute a request, which Accept-Header exactly matches the media type produced by getV1, the method is not being called. Instead, the request is being routed to getWithoutProfile.

This seems to be wrong. I would expect that the best matching method is being called. Routing works correctly if the charset parameter is removed from the getWithoutProfile method.

Affects: 5.3.15

Comment From: thake

A similar problem seems to exist for the consumes property. Given the following controller:

@RestController
@RequestMapping("/hal-documents")
class MyController {
    @PostMapping(
        consumes = ["""application/hal+json;profile="my-resource-v1""""],
        produces = ["""application/hal+json;profile="my-resource-v1""""]
    )
    fun postVersion1(@RequestBody request : String) = "version-1"

    @PostMapping(
        consumes = ["""application/hal+json;profile="my-resource-v2""""],
        produces = ["""application/hal+json;profile="my-resource-v2""""]
    )
    fun postVersion2(@RequestBody request : String) = "version-2";
}

A request that provides a request body with the content type application/hal+json;profile="my-resource-v2" is being routed to postVersion1 but should be routed to postVersion2.

Comment From: rstoyanchev

There is a prior, related discussion in this https://github.com/spring-projects/spring-framework/issues/17949#issuecomment-453429220.

Generally speaking, a produces format is chosen mainly given the type and sub-type of the media type. Parameters provide additional information, but their meaning and relevance is unknown to the framework.

If produces declares a media type with a parameter in it, and if the same parameter is also present in the media type from the Accept header, we'll make sure the two match. In all other cases (parameter present in produces but not in Accept header or vice versa), parameters have no impact and effectively considered a match.

So even though charset is not in the Accept header (and shouldn't be I think, see #22788), it's still considered a match, and so effectively there is one matching parameter for each mapping. It's unclear which is or should be a more specific match. More generally, given a full range of cases with 1 or more of the same or different parameters on either side, it would be hard to imagine a good solution for matching and sorting based on parameters.

Note that for consumes we currently do not have the same matching where if the same parameter is present in both the consumes and the request content-type. It's something we could consider. On the produces side it was added for the case of app/atom+xml;type=feed vs app/atom+xml;type=entry, see #21670, but again for this to work, both produces conditions have to have the same parameter, and Accept header has to have it too.

Comment From: thake

@rstoyanchev thanks for your fast response and for pointing out the discussion of #17949. I've not found this thread in my research.

Generally speaking, a produces format is chosen mainly given the type and sub-type of the media type. Parameters provide > additional information, but their meaning and relevance is unknown to the framework.

Is there an easy way to provide the additional meaning of media type parameters to the framework? Somehow stating that the media type parameter should be part of content negotiation. Meaning that an Accept or Content-Type media type that has a parameter value different to the consumes/produces media type is not compatible. The only way I identified is by creating a custom ProducesRequestCondition which basically copy/pastes most of the ProducesRequestCondition and ConsumesRequestCondition and adds the special media type handling. This seems rather cumbersome and error-prone due to the complex matter of content negotiation.

So even though charset is not in the Accept header (and shouldn't be I think, see https://github.com/spring-projects/spring-framework/issues/22788), it's still considered a match, and so effectively there is one matching parameter for each mapping. It's unclear which is or should be a more specific match. More generally, given a full range of cases with 1 or more of the same or different parameters on either side, it would be hard to imagine a good solution for matching and sorting based on parameters.

I agree that the Accept / Content-Type media type for "+json" types implicitly contains the UTF-8 charset as json is by default UTF-8 (see https://datatracker.ietf.org/doc/html/rfc7159#section-8.1) if not otherwise explicitly stated. If we follow this line of argumentation, we can also assume, that specifying a "+json" produces/consumes media type without a charset matches the UTF-8 charset parameter. Thus application/hal+json;profile="my-resource-v1" matches 2 parameters from the Accept media type application/hal+json;profile="my-resource-v1". The first one is explicitly stated with profile and the second one charset is implicit. As application/hal+json;charset=UTF-8 only matches one parameter, it should be ranked lower.

Note that for consumes we currently do not have the same matching where if the same parameter is present in both the consumes and the request content-type. It's something we could consider. On the produces side it was added for the case of app/atom+xml;type=feed vs app/atom+xml;type=entry, see https://github.com/spring-projects/spring-framework/issues/21670, but again for this to work, both produces conditions have to have the same parameter, and Accept header has to have it too.

I'll open a separate issue for the consumes part, as the cause of the problem seems to be different. (update: created #28024)

Comment From: rstoyanchev

Short of something like the params condition, I don't see a way to expose this. This is however not something we're looking to introduce. Aside from introducing additional syntax and/or annotation attributes, what's lacking is an algorithm that would handle this in the general case.