Spring @ExceptionHandler and HttpMediaTypeNotAcceptableException

14,044

Solution 1

The problem lies in the incompatibility of the requested content type and the object being returned. See my response on how to configure the ContentNegotiationConfigurer so that Spring determines the requested content type according to your needs (looking at the path extension, URL parameter or Accept header).

Depending on how the requested content type is determined, you have following options when an image is requested by the client:

  • if the requested content type is determined by the Accept header, and if the client can/wants to handle a JSON response instead of the image data, then the client should send the request with Accept: image/*, application/json. That way Spring knows that it can safely return either the image byte data or the error JSON message.
  • in any other case your best solution is to just return a HTTP error code, without any error message. You can do that in a couple of ways in your controller:

Set the error code on the response directly

public byte[] getImage(HttpServletResponse resp) {
    try {
        // return your image
    } catch (Exception e) {
        resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
    }
}

Use ResponseEntity

public ResponseEntity<?> getImage(HttpServletResponse resp) {
    try {
        byte[] img = // your image
        return ReponseEntity.ok(img);
    } catch (Exception e) {
        return new ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

Use a separate @ExceptionHandler method in that controller, which will override the default Spring exception handling. That assumes you have either a dedicated exception type for image requests or a separate controller just for serving the images. Otherwise, the exception handler will handle exceptions from other endpoints in that controller, too.

Solution 2

What does your ExceptionInfo class look like? I run into quite similar issue after defining a few exception handlers in @ControllerAdvice annotated class. When exception happened it was caught, although the response was not return and org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representation was thrown.

I figured out that the problem was caused by the fact, that I missed to add getter methods to my ErrorResponse class. After adding getter methods (this class was immutable, so there were no setter methods) everything worked like a charm.

Solution 3

If you are willing to ignore the explicit instruction of your client, as expressed in the Accept header, you can tinker with the content negotiation strategy like so:

/**
 * Content negotiation strategy that adds the {@code application/json} media type if not present in the "Accept"
 * header of the request.
 * <p>
 * Without this media type, Spring refuses to return errors as {@code application/json}, and thus not return them at
 * all, which leads to a HTTP status code 406, Not Acceptable
 */
class EnsureApplicationJsonNegotiationStrategy extends HeaderContentNegotiationStrategy {
    @Override
    public List<MediaType> resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException {
        List<MediaType> mediaTypes = new ArrayList<>(super.resolveMediaTypes(request));
        if (notIncludesApplicationJson(mediaTypes)) {
            mediaTypes.add(MediaType.APPLICATION_JSON);
        }
        return mediaTypes;
    }

    private boolean notIncludesApplicationJson(List<MediaType> mediaTypes) {
        return mediaTypes.stream()
                .noneMatch(mediaType -> mediaType.includes(MediaType.APPLICATION_JSON));
    }
}

Use this in a @Configuration class like so:

@Configuration
public class ContentNegotiationConfiguration implements WebMvcConfigurer {
    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer.strategies(List.of(
                new EnsureApplicationJsonNegotiationStrategy()
        ));
    }
}

Unit tests (using JUnit 5, Mockito):

@ExtendWith(MockitoExtension.class)
public class EnsureApplicationJsonNegotiationStrategyTest {
    @Mock
    private NativeWebRequest request;

    @InjectMocks
    private EnsureApplicationJsonNegotiationStrategy subject;

    @Test
    public void testAddsApplicationJsonToAll() throws HttpMediaTypeNotAcceptableException {
        when(request.getHeaderValues(HttpHeaders.ACCEPT)).thenReturn(new String[]{"*/*"});

        assertThat(subject.resolveMediaTypes(request), contains(
                MediaType.ALL // this includes application/json, so... fine
        ));
    }

    @Test
    public void testAddsApplicationJsonToEmpty() throws HttpMediaTypeNotAcceptableException {
        when(request.getHeaderValues(HttpHeaders.ACCEPT)).thenReturn(new String[0]);

        assertThat(subject.resolveMediaTypes(request), contains(
                MediaType.ALL // that's what the default does, which includes application/json, so... fine
        ));
    }

    @Test
    public void testAddsApplicationJsonToExisting() throws HttpMediaTypeNotAcceptableException {
        when(request.getHeaderValues(HttpHeaders.ACCEPT)).thenReturn(new String[]{"application/something"});

        assertThat(subject.resolveMediaTypes(request), containsInAnyOrder(
                MediaType.valueOf("application/something"),
                MediaType.APPLICATION_JSON
        ));
    }

    @Test
    public void testAddsApplicationJsonToNull() throws HttpMediaTypeNotAcceptableException {
        when(request.getHeaderValues(HttpHeaders.ACCEPT)).thenReturn(null);

        assertThat(subject.resolveMediaTypes(request), contains(
                MediaType.ALL // that's what the default does, which includes application/json, so... fine
        ));
    }

    @Test
    public void testRetainsApplicationJson() throws HttpMediaTypeNotAcceptableException {
        when(request.getHeaderValues(HttpHeaders.ACCEPT)).thenReturn(new String[]{"application/json"});

        assertThat(subject.resolveMediaTypes(request), contains(MediaType.APPLICATION_JSON));
    }
}

Solution 4

(Based on previous answers from Sander Verhagen and Adam Michalik)

I finally wrote a way to avoid twice exceptions result: a content negociation failure ('Accept' not valid) while already executing an exception handler method.

I tell to spring to resolve as APPLICATION_JSON in the case the request media type is not well formatted. So this way there is no more a "on-fly" content negotiation error when producing exception handler response.

