3

I have a Rest web service based on Spring MVC. I use a @RestControllerAdvice to handle exceptions thrown from my @Controllers.


Controller

An example of call is as below

@GetMapping(value = "/{id}/{name:.+}", produces = { MediaType.APPLICATION_OCTET_STREAM_VALUE,
        MediaType.APPLICATION_XML_VALUE, MediaType.APPLICATION_JSON_VALUE })
ResponseEntity<byte[]> getSomething(
        @PathVariable("id") String id, @PathVariable("name") String name) throws customException;

A basic method that can produce 3 media types : APPLICATION_OCTET_STREAM_VALUE, APPLICATION_XML_VALUE and APPLICATION_JSON_VALUE and throws a customException


Exception handler

The definition of my @RestControllerAdvice is as the following :

@ExceptionHandler({ CustomException.class })
public ResponseEntity<Object> handleException(CustomException e) {

    ErrorDto err = errorMapper.map(e);
    Enumeration<String> en = httpServletRequest.getHeaders(HttpHeaders.ACCEPT);

    while (en.hasMoreElements()) {
        String list = en.nextElement();
        StringTokenizer st = new StringTokenizer(list, ",");

        while (st.hasMoreTokens()) {

            String acc = st.nextToken();
            MediaType contentTypeHeader = MediaType.valueOf(acc);

            if (MediaType.APPLICATION_XML.includes(contentTypeHeader)) {

                JAXBElement<ErrorDto> ret = new ObjectFactory().createError(err);
                return ResponseEntity.status(HttpStatus.PRECONDITION_FAILED)
                        .contentType(MediaType.APPLICATION_XML).body(ret);

            } else if (MediaType.APPLICATION_JSON.includes(contentTypeHeader)) {

                return ResponseEntity.status(HttpStatus.PRECONDITION_FAILED)
                        .contentType(MediaType.APPLICATION_JSON).body(err);

            }
        }
    }

    return ResponseEntity.status(HttpStatus.NOT_ACCEPTABLE).body(null);
}

According to the request accept header, the @ExceptionHandler returns a null body and a NOT_ACCEPTABLE http response status if no accept is set, or a object of type ErrorDto if accept is of type APPLICATION_JSON or a JaxbElement of ErrorDto is accept is of type APPLICATION_XML

Please note that I specify the content type of the response when it contains a body.


Problem

My problem is when a client make a call with multiple accept headers, Spring tries to pick up an HttpMessageConverter according to the accept header and not to the response content type. Below an example :

When the client call a method that throws an exception (and then returns an errorDto) with multiple Accept headers as below :

HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.ACCEPT, MediaType.APPLICATION_OCTET_STREAM_VALUE);
headers.add(HttpHeaders.ACCEPT, MediaType.APPLICATION_XML_VALUE);
HttpEntity<?> entity = new HttpEntity<>(body, headers);

restTemplate.exchange(uriComponents.encode().toUri(), httpMethod, entity, Class);

Spring does not return a response with the error in the XML format as expected. It looks for a Octet-Stream <-> JaxbElement converter that does not exist and does not try to find the converter according to the response content type.

How can I force Spring to use a converter according to the response content type ?


I'm using :

  • Spring Boot 1.4.0.RELEASE
Radouane ROUFID
  • 10,595
  • 9
  • 42
  • 80
  • _"My problem is when a client make a call with multiple accept headers, Spring tries to pick up an HttpMessageConverter according to the accept header and not to the response content type"_ It's **not** a problem, that's how HTTP works. You cannot send something the client doesn't understand. – a better oliver Jan 24 '17 at 15:40
  • 1
    In fact in my example, the client understands what it sends in the accept. In the example above, the client understands octet-stream and xml. I want to send him an error in xml rather than octet-stream. but since the Octet-stream was put first in the header, Spring tries to pick up an octet stream httpMessageConverter. – Radouane ROUFID Jan 24 '17 at 15:52
  • I understand. Then there is probably an error in the `handleException` method. – a better oliver Jan 24 '17 at 16:14
  • I tried to debug spring source mvc code to understand how spring handle this, All the magic is done in the method `AbstractMessageConverterMethodProcessor#writeWithMessageConverters`. Unfortunately and as expected, Spring select HttpMessageConverter according to the requestedMediaTypes and not to the response MediaType. – Radouane ROUFID Jan 25 '17 at 07:23
  • If you have control over the client, then you can specify a lower quality value for `MediaType.APPLICATION_OCTET_STREAM_VALUE`. – a better oliver Jan 25 '17 at 08:08
  • I only expose the web service. I have no idea on how the client will call it. I try to cover all the scenarios but it seems complicated. – Radouane ROUFID Jan 25 '17 at 08:27

1 Answers1

0

Spring has this wrong. According to RFC2616, 406 Not Acceptable:

  Note: HTTP/1.1 servers are allowed to return responses which are
  not acceptable according to the accept headers sent in the
  request. In some cases, this may even be preferable to sending a
  406 response. User agents are encouraged to inspect the headers of
  an incoming response to determine if it is acceptable.

In my experience, error handling for REST services is one of the cases where it is preferable to force the response to something simple, like text/plain, to get an meaningful error message to the client.

Spring's over-opinionated behavior here has the effect of masking the primary error that occurs with a 406 response. So the RFC comments above apply equally to all 4xx and 5xx responses. If you can't match an Accept'ed content type, tho, you probably should not be sending any responses in the 1xx, 2xx or 3xx ranges.

Community
  • 1
  • 1
Charlie Reitzel
  • 809
  • 8
  • 13