I have a RESTful web service running under Glassfish 3.1.2 using Jersey and Jackson:
@Stateless @LocalBean @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @Path("users") public class UserRestService { private static final Logger log = ...; @GET @Path("{userId:[0-9]+}") public User getUser(@PathParam("userId") Long userId) { User user; user = loadUserByIdAndThrowApplicableWebApplicationExceptionIfNotFound(userId); return user; } }
For expected exceptions, I throw the appropriate WebApplicationException
, and I'm happy with the HTTP 500 status that is returned if an unexpected exception occurs.
I would now like to add logging for these unexpected exceptions, but despite searching, cannot find out how I should be going about this.
I have tried using a Thread.UncaughtExceptionHandler
and can confirm that it is applied inside the method body, but its uncaughtException
method is never called, as something else is handling the uncaught exceptions before they reach my handler.
Another option I've seen some people use is an ExceptionMapper
, which catches all exceptions and then filters out WebApplicationExceptions:
@Provider public class ExampleExceptionMapper implements ExceptionMapper<Throwable> { private static final Logger log = ...; public Response toResponse(Throwable t) { if (t instanceof WebApplicationException) { return ((WebApplicationException)t).getResponse(); } else { log.error("Uncaught exception thrown by REST service", t); return Response.status(Response.Status.INTERNAL_SERVER_ERROR) // Add an entity, etc. .build(); } } }
While this approach may work, it feels to me like misuse of what ExceptionMappers are supposed to be used for, that is, mapping certain exceptions to certain responses.
Most sample JAX-RS code returns the Response
object directly. Following this approach, I could change my code to something like:
public Response getUser(@PathParam("userId") Long userId) { try { User user; user = loadUserByIdAndThrowApplicableWebApplicationExceptionIfNotFound(userId); return Response.ok().entity(user).build(); } catch (Throwable t) { return processException(t); } } private Response processException(Throwable t) { if (t instanceof WebApplicationException) { return ((WebApplicationException)t).getResponse(); } else { log.error("Uncaught exception thrown by REST service", t); return Response.status(Response.Status.INTERNAL_SERVER_ERROR) // Add an entity, etc. .build(); } }
However, I'm leery of going this route, as my actual project is not as simple as this example, and I would have to implement this same pattern over and over again, not to mention having to manually build up the Responses.
Are there better methods for adding logging for uncaught exceptions? Is there a "right" way of implementing this?
The specific procedure is as follows. When an uncaught exception occurs, the JVM does the following: it calls a special private method, dispatchUncaughtException(), on the Thread class in which the exception occurs; it then terminates the thread in which the exception occurred1.
If an exception is not caught, it is intercepted by a function called the uncaught exception handler. The uncaught exception handler always causes the program to exit but may perform some task before this happens. The default uncaught exception handler logs a message to the console before it exits the program.
When an uncaught exception occurs, the JVM calls a special private method known dispatchUncaughtException( ), on the Thread class in which the exception occurs and terminates the thread. The Division by zero exception is one of the example for uncaught exceptions.
For lack of a better way to implement logging for uncaught JAX-RS exceptions, using a catch-all ExceptionMapper
as in Other Ideas: #1 seems to be the cleanest, simplest way to add this functionality.
Here's my implementation:
@Provider public class ThrowableExceptionMapper implements ExceptionMapper<Throwable> { private static final Logger log = Logger.getLogger(ThrowableExceptionMapper.class); @Context HttpServletRequest request; @Override public Response toResponse(Throwable t) { if (t instanceof WebApplicationException) { return ((WebApplicationException) t).getResponse(); } else { String errorMessage = buildErrorMessage(request); log.error(errorMessage, t); return Response.serverError().entity("").build(); } } private String buildErrorMessage(HttpServletRequest req) { StringBuilder message = new StringBuilder(); String entity = "(empty)"; try { // How to cache getInputStream: http://stackoverflow.com/a/17129256/356408 InputStream is = req.getInputStream(); // Read an InputStream elegantly: http://stackoverflow.com/a/5445161/356408 Scanner s = new Scanner(is, "UTF-8").useDelimiter("\\A"); entity = s.hasNext() ? s.next() : entity; } catch (Exception ex) { // Ignore exceptions around getting the entity } message.append("Uncaught REST API exception:\n"); message.append("URL: ").append(getOriginalURL(req)).append("\n"); message.append("Method: ").append(req.getMethod()).append("\n"); message.append("Entity: ").append(entity).append("\n"); return message.toString(); } private String getOriginalURL(HttpServletRequest req) { // Rebuild the original request URL: http://stackoverflow.com/a/5212336/356408 String scheme = req.getScheme(); // http String serverName = req.getServerName(); // hostname.com int serverPort = req.getServerPort(); // 80 String contextPath = req.getContextPath(); // /mywebapp String servletPath = req.getServletPath(); // /servlet/MyServlet String pathInfo = req.getPathInfo(); // /a/b;c=123 String queryString = req.getQueryString(); // d=789 // Reconstruct original requesting URL StringBuilder url = new StringBuilder(); url.append(scheme).append("://").append(serverName); if (serverPort != 80 && serverPort != 443) { url.append(":").append(serverPort); } url.append(contextPath).append(servletPath); if (pathInfo != null) { url.append(pathInfo); } if (queryString != null) { url.append("?").append(queryString); } return url.toString(); } }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With