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?
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."
You can use a RequestEventListener to listen for an exception event and log the throwable, without interfering with any existing processing. Note that this means first registering an ApplicationEventListener
which then returns an instance of RequestEventListener
.
public class ExceptionLogger implements ApplicationEventListener, RequestEventListener {
private static final Logger log = LoggerFactory.getLogger(RequestExceptionLogger.class);
@Override
public void onEvent(final ApplicationEvent applicationEvent) {
}
@Override
public RequestEventListener onRequest(final RequestEvent requestEvent) {
return this;
}
@Override
public void onEvent(RequestEvent paramRequestEvent) {
if(paramRequestEvent.getType() == Type.ON_EXCEPTION) {
log.error("", paramRequestEvent.getException());
}
}
}