JAX-RS Exception Handling

Exception handling

Exceptions are in general very useful, they help you to understand what could go wrong or what was wrong.

When you are developing web services, if you propagate exceptions properly, they could help you to reduce the number of request from developers using your API asking help or support, by implementing meaningful responses, i.e., the proper implementation of HTTP status codes and providing as much information as possible in the response.

Customer facing exceptions

You must split your errors in two categories:

  • Client errors (400-series HTTP response code) - Tell the client that a fault has taken place on their side. They should not re-transmit the same request again, but fix the error first.
  • Server errors (500-series HTTP response code) - Tell the client the server failed to fulfill an apparently valid request. The client can continue and try again with the request without modification.

Whenever if possible, you should use exceptions classes already defined in javax.ws.rs package that are related to HTTP response codes.

In order to provide a better experience to the final user/developer, I suggest to create an ExceptionMapper to intercept WebApplicationException based exceptions to include the exception message in the response to the user request:


package com.nafiux.ncp.base.exception;

import org.apache.cxf.jaxrs.utils.JAXRSUtils;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;

@Provider
public class WebApplicationExceptionMapperProvider implements ExceptionMapper<WebApplicationException> {

  class ErrorMessage {
    int status;
    String message;
    String developerMessage;

    public ErrorMessage(int status, String message) {
      this.status = status;
      String[] messages = message.split("\\|");
      this.message = messages[0];
      if(messages.length > 1) {
        this.developerMessage = messages[1];
      }
    }

    public int getStatus() { return this.status; }
    public String getMessage() { return this.message; }
    public String getDeveloperMessage() { return this.developerMessage; }
  }

  public Response toResponse(WebApplicationException ex) {
    Response exResponse = ex.getResponse();
    ErrorMessage errorMessage = new ErrorMessage(exResponse.getStatus(), ex.getMessage());
    return JAXRSUtils.fromResponse(ex.getResponse()).entity(errorMessage).build();
  }

}

In the previous example, WebApplicationExceptionMapperProvider expect a pipe (|) as a separator in the exception message to split it in two kind of messages, the first part is a high level error message final user oriented, the second part is a more detailed error message developer oriented (upon each specific exception raised, if no pipe is provided, developerMessage isn’t included in the response).

Example:



throw new ForbiddenException("Invalid username or password|Validate that you're sending the 'username' and 'password' in the payload");


If you need to define your custom exceptions that doesn’t extend from WebApplicationException, you should create also your own ExceptionMapper for that exception/s, so that you can provide the same level of detail in the response to the user.

Without WebApplicationExceptionMapperProvider (Apache CXF has an WebApplicationExceptionMapper already defined and used by default, but it doesn’t include the exception message in the response to the user/developer):


ignacio.ocampo@MXTI1-4WQG8WQ ~ $ curl -i -X POST -H "Content-Type: application/json" http://localhost:8080/api/uaa/svc/public/v1/auth/login -d '{"username": "invalid@user.com", "password": "changeme"}' && echo ""
HTTP/1.1 403 Forbidden
Date: Sun, 06 Aug 2017 21:03:11 GMT
Content-Length: 0
Server: Jetty(9.4.6.v20170531)


With WebApplicationExceptionMapperProvider (the example class described above):


ignacio.ocampo@MXTI1-4WQG8WQ ~ $ curl -i -X POST -H "Content-Type: application/json" http://localhost:8080/api/uaa/svc/public/v1/auth/login -d '{"username": "invalid@user.com", "password": "changeme"}' && echo ""
HTTP/1.1 403 Forbidden
Date: Sun, 06 Aug 2017 21:03:36 GMT
Content-Type: application/json
Transfer-Encoding: chunked
Server: Jetty(9.4.6.v20170531)

{"status":403,"message":"Invalid username or password","developerMessage":"Validate that you're sending the 'username' and 'password' in the payload"}


Internal exceptions

By internal exceptions I mean all exceptions (in the server side obviously).

We must ensure that all exceptions are being “converted” to a customer facing exceptions, there are two ways (see more above):

  • Catch those exceptions at some point in the api implementation layer and convert them in a WebApplicationException.
  • Register a ExceptionMapper for that exception, so that you can provide the same level of detail in the response to the user.

You should have in mind that there could be some hidden RuntimeExceptions (unchecked) that you won’t see until they appears, specially when you’re using third part libraries, e.g.:

@POST
@Path("/login")
@Produces(MediaType.APPLICATION_JSON)
public Response postLogin(LoginInput login) {
  throw new DynamoDBMappingException("This is an example");
}

DynamoDBMappingException extends from RuntimeException (unchecked), if you invoke the method, you will receive:

ignacio.ocampo@MXTI1-4WQG8WQ ~ $ curl -i -X POST -H "Content-Type: application/json" http://localhost:8080/api/uaa/svc/public/v1/auth/login -d '{"username": "invalid@user.com", "password": "changeme"}' && echo ""
HTTP/1.1 500 Server Error
Cache-Control: must-revalidate,no-cache,no-store
Content-Type: text/html;charset=iso-8859-1
Content-Length: 4418
Connection: close
Server: Jetty(9.4.6.v20170531)