@Configuration
public class MyWebMvcConfigurer implements WebMvcConfigurer {
    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer.strategies(singletonList(
                new EnsureValidMediaTypesNegotiationStrategy()
        ));
    }
}

@Slf4j
class EnsureValidMediaTypesNegotiationStrategy extends HeaderContentNegotiationStrategy {
    @Override
    public List<MediaType> resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException {
        try {
            return super.resolveMediaTypes(request);
        } catch (Exception negotiationException) {
            // this fix is used to avoid twice exceptions due to media negotiation
            log.info("Client Accept header is not recognized (json response enforced):{}", negotiationException.getMessage());
            return singletonList(MediaType.APPLICATION_JSON);
        }

    }
}

Note you could add some test to your controller using this value as "Accept" header :

../../../../../../../../../../../../../e*c/h*s*s{{
Share:
14,044

Related videos on Youtube

Lia
Author by

Lia

Java programmer

Updated on September 14, 2022

Comments

  • Lia
    Lia over 1 year

    I have a class annotated with @ControllerAdvice and this method in it:

    @ExceptionHandler(ResourceNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    @ResponseBody
    public ExceptionInfo resourceNotFoundHandler(ResourceNotFoundException ex) {
        List<ErrorContent> errors = new ArrayList<>();
        errors.add(new ErrorContent(ExceptionsCodes.NOT_FOUND_CODE, null,
                "test"));
        return fillExceptionInfo(HttpStatus.NOT_FOUND, errors, ex);
    }
    

    Here is fillExceptionInfo:

    public ExceptionInfo fillExceptionInfo(HttpStatus status, List<ErrorContent> errors, 
            Exception ex) {
        String msg = ex.getMessage();
    
        return new ExceptionInfo(status.toString(), errors, (msg != null && !msg.equals(""))
                ? ex.getMessage()
                : ExceptionUtils.getFullStackTrace(ex));
    }
    

    When a web-client send a request for some json data, which cannot be found, this method works ok. But when server receives a request to image, instead of my exception a HttpMediaTypeNotAcceptableException is thrown. I understand that it happens because of wrong content type, but how can I fix this problem?

    Update

    My goal is to throw ResourceNotFoundException in both cases for json data and for file.

    Exception that I get (so it is thrown from AbstractMessageConverterMethodProcessor):

    ERROR o.s.w.s.m.m.a.ExceptionHandlerExceptionResolver - doResolveHandlerMethodException - Failed to invoke @ExceptionHandler method: public com.lia.utils.GlobalExceptionHandler$ExceptionInfo com.lia.utils.GlobalExceptionHandler.resourceNotFoundHandler(com.lia.app.controllers.exceptions.ResourceNotFoundException) 
        org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representation
            at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:168) ~[spring-webmvc-4.1.1.RELEASE.jar:4.1.1.RELEASE]
            at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:101) ~[spring-webmvc-4.1.1.RELEASE.jar:4.1.1.RELEASE]
            at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.handleReturnValue(RequestResponseBodyMethodProcessor.java:198) ~[spring-webmvc-4.1.1.RELEASE.jar:4.1.1.RELEASE]
            at org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite.handleReturnValue(HandlerMethodReturnValueHandlerComposite.java:71) ~[spring-web-4.1.1.RELEASE.jar:4.1.1.RELEASE]
            at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:122) ~[spring-webmvc-4.1.1.RELEASE.jar:4.1.1.RELEASE]
            at org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver.doResolveHandlerMethodException(ExceptionHandlerExceptionResolver.java:362) ~[spring-webmvc-4.1.1.RELEASE.jar:4.1.1.RELEASE]
            at org.springframework.web.servlet.handler.AbstractHandlerMethodExceptionResolver.doResolveException(AbstractHandlerMethodExceptionResolver.java:60) [spring-webmvc-4.1.1.RELEASE.jar:4.1.1.RELEASE]
            at org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver.resolveException(AbstractHandlerExceptionResolver.java:138) [spring-webmvc-4.1.1.RELEASE.jar:4.1.1.RELEASE]
            at org.springframework.web.servlet.DispatcherServlet.processHandlerException(DispatcherServlet.java:1167) [spring-webmvc-4.1.1.RELEASE.jar:4.1.1.RELEASE]
            at org.springframework.web.servlet.DispatcherServlet.processDispatchResult(DispatcherServlet.java:1004) [spring-webmvc-4.1.1.RELEASE.jar:4.1.1.RELEASE]
            at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:955) [spring-webmvc-4.1.1.RELEASE.jar:4.1.1.RELEASE]
            at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:877) [spring-webmvc-4.1.1.RELEASE.jar:4.1.1.RELEASE]
            at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:966) [spring-webmvc-4.1.1.RELEASE.jar:4.1.1.RELEASE]
            at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:857) [spring-webmvc-4.1.1.RELEASE.jar:4.1.1.RELEASE]
            at javax.servlet.http.HttpServlet.service(HttpServlet.java:687) [javax.servlet-api-3.1.0.jar:3.1.0]
            at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:842) [spring-webmvc-4.1.1.RELEASE.jar:4.1.1.RELEASE]
            at javax.servlet.http.HttpServlet.service(HttpServlet.java:790) [javax.servlet-api-3.1.0.jar:3.1.0]
            at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:717) [jetty-servlet-9.1.1.v20140108.jar:9.1.1.v20140108]
            at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1644) [jetty-servlet-9.1.1.v20140108.jar:9.1.1.v20140108]
    ....
    
  • Alberto Alegria
    Alberto Alegria over 2 years
    The error is a little bit tricky, but this solved it, thanks