Spring Boot 2.3 Ambiguous Mapping

Viewed 408

I have a spring controller with two endpoints that are returning an arbitrary method exception when they are accessed. I am trying to distinguish between them using the HeaderContentNegotiationStrategy which looks at the Accept header of the incoming request to determine which method to map the request to. To my understanding this strategy should compare the incoming accept header with the produces notation. In my case both methods do produce an application/json media type, however Spring also lets you provide a consumes which to my reading of their doc, should also have the content negotiation look at the request's Content-Type header to map the method.

My question is, how do I get the content negotiation to look at the Content-Type header as well when doing this mapping? If you cannot get the content negotiator to look at the consumes notation, why is it there? Whats the point?

Controller:

@ResourcePayloadOverride(action = ActionTypes.READ)
@CustomPermission(Permission.READ)
@RequestMapping(method = RequestMethod.POST,
        produces = { MediaType.APPLICATION_JSON_VALUE, ResourceCollection.MEDIA_TYPE_JSON_VALUE },
        consumes = MediaType.TEXT_PLAIN_VALUE)
public ResponseEntity<ResourceCollection<Class1>> method1(
        @PathVariable(value="param_1") String param1,
        @RequestParam(value="param_2", required=false) long param2,
        @RequestParam(value="param_3", required=false) int param3,
        @RequestParam(value="param_4", required=false) String param4,
        @RequestBody(required=false) String body) {
    [...]
}

@RequestMapping(method = { RequestMethod.POST },
        produces = { MediaType.APPLICATION_JSON_VALUE, Class1.MEDIA_TYPE_JSON_VALUE },
        consumes = { Class1.MEDIA_TYPE_JSON_VALUE, Class2.MEDIA_TYPE_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE})
public ResponseEntity<Class1> post(
        @PathVariable("param1") String param1,
        @RequestParam(value="param2", required = false) String param2,
        @RequestParam(value="param3", required = false) String param3,
        @RequestParam(value="param4", required = false) String param4,
        @RequestBody(required=false) String noop) //Actually needed!!!
{
    [...]
}

Error:

{"version":1,"timeStamp":"2021-05-13T16:30:33.785Z","level":"error","source":"","message":"java.lang.IllegalStateException: Ambiguous handler methods mapped for '/{my_endpoint}': {public org.springframework.http.ResponseEntity {myPackage.class}.method1(java.lang.String,long,int,java.lang.String,java.lang.String), public org.springframework.http.ResponseEntity {myPackage.class}.post(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String)}","messageParameters":{"0":"java.lang.IllegalStateException","1":"Ambiguous handler methods mapped for '/{my_endpoint}': {public org.springframework.http.ResponseEntity {myPackage.class}.method1(java.lang.String,long,int,java.lang.String,java.lang.String), public org.springframework.http.ResponseEntity {myPackage.class).post(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String)}"

I've replaced a number of details with dummy values here but I dont believe this should effect the larger question

1 Answers

Simplifying the problem: Distinguish an endpoint based on consumes


import org.springframework.web.bind.annotation.RequestParam;

@RestController
class ConsumeController {
    @PostMapping(value = "/post", consumes = MediaType.APPLICATION_JSON_VALUE)
    public String postJson(@RequestBody(required = false) String data) {
        return "json: " + data;
    }

    @PostMapping(value = "/post", consumes = MediaType.TEXT_PLAIN_VALUE)
    public String postText(@RequestBody(required = false) String data) {
        return "text: " + data;
    }
}

That will do what is expected when there is actually data provided:

curl -d "{'x':'y'}" -H "Content-Type:application/json" -X POST http://localhost:8080/post
json: {'x':'y'}

curl -d "hello" -H "Content-Type:text/plain" -X POST http://localhost:8080/post
text: hello

But that won't work when no data is sent:

curl -H "Content-Type:application/json" -X POST http://localhost:8080/post
{...error: Ambiguous handler methods mapped for '/post'}

The request cannot be distinguished when there is nothing to consume ...

Possible workarounds:

  • make the data required @RequestBody(required = true) String data and handle empty data
  • make your endpoint unique by with an extra header like @PostMapping(value= "/post", consumes = MediaType.TEXT_PLAIN_VALUE, headers="allow=text")
curl -H "Content-Type:text/plain" -H "allow:text"  -X POST http://localhost:8080/post
text: null
Related