A Better REST Exception Mapping Technique

The conventional REST Exception Mapper mechanism in JAX-RS means that exception mapping code is disconnected from the service code, so that developers have to hunt for some remote classes both to understand and to define exception mapping behaviour. It is unnecessarily awkward to trace backwards from a service method to discover what exceptions might emerge and how they might be mapped to HTTP responses.

A declarative annotation-driven approach provides simpler implementation, allows for automated documentation, and enables defining and expressing exception mapping behaviour directly at the point of use.

 


 

 

For REST services, exceptions emerging from lower tiers often need to be converted to an HTTP response

  • with an HTTP status code that matches the semantics of the exception in the context of that REST service
  • and commonly other details like error messages and codes

I once worked on a project where we had to expose a large legacy service tier as a REST service. The internal exception model was built for consumption by code in the same environment and to be understood by the same developers. The granularity and information content of the exceptions was not suitable for onward transmission to clients of the REST service, so there was a lot of exception mapping work to do.

The translations involved a lot of detail, but there were only a few simple patterns going on. Given an exception type, choose an HTTP status code. Given an internal error code, choose some external code. Given an exception type, select an error message. Given some combination of type and error code, map to some other status and message and other details.

The usual approach to exception mapping in REST is with javax.ws.rs.ext.ExceptionMapper, eg


public BookExceptionMapper implements ExceptionMapper<BookException> {
    public Response toResponse(BookException ex) {
        return Response.status(Response.Status.BAD_REQUEST).build();
    }
}

When an exception occurs, the runtime will look for an Exception Mapper that can handle that type, or the nearest superclass handler.

A big problem with this is that when looking at the REST service code there’s no obvious local expression of the error responses that may emerge. You have to drill down into the code to see what exceptions might be thrown, then you have to go and find the exception mapper for that type and see what happens to it, eventually uncovering the HTTP status code and other details that will form the end result.

As well as being manual and tedious for the developer to trace, it’s also manual and tedious to document the possible errors that may emerge from the REST service for clients to understand.

Wouldn’t it be nice if the developer could look at the REST service code and immediately see what exceptions may emerge and how they will be mapped to HTTP responses, define new mappings right there, and also automatically generate the client-facing documentation that explains the potential errors and their meanings?

We achieved that using a custom annotation to declaratively express the exception mappings, and an interceptor to process and perform the mapping and translation.

By modifying our REST documentation tool (we were using Enunciate) to pass through our custom annotation, we could also automate the client facing documentation.

We defined the ErrorResponse annotation with the details needed to map an internal exception class to a given HTTP response, the supplementary error code we needed to pass in that error response, and an explanatory description that could be used for documentation:


@ErrorResponse(
    cause = KaboomException.class,
    httpStatus = 400,
    errorCode = 639,
    description = "This might happen if you've been really bad")

 

Now, when you look at the REST service interface you can see (and define) the exception mapping right there:

 


@ErrorResponse(
    cause = MyException.class,
    httpStatus = 403,
    errorCode = 789,
    description = "Ooh you did it totally wrong")

@Path("/things/{thingId}")
Thing getThing(@PathParam("thingId") int thingId){
    ...
}

 

In some cases we needed to further sub-divide a given exception type into HTTP responses according to some internal details of the exception. We added an optional element to the annotation to create a more-specific mapping, and modified the interceptor to match according to “specificity”, eg:


@ErrorResponse(
    cause = KaboomException.class,
    whenKey = SOME_MATCHING_VALUE

    httpStatus = 400,
    errorCode = 639,
    description = "This might happen if you've been really bad")

Note how the first two elements of the ErrorResponse mapping are addressing the cause. When you see this exception containing this value…

The rest of it is specifying the result – use this status code, send an error body containing this error code, and here’s the documentation for what this error means to a client.

 

The interceptor that processes the exceptions by looking for these annotations can also simplify some other things. For exaample, the interceptor can see which method the exception came from and use a convention to lookup some predefined error message text to add the error response body before sending it to the client. For example, imagine a list of error message text linked to the method name and the status code such as

getThing.400=This is the meaningful description for this error from this method

The interceptor can automatically look up message text according to that convention and so we can separate it from the annotation, leaving the developer to just specify the bits that only change at development time such as the mapping from exception to status code. Message text is now separate and can change at its own pace.

 

It’s hard to lay it out well in a blog post, but being able to see and to define the exception mapping declarations at the point where those exceptions emerge from the service is so much nicer than having to root around for ExceptionMapper classes that have no direct connection:

 


@ErrorResponse(cause = MyException.class, httpStatus = 403, errorCode = 789,
               description = "Incompatible user status")
@ErrorResponse(cause = OtherException.class, httpStatus = 401, errorCode = 157,
               description = "Reading the manual failure")

@Path("/things/{thingId}")
Thing getThing(@PathParam("thingId") int thingId){
 ...
}

 

Being able to automatically process those annotations when generating API documentation is not just a timesaver, but also avoids the inconsistencies that arise from manual updates. Adopting simple conventions for mapping from exception details to things like textual clarification messages also leads to cleaner code and automaton both of execution and documentation.

 

 

Advertisements
Posted in Java

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: