Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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          |
+----+---------------------+-----------------------+------------------+-------------------------------------+
like image 362
dma_k Avatar asked Oct 19 '12 15:10

dma_k


People also ask

How do you handle exceptions in spring?

Spring MVC provides exception handling for your web application to make sure you are sending your own exception page instead of the server-generated exception to the user. The @ExceptionHandler annotation is used to detect certain runtime exceptions and send responses according to the exception.

How do you handle global exception?

In the Design tab part of the Ribbon, select New > Global Handler. The New Global Handler window opens. Type in a Name for the handler and save it in the project path. Click Create, a Global Exception Handler is added to the automation project.

How do you handle an exception in spring boot using annotations?

The @ExceptionHandler is an annotation used to handle the specific exceptions and sending the custom responses to the client. Define a class that extends the RuntimeException class. You can define the @ExceptionHandler method to handle the exceptions as shown.


2 Answers

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); } 
like image 89
oehmiche Avatar answered Sep 17 '22 17:09

oehmiche


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.

like image 34
dma_k Avatar answered Sep 16 '22 17:09

dma_k