spring-boot: @RequestMapping Content-Type error when a @RequestBody is used but Content-Type header is not given

We are trying to upgrade to 1.3.0.M1 from 1.2.1. When using the 1.2.1 version, we did not have to set the Content-Type: application/json header when sending a request body to a method web service endpoint like:

   @RequestMapping(value = "filter", method = RequestMethod.POST)
    List<StateData> filter(@RequestBody StateData filter) {
    }

The Content-Type would be With version 1.3.0.M1, the call will fail when passing a @RequestBody if the Content-Type header is not set, with the error:

org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'application/octet-stream' not supported at
org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver.readWithMessageConverters(AbstractMessageConverterMethodArgumentResolver.java:216) at
org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.readWithMessageConverters(RequestResponseBodyMethodProcessor.java:148) at
org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.resolveArgument(RequestResponseBodyMethodProcessor.java:126) at
org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:77) at
org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:162) at
org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:129) at
org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:111) at
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandleMethod(RequestMappingHandlerAdapter.java:799) at
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:728) at
org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85) at 
org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:959)
    ... 67 more

About this issue

  • Original URL
  • State: closed
  • Created 9 years ago
  • Comments: 24 (11 by maintainers)

Commits related to this issue

Most upvoted comments

for my case the headers must be :

Content-Type: application/json and i solve the problem good luck

Hi @lewisdavidcole

Could you give more details about this issue please? Especially, HTTP headers for request and response. In any case, what you’re describing here looks normal - application/octet-stream is a sensible default for "Content-Type" if none is sent by the client. What HTTP client are you using?

I’ve just reproduced this exact behavior in Spring Boot 1.2.1, with the following:

@RestController
public class TestController {

    @RequestMapping(value = "/test", method = RequestMethod.POST)
    public String test(@RequestBody Person person) {
        return "Hello " + person.getName();
    }

    private static class Person {
        private String name;

        public Person() {
        }

        public Person(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }
    }
}
curl --data '{"name":"test"}' -v -X POST -H 'Content-Type:' http://localhost:8080/test

> POST /test HTTP/1.1
> User-Agent: curl/7.37.1
> Host: localhost:8080
> Accept: */*
> Content-Length: 15


< HTTP/1.1 415 Unsupported Media Type
< Server: Apache-Coyote/1.1
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Wed, 24 Jun 2015 09:20:22 GMT
<

{"timestamp":1435137622778,"status":415,"error":"Unsupported Media Type","exception":"org.springframework.web.HttpMediaTypeNotSupportedException","message":"Content type 'application/octet-stream' not supported","path":"/test"}%
curl --data '{"name":"test"}' -v -X POST -H 'Content-Type:application/json' http://localhost:8080/test

> POST /test HTTP/1.1
> User-Agent: curl/7.37.1
> Host: localhost:8080
> Accept: */*
> Content-Type:application/json
> Content-Length: 15
>

< HTTP/1.1 200 OK
< Server: Apache-Coyote/1.1
< Content-Type: text/plain;charset=UTF-8
< Content-Length: 10
< Date: Wed, 24 Jun 2015 09:23:46 GMT
<

Hello test%

Maybe do you have a custom ContentNegotiationStrategy that sets the defaultContentType? I don’t think Boot is doing anything like this.

Thanks for looking deeper into the behavior. I think there is one aspect missing: The input to the controller method is marked as optional:

@RestController
@RequestMapping(value = "sample")
class SampleController {

    @RequestMapping(value = "filter", method = [RequestMethod.GET, RequestMethod.POST])
    SampleData filter(@RequestBody(required = false) SampleData filter) {
       filter ?: new SampleData()
    }
}

There are 2 important parts to the method signature:

  1. It supports both GET and POST
  2. the @RequestBody(required=false) makes the data optional

There is nothing special about the SampleData object, but note that all my code is in Groovy:

class SampleData {
    String sampleString1 = "default"
    String sampleString2 = "default"
}

I created these in a separate sample project to remove any inadvertent configuration or code that would affect the outcome. If you are interested, here is my build.gradle file (using 3.0.0M1):

group 'com.rbs'
version '1.0-SNAPSHOT'

