Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring return JSON for HTTP 406 (NOT_ACCEPTABLE)

Spring allows the definition of @ExceptionHandlers inside of @RestControllerAdvice.

I already defined a lot of other ExceptionHandlers in there for HTTP 400, 404, 405,... However the ExceptionHandler for HTTP 406 (NOT_ACCEPTABLE) does not seem to work. The handler is triggered, I checked that in the logs, but the result is not used.

My goal is it to return a HTTP 406 with a JSON body.

Variant 1

@ResponseStatus(HttpStatus.NOT_ACCEPTABLE)
@ExceptionHandler(HttpMediaTypeNotAcceptableException.class)
public ErrorDTO requestMethodNotSupported(final HttpMediaTypeNotAcceptableException e) {
    final ErrorDTO dto = new ErrorDTO(HttpStatus.NOT_ACCEPTABLE, "http.media_not_acceptable");
    return dto;
}

Variant 2

@ExceptionHandler(HttpMediaTypeNotAcceptableException.class)
public ResponseEntity<ErrorDTO> requestMethodNotSupported2(final HttpMediaTypeNotAcceptableException e) {
    final ErrorDTO dto = new ErrorDTO(HttpStatus.NOT_ACCEPTABLE, "http.media_not_acceptable");
    return ResponseEntity.status(HttpStatus.NOT_ACCEPTABLE).contentType(MediaType.APPLICATION_JSON_UTF8).body(dto);
}

But I always get a HTML response similar to this from the Tomcat:

HTTP Status 406 -

type: Status report

message:

description: The resource identified by this request is only capable of generating responses with characteristics not acceptable according to the request "accept" headers.

instead of

{ "errorCode": 406, "errorMessage": "http.media_not_acceptable" }

Request-Headers:

  • Accept: application/stuff-that-cannot-be-present

Actual-Response-Headers:

  • Content-Type: text/html

Expected-Response-Headers:

  • Content-Type: application/json

I know that I could simply "fix" the Accept-Header that is send by the client, however the server should always respond in JSON, if it does not know how to respond.

I use Spring 4.3.3.RELEASE and Jackson 2.8.4.

like image 981
ST-DDT Avatar asked Feb 05 '23 19:02

ST-DDT


2 Answers

Finally I found a solution for this:

Instead of returning a serializable object just return the bytes directly.

private final ObjectMapper objectMapper = new ObjectMapper();

@ExceptionHandler(HttpMediaTypeNotAcceptableException.class)
public ResponseEntity<byte[]> mediaTypeNotAcceptable(HttpMediaTypeNotAcceptableException e) {
    Object response = ...;
    try {
        return ResponseEntity.status(HttpStatus.NOT_ACCEPTABLE)
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .body(objectMapper.writeValueAsBytes(response));
    } catch (Exception subException) {
        // Should never happen!!!
        subException.addSuppressed(e);
        throw subException;
    }
}

EDIT:

As an alternative you can create a custom HttpMessageConverter<ErrorResponse> for your ErrorResponse object.

  • Go to your or create a impl of WebMvcConfigurerAdapter#extendMessageConverters(converters)
  • Pick a HttpMessageConverter that is capable of creating your expected result/content type.
  • Wrap it in a way to fulfill the following conditions:
    • getSupportedMediaTypes() returns MediaType.ALL
    • canRead() returns false
    • canWrite()returns only true for your ErrorResponse
    • write() sets the forced CT and forward your expected content type to the wrapped converter.
  • Add your wrapper to the converters list.
    • If added as first element then it will always return your expected result (forced)
      • Requested: json , Returned: forced CT
      • Requested: xml , Returned: forced CT
      • Requested: image , Returned: forced CT
    • If added as last element then it will only return the result as your expected result, if there was no other matching converter (fallback)
      • Requested: json , Returned: json
      • Requested: xml , Returned: xml
      • Requested: image , Returned: forced CT
like image 87
ST-DDT Avatar answered Feb 15 '23 09:02

ST-DDT


Building on @ST-DDT findings. If you are also extending ResponseEntityExceptionHandler then you cannot just add another method to handle HttpMediaTypeNotAcceptableException. However, there is an even simpler solution to the whole problem then:

    @Override
    public ResponseEntity<Object> handleHttpMediaTypeNotAcceptable(HttpMediaTypeNotAcceptableException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
        ResponseEntity<Object> response = super.handleHttpMediaTypeNotAcceptable(ex, headers, status, request);

        // Workaround to return JSON response for 406
        return ResponseEntity.status(NOT_ACCEPTABLE)
                .contentType(APPLICATION_JSON)
                .body(response.getBody());
    }
like image 32
Aleksander Stelmaczonek Avatar answered Feb 15 '23 09:02

Aleksander Stelmaczonek