samanime
samanime

Reputation: 26567

Determine acceptable content types in ExceptionHandler

In a Spring application, I have an endpoint which normally returns an image (produces = MediaType.IMAGE_PNG_VALUE).

I also have @ExceptionHandler functions to handle various functions.

I'm trying to find a way to determine, from within the @ExceptionHandler, if the client will accept text/plain or text/json so in the event of an error I can return back one of those, or omit it if they are only expecting image/png.

How can I determine what acceptable content types I can return for a given request?

Upvotes: 4

Views: 1566

Answers (2)

samanime
samanime

Reputation: 26567

This is the answer I came up with. It's similar to YoungSpice's, but it is a little more flexible and uses MediaType directly (which means it'll handle wildcard types like text/* and the like):

private ResponseEntity<String> buildResponse(WebRequest request, HttpStatus status, String message) {
        HttpHeaders httpHeader = new HttpHeaders();
        List<MediaType> acceptHeader =
                MediaType.parseMediaTypes(Arrays.asList(request.getHeaderValues(HttpHeaders.ACCEPT)));

        if (acceptHeader.stream().anyMatch(mediaType -> mediaType.isCompatibleWith(MediaType.APPLICATION_JSON))) {
            httpHeader.setContentType(MediaType.APPLICATION_JSON);
            return new ResponseEntity<>("{ \"error\": \"" + message + "\" }", httpHeader, status);
        } else if (acceptHeader.stream().anyMatch(mediaType -> mediaType.isCompatibleWith(MediaType.TEXT_PLAIN))) {
            httpHeader.setContentType(MediaType.TEXT_PLAIN);
            return new ResponseEntity<>(message, httpHeader, status);
        } else {
            return ResponseEntity.status(status).body(null);
        }
    }

Basically, it uses MediaType.parseMediaTypes() to parse the Accept header, then I stream through them and use the mediaType.isCompatibleWith() function to check if my target is acceptable. This will let it handle if the header has something like application/* instead of application/json directly.

It also seems like if Accept isn't explicitly provided in the request, there is an implied */*, which seems to work as intended.

Upvotes: 3

The_Tourist
The_Tourist

Reputation: 2128

You can access the request to inspect headers and return an appropriate response. It is standard Content Negotiation.

Here's an example:

@ControllerAdvice
public class MyExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler(value = {RuntimeException.class})
    protected ResponseEntity<Object> handleMyException(RuntimeException ex, WebRequest request) {
        List<String> acceptableMimeTypes = Arrays.asList(request.getHeaderValues(HttpHeaders.ACCEPT));
        if (acceptableMimeTypes.contains(MediaType.TEXT_PLAIN_VALUE)) {
            return ResponseEntity.ok()
                    .header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE)
                    .body("hello");
        }
        throw ex;
    }
}

There are some arguments that spring-mvc can automagically inject into controller methods, and WebRequest (which is spring's representation of an http request) is one of those. If the client has sent an Accept : text/plain header with the request, the above example returns the string hello if there's a RuntimeException. If there's no exception, this logic won't get triggered at all, so the endpoint will just return whatever it normally returns. You can read more about @ControllerAdvice and @ExceptionHandler here.

Of course, be sure to think about the exact exception types you want to handle, and semantically appropriate status codes to return so that the clients know how to correctly interpret the response.

Upvotes: 5

Related Questions