Search code examples
javarestjakarta-eejerseyjax-rs

Jersey... how to log all exceptions, but still invoke ExceptionMappers


I'm in a little bit of bind... want my cake and to eat it too.

I want to log all exceptions my application throws. So if someone hits an incorrect URL, i want to log the stack trace to SLF4J.

So you're probably thinking, 'hey thats easy, just implement an exceptionmapper and log the exception." So I did:

public class RestExceptionMapper implements ExceptionMapper<java.lang.Exception> {
    private static final Logger log = LoggerFactory.getLogger(RestExceptionMapper.class);

    /**
     * {@inheritDoc}
     */
    @Override
    public Response toResponse(Exception exception) {
        log.error("toResponse() caught exception", exception);
        return null;
    }
}

If you do this, instead of 404 errors when someone types a wrong URL in, they get a 500 error. One would guess returning null would propagate the exception down the chain handlers, but Jersey doesn't do that. It actually provides very little info why it would choose one handler over another...

Has anyone ran into this problem and how did you solve it?


Solution

  • To return the correct http status code, your exception mapper could look something like this:

    @Provider
    public class RestExceptionMapper implements ExceptionMapper<Throwable>
    {
        private static final Logger log = LoggerFactory.getLogger(RestExceptionMapper.class);
    
        @Override
        public Response toResponse(Throwable exception)
        {
            log.error("toResponse() caught exception", exception);
    
            return Response.status(getStatusCode(exception))
                    .entity(getEntity(exception))
                    .build();
        }
    
        /*
         * Get appropriate HTTP status code for an exception.
         */
        private int getStatusCode(Throwable exception)
        {
            if (exception instanceof WebApplicationException)
            {
                return ((WebApplicationException)exception).getResponse().getStatus();
            }
    
            return Response.Status.INTERNAL_SERVER_ERROR.getStatusCode();
        }
    
        /*
         * Get response body for an exception.
         */
        private Object getEntity(Throwable exception)
        {
            // return stack trace for debugging (probably don't want this in prod...)
            StringWriter errorMsg = new StringWriter();
            exception.printStackTrace(new PrintWriter(errorMsg));
            return errorMsg.toString();            
        }
    }
    

    Also it sounds like you are interested in cascading exception mappers, but according to the spec this isn't possible:

    JAX-RS 2.0 Spec, Chapter 4.4

    "Exception mapping providers map a checked or runtime exception to an instance of Response. An exception mapping provider implements the ExceptionMapper interface and may be annotated with @Provider for automatic discovery. When choosing an exception mapping provider to map an exception, an implementation MUST use the provider whose generic type is the nearest superclass of the exception.

    When a resource class or provider method throws an exception for which there is an exception mapping provider, the matching provider is used to obtain a Response instance. The resulting Response is processed as if a web resource method had returned the Response, see Section 3.3.3. In particular, a mapped Response MUST be processed using the ContainerResponse filter chain defined in Chapter 6.

    To avoid a potentially infinite loop, a single exception mapper must be used during the processing of a request and its corresponding response. JAX-RS implementations MUST NOT attempt to map exceptions thrown while processing a response previously mapped from an exception. Instead, this exception MUST be processed as described in steps 3 and 4 in Section 3.3.4."