30

I have two Spring MVC controller methods. Both receive the same data in the request body (in the format of an HTLM POST form: version=3&name=product1&id=2), but one method handles PUT requests and another DELETE:

@RequestMapping(value = "ajax/products/{id}", method = RequestMethod.PUT)
@ResponseBody
public MyResponse updateProduct(Product product, @PathVariable("id") int productId) {

//...
}

@RequestMapping(value = "ajax/products/{id}", method = RequestMethod.DELETE)
@ResponseBody
public MyResponse updateProduct(Product product, @PathVariable("id") int productId) {

//...
}

In the first method, all fields of the product argument are correctly initialised. In the second, only the id field is initialised. Other fields are null or 0. (id is, probably, initialised because of the id path variable).

I can see that the HttpServletRequest object contains values for all fields in the request body (version=3&name=product1&id=2). They just are not mapped to the fields of the product parameter.

How can I make the second method work?

I also tried to use the @RequestParam annotated parameters. In the method that handles PUT requests, it works. In the DELETE method, I get an exception: org.springframework.web.bind.MissingServletRequestParameterException: Required String parameter 'version' is not present.

I need to pass data in the body of DELETE requests because the data contain a row version which is used for optimistic locking.

Alex
  • 883
  • 4
  • 11
  • 23

5 Answers5

40

The problem is not a Spring problem, but a Tomcat problem.

By default, Tomcat will only parse arguments that are in the form style, when the HTTP method is POST (at least for version 7.0.54 that I checked but it's probably the same for all Tomcat 7 versions).

In order to be able to handle DELETE methods as well you need to set the parseBodyMethods attribute of the Tomcat Connector. The connector configuration is done in server.xml.

Your updated connector would most likely look like:

<Connector port="8080" protocol="HTTP/1.1" 
           connectionTimeout="20000"
           redirectPort="8443"
           parseBodyMethods="POST,PUT,DELETE"
           URIEncoding="UTF-8" />

Here is documentation page for configuring Tomcat connectors.

Once you setup Tomcat to parse the parameters, Spring will work just fine (although in your case you will probably need to remove @RequestBody from the controller method)

geoand
  • 60,071
  • 24
  • 172
  • 190
  • Thanks! It explains my problem. I will upvote this answer when I get 3 more points of reputation. But I would rather pull the `version` from `HttpServletRequest` directly than mess up with Tomcat configuration files. – Alex Aug 19 '14 at 12:32
  • No problem! The problem is that you wont be able to pull `version` from `HttpServletRequest.getParameter` either since Tomcat has never populated the necessary data structures with that info. – geoand Aug 19 '14 at 12:35
  • Also changing the connector in `server.xml` is really no big deal. Probably all you will want to do is add `parseBodyMethods="POST,PUT,DELETE"` (or change it if it already exists) – geoand Aug 19 '14 at 12:39
  • I am currently using the `request.getInputStream()` method for debugging. Not an elegant solution, but it works :) – Alex Aug 19 '14 at 12:41
  • @Alex Yes that will definitely work :). But it would probably be best of you avoid it :) :) – geoand Aug 19 '14 at 12:41
  • 1
    In the end, I used the following solution: I added `@RequestBody MultiValueMap formData` parameter to the controller method, and then got the `version` field: `String version = formData.get("version").get(0)`. For this to work, `FormHttpMessageConverter` should be registered. This message converter is registered by default if Java Config or `` tag is used. I'll wait a bit for comments from others, and then will accept this answer if nobody says anything new. – Alex Aug 19 '14 at 14:02
  • What about HttpPutFormContentFilter? – mvmn Mar 03 '16 at 12:39
3

You can try adding the annotation @RequestBody to your Product argument.

But if you just need to pass version information, using a request param is more appropriate.

So add a new argument in your delete method @RequestParam("version") int version, and when calling the delete method pass a query param like ..ajax/products/123?version=1

As you said request param is not working for you in delete, can you post the exact url you used and the method signature ?

coder
  • 4,458
  • 2
  • 17
  • 23
  • I cannot pass the version in the URL, because I have to keep my URLs RESTful. My request body looks like this: `version=3&name=product1&id=2` (it's the format of a POST HTML form I think). – Alex Aug 19 '14 at 12:16
  • There is a reason why I do not use `@RequestBody` in my `PUT` and `POST` controller methods: using no `@RequestBody` makes processing data binding errors more simple (you can process them the same way as validation errors). Data binding errors are errors that happen when you put a string in an integer field, for example. – Alex Aug 19 '14 at 12:17
  • If I understand it correctly, `@RequestBody` would bind the incoming form to the `product` object with a "message converter", and any data binding error would result in a hard-to-process exception (at least it's the case when you use `@RequestBody` for JSON data). On the other hand, I do not need to worry about data binding errors for the `DELETE` method, so I will try to use `@RequestBody`. – Alex Aug 19 '14 at 12:17
  • +1 for suggesting the `@RequestBody` annotation. However, it looks like there is no message converter that can convert an `application/x-www-form-urlencoded` form to a Product object (I found only a message converter for `MultiValueMap`). So, I think, for `@RequestBody Product product` to work, I would have to write my own message converter. – Alex Aug 19 '14 at 13:53
2

Spring boot 1.5.*

@Bean
public TomcatEmbeddedServletContainerFactory tomcatEmbeddedServletContainerFactory() {
    return new TomcatEmbeddedServletContainerFactory(){
        @Override
        protected void customizeConnector(Connector connector) {
            super.customizeConnector(connector);
            connector.setParseBodyMethods("POST,PUT,DELETE");
        }
    };
}
GreatC
  • 342
  • 4
  • 9
1

Passing data in the body of a DELETE request

@Component public class CustomiseTomcat implements WebServerFactoryCustomizer {

@Override
public void customize(TomcatServletWebServerFactory factory) {

    factory.addConnectorCustomizers( new TomcatConnectorCustomizer() {
        @Override
        public void customize(Connector connector) {
            connector.setParseBodyMethods("POST,PUT,DELETE");
        }
    });
}

}

KESHAV KUMAR
  • 140
  • 1
  • 7
1

for spring boot 2.0+ :

@Bean
public TomcatServletWebServerFactory containerFactory() {
    return new TomcatServletWebServerFactory() {
        @Override
        protected void customizeConnector(Connector connector) {
            super.customizeConnector(connector);
            connector.setParseBodyMethods("POST,PUT,DELETE");
        }
    };
}
  • Hi, java_pickle , welcome to Stack Overflow. Some explanation with the answer is very much appreciable. It will help to understand the objective of code. Happy coding! – Abdur Rahman Jul 02 '20 at 08:26