Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

java

json

spring

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:

  • Extending MappingJacksonHttpMessageConverter so that its canRead() and canWrite() methods always return true. Unfortunately, Spring doesn't even get to the point of looking at message converters before bailing out due to the lack of content type.
  • Using interceptors or Servlet filters to manually set the content type. Unfortunately, I can't see a way for either of these mechnisms to actually make a change to the incoming request aside from setting new attributes.

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.

like image 412
jpappe Avatar asked Apr 17 '14 12:04

jpappe


1 Answers

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.

like image 148
jpappe Avatar answered Oct 17 '22 21:10

jpappe