Spring MVC - Why not able to use @RequestBody and @RequestParam together

111,967

Solution 1

The @RequestBody javadoc states

Annotation indicating a method parameter should be bound to the body of the web request.

It uses registered instances of HttpMessageConverter to deserialize the request body into an object of the annotated parameter type.

And the @RequestParam javadoc states

Annotation which indicates that a method parameter should be bound to a web request parameter.

  1. Spring binds the body of the request to the parameter annotated with @RequestBody.

  2. Spring binds request parameters from the request body (url-encoded parameters) to your method parameter. Spring will use the name of the parameter, ie. name, to map the parameter.

  3. Parameters are resolved in order. The @RequestBody is processed first. Spring will consume all the HttpServletRequest InputStream. When it then tries to resolve the @RequestParam, which is by default required, there is no request parameter in the query string or what remains of the request body, ie. nothing. So it fails with 400 because the request can't be correctly handled by the handler method.

  4. The handler for @RequestParam acts first, reading what it can of the HttpServletRequest InputStream to map the request parameter, ie. the whole query string/url-encoded parameters. It does so and gets the value abc mapped to the parameter name. When the handler for @RequestBody runs, there's nothing left in the request body, so the argument used is the empty string.

  5. The handler for @RequestBody reads the body and binds it to the parameter. The handler for @RequestParam can then get the request parameter from the URL query string.

  6. The handler for @RequestParam reads from both the body and the URL query String. It would usually put them in a Map, but since the parameter is of type String, Spring will serialize the Map as comma separated values. The handler for @RequestBody then, again, has nothing left to read from the body.

Solution 2

It's too late to answer this question, but it could help for new readers, It seems version issues. I ran all these tests with spring 4.1.4 and found that the order of @RequestBody and @RequestParam doesn't matter.

  1. same as your result
  2. same as your result
  3. gave body= "name=abc", and name = "abc"
  4. Same as 3.
  5. body ="name=abc", name = "xyz,abc"
  6. same as 5.

Solution 3

It happens because of not very straight forward Servlet specification. If you are working with a native HttpServletRequest implementation you cannot get both the URL encode body and the parameters. Spring does some workarounds, which make it even more strange and nontransparent.

In such cases Spring (version 3.2.4) re-renders a body for you using data from the getParameterMap() method. It mixes GET and POST parameters and breaks the parameter order. The class, which is responsible for the chaos is ServletServerHttpRequest. Unfortunately it cannot be replaced, but the class StringHttpMessageConverter can be.

The clean solution is unfortunately not simple:

  1. Replacing StringHttpMessageConverter. Copy/Overwrite the original class adjusting method readInternal().
  2. Wrapping HttpServletRequest overwriting getInputStream(), getReader() and getParameter*() methods.

In the method StringHttpMessageConverter#readInternal following code must be used:

    if (inputMessage instanceof ServletServerHttpRequest) {
        ServletServerHttpRequest oo = (ServletServerHttpRequest)inputMessage;
        input = oo.getServletRequest().getInputStream();
    } else {
        input = inputMessage.getBody();
    }

Then the converter must be registered in the context.

<mvc:annotation-driven>
    <mvc:message-converters register-defaults="true/false">
        <bean class="my-new-converter-class"/>
   </mvc:message-converters>
</mvc:annotation-driven>

The step two is described here: Http Servlet request lose params from POST body after read it once

Share:
111,967
abhihello123
Author by

abhihello123

A developer who worked on so many technologies(C# ASP.net, Cocoa, iPhone, C, Java, Oracle etc.) that he couldnt learn one technology :( Now concentrating on core J2EE technologies.

Updated on July 12, 2022

Comments

  • abhihello123
    abhihello123 almost 2 years

    Using HTTP dev client with Post request and Content-Type application/x-www-form-urlencoded

    1) Only @RequestBody

    URL: localhost:8080/SpringMVC/welcome
    Body: name=abc

    @RequestMapping(method = RequestMethod.POST)
    public String printWelcome(@RequestBody String body, Model model) {
        model.addAttribute("message", body);
        return "hello";
    }
    // Gives body as 'name=abc' as expected
    

    2) Only @RequestParam

    URL: localhost:8080/SpringMVC/welcome
    In Body - name=abc

    @RequestMapping(method = RequestMethod.POST)
    public String printWelcome(@RequestParam String name, Model model) {
        model.addAttribute("name", name);
        return "hello";
    }
    // Gives name as 'abc' as expected
    

    3) Both together

    URL: localhost:8080/SpringMVC/welcome
    Body: name=abc

    @RequestMapping(method = RequestMethod.POST)
    public String printWelcome(
        @RequestBody String body, 
        @RequestParam String name, Model model) 
    {
        model.addAttribute("name", name);
        model.addAttribute("message", body);
        return "hello";
    }
    // HTTP Error Code 400 - The request sent by the client was syntactically incorrect.
    

    4) Above with params position changed

    URL: localhost:8080/SpringMVC/welcome
    Body: name=abc

    @RequestMapping(method = RequestMethod.POST)
    public String printWelcome(
        @RequestParam String name, 
        @RequestBody String body, Model model) 
    {
        model.addAttribute("name", name);
        model.addAttribute("message", body);
        return "hello";
    }
    // No Error. Name  is 'abc'. body is empty
    

    5) Together but get type url parameters

    URL: localhost:8080/SpringMVC/welcome?name=xyz
    Body: name=abc

    @RequestMapping(method = RequestMethod.POST)
    public String printWelcome(
        @RequestBody String body, 
        @RequestParam String name, Model model) 
    {
        model.addAttribute("name", name);
        model.addAttribute("message", body);
        return "hello";
    }
    // name is 'xyz' and body is 'name=abc'
    

    6) Same as 5) but with parameters position changed

    @RequestMapping(method = RequestMethod.POST)
    public String printWelcome(
        @RequestParam String name, 
        @RequestBody String body, Model model) 
    {
        model.addAttribute("name", name);
        model.addAttribute("message", body);
        return "hello";
    }
    // name = 'xyz,abc' body is empty
    

    Can someone explain this behaviour?

  • abhihello123
    abhihello123 over 10 years
    Suppose for case 4), Post Body is name=abc&age=2, then according to your explanation name should be abc and body should be &age=2 but actual result is name is abc and body is empty.
  • Sotirios Delimanolis
    Sotirios Delimanolis over 10 years
    @abhihello123 It reads the whole url encoded form parameters. So the whole name=abc&age=2.
  • abhihello123
    abhihello123 over 10 years
    but if I put another @RequestParam it puts 2 into age. That means name reads only abc. sorry for bugging.
  • Sotirios Delimanolis
    Sotirios Delimanolis over 10 years
    @abhihello123 Yes, in that case, the full query string has been read and the next @RequestParam can be built from it. The parameters are stored in a map that can be checked each time for request params.
  • abhihello123
    abhihello123 over 10 years
    Thank You. Was reading spring docs but these points were not that clear.
  • Sotirios Delimanolis
    Sotirios Delimanolis over 10 years
    @abhihello123 If you have a lot of patience. Download the source jars and use a debugger while sending a request and step through the code. The class you want to see is RequestParamMethodArgumentResolver, which does handling for @RequestParam.
  • naXa stands with Ukraine
    naXa stands with Ukraine about 9 years
    Yes, response status code 400 won't be generated. But he also won't be able to get the value of @RequestParam (if it's passed) anyway!