<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<title>Error 500 Server Error</title>
</head>
<body><h2>HTTP ERROR 500</h2>
<p>Problem accessing /api/uaa/svc/public/v1/auth/login. Reason:
<pre>    Server Error</pre></p><h3>Caused by:</h3><pre>com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMappingException: This is an example
  at com.nafiux.ncp.uaa.application.UAAAplicationImpl.postLogin(UAAAplicationImpl.java:42)
  at com.nafiux.ncp.uaa.api.v1.auth.AuthenticationApiImpl.postLogin(AuthenticationApiImpl.java:15)
  at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
  at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
  at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
  at java.lang.reflect.Method.invoke(Method.java:497)
  at org.apache.cxf.service.invoker.AbstractInvoker.performInvocation(AbstractInvoker.java:180)
  at org.apache.cxf.service.invoker.AbstractInvoker.invoke(AbstractInvoker.java:96)
  at org.apache.cxf.jaxrs.JAXRSInvoker.invoke(JAXRSInvoker.java:189)
  at org.apache.cxf.jaxrs.JAXRSInvoker.invoke(JAXRSInvoker.java:99)
  at org.apache.cxf.interceptor.ServiceInvokerInterceptor$1.run(ServiceInvokerInterceptor.java:59)
  at org.apache.cxf.interceptor.ServiceInvokerInterceptor.handleMessage(ServiceInvokerInterceptor.java:96)
  at org.apache.cxf.phase.PhaseInterceptorChain.doIntercept(PhaseInterceptorChain.java:308)
  at org.apache.cxf.transport.ChainInitiationObserver.onMessage(ChainInitiationObserver.java:121)
  at org.apache.cxf.transport.http.AbstractHTTPDestination.invoke(AbstractHTTPDestination.java:262)
  at org.apache.cxf.transport.servlet.ServletController.invokeDestination(ServletController.java:234)
  at org.apache.cxf.transport.servlet.ServletController.invoke(ServletController.java:208)
  at org.apache.cxf.transport.servlet.ServletController.invoke(ServletController.java:160)
  at org.apache.cxf.transport.servlet.CXFNonSpringServlet.invoke(CXFNonSpringServlet.java:180)
  at org.apache.cxf.transport.servlet.AbstractHTTPServlet.handleRequest(AbstractHTTPServlet.java:299)
  at org.apache.cxf.transport.servlet.AbstractHTTPServlet.doPost(AbstractHTTPServlet.java:218)
  at javax.servlet.http.HttpServlet.service(HttpServlet.java:707)
  at org.apache.cxf.transport.servlet.AbstractHTTPServlet.service(AbstractHTTPServlet.java:274)
  at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:841)
  at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:535)
  at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:188)
  at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1253)
  at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:168)
  at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:473)
  at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:166)
  at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1155)
  at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:141)
  at org.eclipse.jetty.server.handler.HandlerCollection.handle(HandlerCollection.java:126)
  at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:132)
  at org.eclipse.jetty.server.Server.handle(Server.java:564)
  at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:317)
  at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:251)
  at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:279)
  at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:110)
  at org.eclipse.jetty.io.ChannelEndPoint$2.run(ChannelEndPoint.java:124)
  at org.eclipse.jetty.util.thread.Invocable.invokePreferred(Invocable.java:128)
  at org.eclipse.jetty.util.thread.Invocable$InvocableExecutor.invoke(Invocable.java:222)
  at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.doProduce(EatWhatYouKill.java:294)
  at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.produce(EatWhatYouKill.java:126)
  at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:673)
  at org.eclipse.jetty.util.thread.QueuedThreadPool$2.run(QueuedThreadPool.java:591)
  at java.lang.Thread.run(Thread.java:745)
</pre>
<hr><a href="http://eclipse.org/jetty">Powered by Jetty:// 9.4.6.v20170531</a><hr/>

</body>
</html>

You could define a GenericExceptionMapperProvider implements ExceptionMapper<Throwable> such as:


package com.nafiux.ncp.base.exception;

import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;

@Provider
public class GenericExceptionMapperProvider implements ExceptionMapper<Throwable> {

  class ErrorMessage {
    int status;
    String message;
    String developerMessage;

    public ErrorMessage(int status, String message) {
      this.status = status;
      String[] messages = message.split("\\|");
      this.message = messages[0];
      if(messages.length > 1) {
        this.developerMessage = messages[1];
      }
    }

    public int getStatus() { return this.status; }
    public String getMessage() { return this.message; }
    public String getDeveloperMessage() { return this.developerMessage; }
  }

  public Response toResponse(Throwable ex) {

    ErrorMessage errorMessage = new ErrorMessage(500, "An internal error has occurred|Perhaps here the REQUESTID or some reference that could help you to track the problem...");
    return Response.serverError().entity(errorMessage).build();

  }

}

As you can see now, the error is masqueraded:


ignacio.ocampo@MXTI1-4WQG8WQ ~ $ curl -i -X POST -H "Content-Type: application/json" http://localhost:8080/api/uaa/svc/public/v1/auth/login -d '{"username": "invalid@user.com", "password": "changeme"}' && echo ""
HTTP/1.1 500 Server Error
Date: Mon, 07 Aug 2017 17:00:33 GMT
Content-Type: application/json
Transfer-Encoding: chunked
Server: Jetty(9.4.6.v20170531)

{"status":500,"message":"An internal error has occurred","developerMessage":"Perhaps here the REQUESTID or some reference that could help you to track the problem..."}

JAX-RS supports exception inheritance as well. When an exception is thrown, JAX-RS will first try to find an ExceptionMapper for that exception’s type. If it cannot find one, it will look for a mapper that can handle the exception’s superclass. It will continue this process until there are no more superclasses to match against (read more).

Here some examples about errors formats from some well-known companies:

Reference:

comments powered by Disqus