jpappe
jpappe

Reputation: 375

Spring MVC: @RequestBody when no content-type is specified

I have a Spring MVC application that receives an HTTP request from an external system in the form of a JSON string, and its response is returned similarly as a JSON string. My controller is correctly annotated with @RequestBody and @ResponseBody and I have integration tests that actually send requests to verify that everything works as expected.

However, when I went to test my application against the actual external system that will be using it, I discovered that the incoming requests to not specify a content-type! This completely confuses Spring and results in the following types of errors:

DEBUG [] 2014-04-17 13:33:13,471 AbstractHandlerExceptionResolver.java:132 resolveException - Resolving exception from handler [com.example.controller.MyController@1d04f0a]: org.springframework.web.HttpMediaTypeNotSupportedException: Cannot extract parameter (ValidationRequest request): no Content-Type found

So, is there a way to force Spring to route such a request via the MappingJacksonHttpMessageConverter, either by somehow forcing Spring to use a custom handler chain or modifying the incoming request to explicitly set a content-type?

I've tried a few things:

Any ideas are appreciated.


To address the comments below, my @RequestMapping looks like:

@RequestMapping(value="/{service}" )
public @ResponseBody MyResponseObject( @PathVariable String service, @RequestBody MyRequestObject request) {

So there's nothing here that specifies JSON, but without a content type Spring doesn't appear to even take a stab at building my request object from the incoming request (which makes sense, as it does not have enough information to determine how to do so).

And as for @geoand's comment asking "why can you not add the content-type http header in a Servlet Filter or Spring Interceptor", the answer is "because I'm dumb and forgot how servlet filters work". That is the approach that I ultimately used to solve the problem, which I will be adding as an answer imminently.

Upvotes: 7

Views: 5742

Answers (1)

jpappe
jpappe

Reputation: 375

I was being a bit dumb when I asked this question because I was looking for a way in Spring to directly manipulate the incoming request or otherwise explicitly tell the handler chain that I wanted the request to always be treated as JSON. Once I thought about it for a bit, I realized that this is exactly what Servlet Filters are for.

First, I created a new HttpServletRequestWrapper that looks like this:

public class ForcedContentTypeHttpServletRequestWrapper extends HttpServletRequestWrapper {

    private static final Logger log = Logger.getLogger( ForcedContentTypeHttpServletRequestWrapper.class );

    // this is the header to watch out for and what we should make sure it always resolves to.
    private static final String CONTENT_TYPE_HEADER = "content-type";
    private static final String CONTENT_TYPE = "application/json";


    public ForcedContentTypeHttpServletRequestWrapper( HttpServletRequest request ) {
        super( request );
    }

    /**
     * If content type is explicitly queried, return our hardcoded value
     */
    @Override
    public String getContentType() {
        log.debug( "Overriding request's content type of " + super.getContentType() );
        return CONTENT_TYPE;
    }

    /**
     * If we are being asked for the content-type header, always return JSON
     */
    @Override
    public String getHeader( String name ) {
        if ( StringUtils.equalsIgnoreCase( name, CONTENT_TYPE_HEADER ) ) {
            if ( super.getHeader( name ) == null ) {
                log.debug( "Content type was not originally included in request" );
            }
            else {
                log.debug( "Overriding original content type from request: " + super.getHeader( name ) );
            }
            log.debug( "Returning hard-coded content type of " + CONTENT_TYPE );
            return CONTENT_TYPE;
        }

        return super.getHeader( name );
    }

    /**
     * When asked for the names of headers in the request, make sure "content-type" is always
     * supplied.
     */
    @SuppressWarnings( { "unchecked", "rawtypes" } )
    @Override
    public Enumeration getHeaderNames() {

        ArrayList headerNames = Collections.list( super.getHeaderNames() );
        if ( headerNames.contains( CONTENT_TYPE_HEADER ) ) {
            log.debug( "content type already specified in request. Returning original request headers" );
            return super.getHeaderNames();
        }

        log.debug( "Request did not specify content type. Adding it to the list of headers" );
        headerNames.add( CONTENT_TYPE_HEADER );
        return Collections.enumeration( headerNames );
    }

    /**
     * If we are being asked for the content-type header, always return JSON
     */
    @SuppressWarnings( { "rawtypes", "unchecked" } )
    @Override
    public Enumeration getHeaders( String name ) {
        if ( StringUtils.equalsIgnoreCase( CONTENT_TYPE_HEADER, name ) ) {
            if ( super.getHeaders( name ) == null ) {
                log.debug( "Content type was not originally included in request" );
            }
            else {
                log.debug( "Overriding original content type from request: " + Collections.list( super.getHeaders( name ) ) );
            }
            log.debug( "Returning hard-coded content type of " + CONTENT_TYPE );
            return Collections.enumeration( Arrays.asList( CONTENT_TYPE ) );
        }

        return super.getHeaders( name );
    }

}

I then put this wrapper to use in a Filter like so:

public class ContentTypeFilter implements Filter {

    /**
     * @see Filter#destroy()
     */
    @Override
    public void destroy() {
        // do nothing
    }

    /**
     * @see Filter#doFilter(ServletRequest, ServletResponse, FilterChain)
     */
    @Override
    public void doFilter( ServletRequest request, ServletResponse response, FilterChain chain ) throws IOException, ServletException {
        ForcedContentTypeHttpServletRequestWrapper requestWrapper = new ForcedContentTypeHttpServletRequestWrapper( (HttpServletRequest) request );
        chain.doFilter( requestWrapper, response );
    }

    /**
     * @see Filter#init(FilterConfig)
     */
    @Override
    public void init( FilterConfig fConfig ) throws ServletException {
        // do nothing
    }

}

It's not exactly bullet-proof, but it correctly handles request from the one source that this application actually cares about.

Upvotes: 4

Related Questions