dma_k
dma_k

Reputation: 10639

How to change the content type in exception handler

Suppose I have a controller that serves GET request and returns bean to be serialized to JSON and also provides an exception handler for IllegalArgumentException that can be raised in service:

@RequestMapping(value = "/meta/{itemId}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public MetaInformation getMetaInformation(@PathVariable int itemId) {
    return myService.getMetaInformation(itemId);
}

@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
@ResponseBody
public String handleIllegalArgumentException(IllegalArgumentException ex) {
    return ExceptionUtils.getStackTrace(ex);
}

Message convertors are:

<mvc:annotation-driven>
    <mvc:message-converters>
        <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter" />
        <bean class="org.springframework.http.converter.StringHttpMessageConverter" />
    </mvc:message-converters>
</mvc:annotation-driven>

Now when I request the given URL in browser I see the correct JSON reply. However if exception is raised, the stringified exception is converted into JSON as well, but I would love it to be processed by StringHttpMessageConverter (resulting text/plain mime type). How can I go it?

To make the picture more complete (and complicated), suppose I also have the following handler:

@RequestMapping(value = "/version", method = RequestMethod.GET)
@ResponseBody
public String getApplicationVersion() {
    return "1.0.12";
}

This handler allows the return string to be serialized by both MappingJackson2HttpMessageConverter and StringHttpMessageConverter depending in passed Accept-type by the client. The return types and values should be as following:

+----+---------------------+-----------------------+------------------+-------------------------------------+
| NN | URL                 | Accept-type           | Content-type     | Message converter                   |
|    |                     | request header        | response header  |                                     |
+----+---------------------+-----------------------+------------------+-------------------------------------+
| 1. | /version            | text/html; */*        | text/plain       | StringHttpMessageConverter          |
| 2. | /version            | application/json; */* | application/json | MappingJackson2HttpMessageConverter |
| 3. | /meta/1             | text/html; */*        | application/json | MappingJackson2HttpMessageConverter |
| 4. | /meta/1             | application/json; */* | application/json | MappingJackson2HttpMessageConverter |
| 5. | /meta/0 (exception) | text/html; */*        | text/plain       | StringHttpMessageConverter          |
| 6. | /meta/0 (exception) | application/json; */* | text/plain       | StringHttpMessageConverter          |
+----+---------------------+-----------------------+------------------+-------------------------------------+

Upvotes: 37

Views: 47218

Answers (2)

oehmiche
oehmiche

Reputation: 1028

I think removing the produces = MediaType.APPLICATION_JSON_VALUE from @RequestMapping of the getMetaInformation will give you the desired result.

The response-type will be negotiated according to the content-type value in the Accept header.


edit

As this does not cover scenario 3,4 here is a solution working with ResponseEntity.class directly:

@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleIllegalArgumentException(Exception ex) {
    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.TEXT_PLAIN);
    return new ResponseEntity<String>(ex.getMessage(), headers, HttpStatus.BAD_REQUEST);
}

Upvotes: 33

dma_k
dma_k

Reputation: 10639

There are several aspects relating to the problem:

  • StringHttpMessageConverter adds catch-all mime type */* to the list of supported media types, while MappingJackson2HttpMessageConverter is bound to application/json only.
  • When @RequestMapping is providing produces = ..., this value is stored in HttpServletRequest (see RequestMappingInfoHandlerMapping.handleMatch()) and when the error handler is called, this mime type is automatically inherited and used.

The solution in simple case would be to put StringHttpMessageConverter first in the list:

<mvc:annotation-driven>
    <mvc:message-converters>
        <bean class="org.springframework.http.converter.StringHttpMessageConverter">
            <property name="supportedMediaTypes">
                <array>
                    <util:constant static-field="org.springframework.http.MediaType.TEXT_PLAIN" />
                </array>
            </property>
        </bean>
        <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter" />
    </mvc:message-converters>
</mvc:annotation-driven>

and also remove produces from @RequestMapping annotation:

@RequestMapping(value = "/meta/{itemId}", method = RequestMethod.GET)
@ResponseBody
public MetaInformation getMetaInformation(@PathVariable int itemId) {
    return myService.getMetaInformation(itemId);
}

Now:

  • StringHttpMessageConverter will discard all types, which only MappingJackson2HttpMessageConverter can handle (MetaInformation, java.util.Collection, etc) allowing them to be passed further.
  • In case of exception in scenario (5, 6) StringHttpMessageConverter will take the precedence.

So far so good, but unfortunately things get more complicated with ObjectToStringHttpMessageConverter. For handler return type java.util.Collection<MetaInformation> this message convertor will report that it can convert this type to java.lang.String. The limitation comes from the fact that collection element types are erased and AbstractHttpMessageConverter.canWrite(Class<?> clazz, MediaType mediaType) method gets java.util.Collection<?> class for check, however when it comes to conversion step ObjectToStringHttpMessageConverter fails. To solve the problem we keep produces for @RequestMapping annotation where JSON convertor should be used, but to force correct content type for exception handler, we will erase HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE attribute from HttpServletRequest:

@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
@ResponseBody
public String handleIllegalArgumentException(HttpServletRequest request, IllegalArgumentException ex) {
    request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
    return ExceptionUtils.getStackTrace(ex);
}

@RequestMapping(value = "/meta", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public Collection<MetaInformation> getMetaInformations() {
    return myService.getMetaInformations();
}

Context stays the same as it was originally:

<mvc:annotation-driven>
    <mvc:message-converters>
        <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter" />
        <bean class="org.springframework.http.converter.ObjectToStringHttpMessageConverter">
            <property name="conversionService">
                <bean class="org.springframework.context.support.ConversionServiceFactoryBean" />
            </property>
            <property name="supportedMediaTypes">
                <array>
                    <util:constant static-field="org.springframework.http.MediaType.TEXT_PLAIN" />
                </array>
            </property>
        </bean>
    </mvc:message-converters>
</mvc:annotation-driven>

Now scenarios (1,2,3,4) are handled correctly because of content-type negotiation, and scenarios (5,6) are processed in exception handler.

Alternatively one can replace collection return type with arrays, then solution #1 is applicable again:

@RequestMapping(value = "/meta", method = RequestMethod.GET)
@ResponseBody
public MetaInformation[] getMetaInformations() {
    return myService.getMetaInformations().toArray();
}

For discussion:

I think that AbstractMessageConverterMethodProcessor.writeWithMessageConverters() should not inherit class from value, but rather from method signature:

Type returnValueType = returnType.getGenericParameterType();

and HttpMessageConverter.canWrite(Class<?> clazz, MediaType mediaType) should be changed to:

canWrite(Type returnType, MediaType mediaType)

or (in case it is too limiting potential class-based convertors) to

canWrite(Class<?> valueClazz, Type returnType, MediaType mediaType)

Then parametrized types could be handled correctly and solution #1 would be applicable again.

Upvotes: 16

Related Questions