task wrapper(type: Wrapper) {
  gradleVersion = '2.4'
  distributionUrl = "https://services.gradle.org/distributions/gradle-$gradleVersion-all.zip"
}

buildscript {
    repositories {
        jcenter()
        maven { url "http://repo.spring.io/snapshot" }
        maven { url "http://repo.spring.io/milestone" }
    }
    dependencies {
        classpath   "org.springframework.boot:spring-boot-gradle-plugin:1.3.0.M1"
    }
}

repositories {
    jcenter()
    maven { url "http://repo.spring.io/snapshot" }
    maven { url "http://repo.spring.io/milestone" }
}

apply plugin: "spring-boot"
apply plugin: "idea"
apply plugin: "groovy"

sourceCompatibility = 1.8

dependencies {
    compile 'org.codehaus.groovy:groovy-all',
            'org.springframework.boot:spring-boot-starter-web'

}

springBoot { mainClass = "com.rbs.TestApp" }

Using the sample project and only cURL instead of PostMan, you ca see the results and URL’s used at the bottom of the post.

Summary

The results are pretty clear, there is an obvious change in behavior, which I think has degraded.

Version 1.2.1

GET requests don’t accept a response body, so the Content-Type is never required. POST requests - ONLY if data is set on the request body is the Content-Type needed.

Version 3.0.0M1

GET requests where the controller method has an optional @RequestBody requires the Content-Type to be set, whether or not the data is set on the request body. Note that in the example Controller method, the method supports both GET and POST requests, with the @RequestBody’s required attribute set false.

POST requests - The Content-Type is always required whether the request body is set or not.

I believe the way version 1.2.1 functions is the correct behavior. Because the @RequestBody can be an optional parameter, the Content-Type should not be required if the request body is not set, regardless of the request type.

Just for fyi… we have the filter endpoint to support both GET and POST requests because we allow the filter to be called without criteria as a GET to retrieve all results possible.

cURL Results

Spring Boot 1.3.0.M1

GET request with Content-Type set

curl -X GET -H “Content-Type: application/json” http://localhost:8080/TestApp/sample/filter

