Daniel Wojciechowski
Daniel Wojciechowski

Reputation: 83

Spring @ExceptionHandler returns HTTP 406

I've got some strange Error.

What I want to do: The client asks GET: /invoices/invoiceNumber with header Accept: application/pdf and I want to return PDF file. If client forgot about header I return HTTP 406.

The method that returns PDF bytes throws DocumentNotFoundException that is handled by Spring ExceptionHandler and should return 404, but it didn't. Instead of that, I've got 406 and server log:

 2017-06-01 15:14:03.844  WARN 2272 --- [qtp245298614-13] o.e.jetty.server.handler.ErrorHandler    : Error page loop /error

The same magic happens when Spring Security returns HTTP 401.

So I think that problem is that Client Accept application/pdf but Spring ExceptionHandler returns application/json, so jetty dispatcher override 404 with 406 :(

My code:

@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "Invoice not found")
@ExceptionHandler(DocumentNotFoundException.class)
public void handleException() {
    //impl not needed
}

@GetMapping(value = "invoices/**", produces = MediaType.APPLICATION_PDF_VALUE)
public ResponseEntity<byte[]> getInvoicePdf(HttpServletRequest request) {
    String invoiceNumber = extractInvoiceNumber(request);
    final byte[] invoicePdf = invoiceService.getInvoicePdf(invoiceNumber);
    return new ResponseEntity<>(invoicePdf, buildPdfFileHeader(invoiceNumber), HttpStatus.OK);

}

@GetMapping(value = "invoices/**")
public ResponseEntity getInvoiceOther() {
    return new ResponseEntity<>(HttpStatus.NOT_ACCEPTABLE);
}

Can someone help me with understanding that?

Upvotes: 5

Views: 3537

Answers (2)

Drakes
Drakes

Reputation: 23660

To combat the infamous 406 issue, this is the solution I went with before the fix was released today (Jan 20, 2020) in Spring Boot 2.2.4.

I extended ResponseEntity<> and forced it to always use JSON content type. Then I return this new instantiation in the global exception handler under the purview of the @Rest/ControllerAdvice like so:

import org.jetbrains.annotations.Nullable;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.MultiValueMap;

import static org.springframework.http.HttpHeaders.CONTENT_TYPE;

public class JsonResponseEntity<T> extends ResponseEntity<T>
{
    // You need an inner class or else you will run into super() issues
    private static class Helper
    {
        private static MultiValueMap<String, String> headerHelper( @Nullable MultiValueMap<String, String> headers )
        {
            if ( headers == null )
            {
                headers = new HttpHeaders();
            }

            // The following is a generic version of: getHeaders().setContentType( MediaType.APPLICATION_JSON ); // NOSONAR
            headers.set( CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8.toString() );
            return headers;
        }
    }

    public JsonResponseEntity( HttpStatus status )
    {
        this(null, Helper.headerHelper( null ), status);
    }

    public JsonResponseEntity( T body, HttpStatus status )
    {
        this(body, Helper.headerHelper( null ), status);
    }

    public JsonResponseEntity( MultiValueMap<String, String> headers, HttpStatus status )
    {
        super( Helper.headerHelper( headers ), status );
    }

    public JsonResponseEntity( T body, MultiValueMap<String, String> headers, HttpStatus status )
    {
        super( body, Helper.headerHelper( headers ), status );
    }
}

Then in the expection handler you can return:

@ExceptionHandler(MyException.class)
public ResponseEntity<ErrorResponse> handleMyException(MyException e) {
    ErrorResponse errorResponse = new ErrorResponse(HttpStatus.NOT_FOUND, e.getLocalizedMessage()); // ErrorResponse is just a POJO

    // You can do this:
    // return ResponseEntity.status( 404 ).contentType( MediaType.APPLICATION_JSON ).body( errorResponse );

    // or this:
    return new JsonResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND);
}

By forcing the content type to JSON at the ResponseEntity level, the 406 issue should dissapear.

Upvotes: 0

Marvin Frommhold
Marvin Frommhold

Reputation: 1040

The problem is that Spring tries to convert the error response to application/pdf but fails to find a suitable HttpMessageConverter which supports conversion to PDF.

The easiest solution is to manually create the error response:

@ExceptionHandler(DocumentNotFoundException.class)
public ResponseEntity<?> handleException(DocumentNotFoundException e) {

    return ResponseEntity
        .status(HttpStatus.NOT_FOUND)
        .contentType(MediaType.APPLICATION_JSON_UTF8)
        .body("{\"error\": \"Invoice not found\"}");
}

This bypasses message conversion and results in a HTTP 404 response code.

Upvotes: 2

Related Questions