Connected to localhost (127.0.0.1) port 8080 (#0) GET /TestApp/sample/filter HTTP/1.1 User-Agent: curl/7.39.0 Host: localhost:8080 Accept: / Content-Type: application/json

< HTTP/1.1 200 OK < Server: Apache-Coyote/1.1 < X-Application-Context: application:8080 < Content-Type: application/json;charset=UTF-8 < Transfer-Encoding: chunked < Date: Mon, 29 Jun 2015 18:49:33 GMT < {“sampleString1”:“default”,“sampleString2”:“default”}* Connection #0 to host localhost left intact

GET request with no Content-Type set

curl -v -X GET http://localhost:8080/TestApp/sample/filter

GET /TestApp/sample/filter HTTP/1.1 User-Agent: curl/7.39.0 Host: localhost:8080 Accept: /

< HTTP/1.1 415 Unsupported Media Type < Server: Apache-Coyote/1.1 < X-Application-Context: application:8080 < Content-Type: application/json;charset=UTF-8 < Transfer-Encoding: chunked < Date: Mon, 29 Jun 2015 20:00:38 GMT < {“timestamp”:“2015-06-29T20:00:38.582+0000”,“status”:415,“error”:“Unsupported Media Type”,“exception “:“org.springframework.web.HttpMediaTypeNotSupportedException”,“message”:“Content type ‘application/ octet-stream’ not supported”,“path”:”/TestApp/sample/filter”}* Connection #0 to host localhost left intact

POST with no Content-Type and no request body

curl -v -X POST http://localhost:8080/TestApp/sample/filter

POST /TestApp/sample/filter HTTP/1.1 User-Agent: curl/7.39.0 Host: localhost:8080 Accept: /

< HTTP/1.1 415 Unsupported Media Type < Server: Apache-Coyote/1.1 < X-Application-Context: application:8080 < Content-Type: application/json;charset=UTF-8 < Transfer-Encoding: chunked < Date: Mon, 29 Jun 2015 18:55:39 GMT < {“timestamp”:“2015-06-29T18:55:39.344+0000”,“status”:415,“error”:“Unsupported Media Type”,“exception “:“org.springframework.web.HttpMediaTypeNotSupportedException”,“message”:“Content type ‘application/ octet-stream’ not supported”,“path”:”/TestApp/sample/filter”}* Connection #0 to host localhost left intact

POST with no Content-Type and a request body

curl --data “{"sampleString1":"test"}” -v -X POST "http://localhost:8080/TestApp/sample/filter

POST /TestApp/sample/filter HTTP/1.1 User-Agent: curl/7.39.0 Host: localhost:8080 Accept: / Content-Length: 24 Content-Type: application/x-www-form-urlencoded

upload completely sent off: 24 out of 24 bytes < HTTP/1.1 415 Unsupported Media Type < Server: Apache-Coyote/1.1 < X-Application-Context: application:8080 < Content-Type: application/json;charset=UTF-8 < Transfer-Encoding: chunked < Date: Mon, 29 Jun 2015 20:13:27 GMT < {“timestamp”:“2015-06-29T20:13:27.388+0000”,“status”:415,“error”:“Unsupported Media Type”,“exception “:“org.springframework.web.HttpMediaTypeNotSupportedException”,“message”:“Content type ‘application/ x-www-form-urlencoded;charset=UTF-8’ not supported”,“path”:”/TestApp/sample/filter”}* Connection #0 to host localhost left intact

POST with Content-Type set and no request body

curl -v -X POST -H “Content-Type: application/json” http://localhost:8080/TestApp/sample/filter

POST /TestApp/sample/filter HTTP/1.1 User-Agent: curl/7.39.0 Host: localhost:8080 Accept: / Content-Type: application/json

< HTTP/1.1 200 OK < Server: Apache-Coyote/1.1 < X-Application-Context: application:8080 < Content-Type: application/json;charset=UTF-8 < Transfer-Encoding: chunked < Date: Mon, 29 Jun 2015 18:56:27 GMT < {“sampleString1”:“default”,“sampleString2”:“default”}* Connection #0 to host localhost left intact

POST with Content-Type set and a request body

curl --data “{"sampleString1":"test"}” -v -X POST -H “Content-Type: application/json” “http://localhost:8080/TestApp/sample/filter

POST /TestApp/sample/filter HTTP/1.1 User-Agent: curl/7.39.0 Host: localhost:8080 Accept: / Content-Type: application/json Content-Length: 24

< upload completely sent off: 24 out of 24 bytes < HTTP/1.1 200 OK < Server: Apache-Coyote/1.1 < X-Application-Context: application:8080 < Content-Type: application/json;charset=UTF-8 < Transfer-Encoding: chunked < Date: Mon, 29 Jun 2015 19:57:45 GMT < {“sampleString1”:“test”,“sampleString2”:“default”}* Connection #0 to host localhost left intact

Spring Boot 1.2.1

GET request with Content-Type set

curl -v -X GET -H “Content-Type: application/json” http://localhost:8080/TestApp/sample/filter

GET /TestApp/sample/filter HTTP/1.1 User-Agent: curl/7.39.0 Host: localhost:8080 Accept: / Content-Type: application/json

< HTTP/1.1 200 OK < Server: Apache-Coyote/1.1 < Content-Type: application/json;charset=UTF-8 < Transfer-Encoding: chunked < Date: Mon, 29 Jun 2015 20:20:49 GMT < {“sampleString1”:“default”,“sampleString2”:“default”}* Connection #0 to host localhost left intact

GET request with no Content-Type set

curl -v -X GET http://localhost:8080/TestApp/sample/filter

GET /TestApp/sample/filter HTTP/1.1 User-Agent: curl/7.39.0 Host: localhost:8080 Accept: /

< HTTP/1.1 200 OK < Server: Apache-Coyote/1.1 < Content-Type: application/json;charset=UTF-8 < Transfer-Encoding: chunked < Date: Mon, 29 Jun 2015 20:21:23 GMT < {“sampleString1”:“default”,“sampleString2”:“default”}* Connection #0 to host localhost left intact

POST with no Content-Type and no request body

curl -v -X POST http://localhost:8080/TestApp/sample/filter

POST /TestApp/sample/filter HTTP/1.1 User-Agent: curl/7.39.0 Host: localhost:8080 Accept: /

< HTTP/1.1 200 OK < Server: Apache-Coyote/1.1 < Content-Type: application/json;charset=UTF-8 < Transfer-Encoding: chunked < Date: Mon, 29 Jun 2015 20:24:14 GMT < {“sampleString1”:“default”,“sampleString2”:“default”}* Connection #0 to host localhost left intact

POST with no Content-Type and a request body

curl --data “{"sampleString1":"test"}” -v -X POST “http://localhost:8080/TestApp/sample/filter

POST /TestApp/sample/filter HTTP/1.1 User-Agent: curl/7.39.0 Host: localhost:8080 Accept: / Content-Length: 24 Content-Type: application/x-www-form-urlencoded

  • upload completely sent off: 24 out of 24 bytes < HTTP/1.1 415 Unsupported Media Type < Server: Apache-Coyote/1.1 < Content-Type: application/json;charset=UTF-8 < Transfer-Encoding: chunked < Date: Mon, 29 Jun 2015 20:25:35 GMT < {“timestamp”:“2015-06-29T20:25:35.601+0000”,“status”:415,“error”:“Unsupported Media Type”,“exception “:“org.springframework.web.HttpMediaTypeNotSupportedException”,“message”:“Content type ‘application/ x-www-form-urlencoded;charset=UTF-8’ not supported”,“path”:”/TestApp/sample/filter”}* Connection #0 to host localhost left intact
POST with Content-Type set and no request body

curl -v -X POST -H “Content-Type: application/json” http://localhost:8080/TestApp/sample/filter

POST /TestApp/sample/filter HTTP/1.1 User-Agent: curl/7.39.0 Host: localhost:8080 Accept: / Content-Type: application/json

< HTTP/1.1 200 OK < Server: Apache-Coyote/1.1 < Content-Type: application/json;charset=UTF-8 < Transfer-Encoding: chunked < Date: Mon, 29 Jun 2015 20:22:23 GMT < {“sampleString1”:“default”,“sampleString2”:“default”}* Connection #0 to host localhost left intact

POST with Content-Type set and a request body

curl --data “{"sampleString1":"test"}” -v -X POST -H “Content-Type: application/json” “http://localhost:8080/TestApp/sample/filter

POST /TestApp/sample/filter HTTP/1.1 User-Agent: curl/7.39.0 Host: localhost:8080 Accept: / Content-Type: application/json Content-Length: 24

upload completely sent off: 24 out of 24 bytes < HTTP/1.1 200 OK < Server: Apache-Coyote/1.1 < Content-Type: application/json;charset=UTF-8 < Transfer-Encoding: chunked < Date: Mon, 29 Jun 2015 20:26:55 GMT < {“sampleString1”:“test”,“sampleString2”:“default”}* Connection #0 to host localhost left intact

I met the same problem. Did you solve it? How did you solve it?

specify @JsonProperty in entity class constructor like this.

 @JsonCreator
 public Location(@JsonProperty("sl_no") Long sl_no, 
            @JsonProperty("location")String location,
            @JsonProperty("location_type") String location_type,
            @JsonProperty("store_sl_no")Long store_sl_no) {
    this.sl_no = sl_no;
    this.location = location;
    this.location_type = location_type;
    this.store_sl_no = store_sl_no;
} 

It’s not a Content-Type header check but rather a failure to find a suitable HttpMessageConverter. The StringHttpMessageConverter can deal with “text/plain” but it only converts to String. The JacksonHttpMessageConverter can convert to SNSMessage but it only gets involved for application/json. You could configure the JacksonHttpMessageConverter to also convert text/plain but that could cause you issues in other endpoints. I do not see what we could do in Spring MVC to make this better. We are in a worse position to make assumptions.

Probably the simplest reliable thing to do is to insert the request body as a String and invoke the Jackson converter manually. This is just a weird situation to begin with. Another option might be to use a custom ContentNegotiationStrategy that resolves the content type for very specific URLs. Then you can keep your method signature as it is.