JAX-RS exception handling on Websphere Liberty

alukes :

I need some help with understanding how Websphere Liberty (18.0.0.1) handles exceptions thrown within a JAX-RS endpoint invocation. I'm using Liberty feature jaxrs-2.0, so the implementation should be provided by WLP.

Now, my application has a POST HTTP endpoint accepting JSON payload and I'd like to provide a custom error messages for all the possible wrong client inputs.

Here's one case that works in a way I expected it:

  1. Client sends application/xml instead of application/json
  2. There's a ClientErrorException thrown by the container
  3. I can use my own exception mapper (implementing ExceptionMapper<WebApplicationException> to handle this exception (actually to handle all the web application exception, which I'm fine with)
  4. This way I can format the error message, mark error with ID, whatever is needed. That's good

And here's the case not working for me:

  1. Client sends application/json, but with empty body
  2. The core exception in this case is java.io.EOFException: No content to map to Object due to end of input - yeah, that looks accurate
  3. Now what I can't figure out - instead of wrapping this EOFException into some kind of WebApplicationException (which I could handle easily), WLP is wrapping the exception issue into JaxRsRuntimeException

A couple of points here:

  • I don't want to create a mapper implementing ExceptionMapper<JaxRsRuntimeException> because that exception is not a part of JAX-RS 2.0 spec and I'd have to provide the import to JaxRsRuntimeException and wire the application with some Liberty-specific library.
  • A possible solution is to have my mapper implement a generic ExceptionMapper<RuntimeException> and string check if it finds exception of classname 'JaxRsRuntimeException' and then handle it. But that just doesn't seem right to me.

So, is that a WLP design not to give me a WebApplicationException in this case? What would be the elegant solution to handle this scenario?

Thanks


EDIT: Added some parts of source code.

REST endpoint and resource method:

@Path("/books")
public class BookEndpoint {

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    public Response createBook(Book book, @Context UriInfo uriInfo) {
        bookDao.create(book);
        UriBuilder builder = uriInfo.getAbsolutePathBuilder();
        builder.path(Integer.toString(book.getId()));
        return Response.created(builder.build()).entity(book).build();
    }
}

Entity with JAXB annotations:

@XmlRootElement
public class Book {

    private int id;
    private String title;

    // getters, setters
}

Exception stack trace:

com.ibm.ws.jaxrs20.JaxRsRuntimeException: java.io.EOFException: No content to map to Object duto end of input
    at org.apache.cxf.jaxrs.utils.JAXRSUtils.toJaxRsRuntimeException(JAXRSUtils.java:1928)
    at [internal classes]
    at org.apache.logging.log4j.web.Log4jServletFilter.doFilter(Log4jServletFilter.java:71)
    at com.ibm.ws.webcontainer.filter.FilterInstanceWrapper.doFilter(FilterInstanceWrapper.java:201)
    at [internal classes]
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at java.lang.Thread.run(Thread.java:745)
Caused by: java.io.EOFException: No content to map to Object duto end of input
    at org.codehaus.jackson.map.ObjectMapper._initForReading(ObjectMapper.java:2775)
    at [internal classes]
    at java.security.AccessController.doPrivileged(Native Method)
    at org.apache.cxf.jaxrs.utils.JAXRSUtils.readFromMessageBodyReader(JAXRSUtils.java:1413)
    at [internal classes]
    ... 48 more
Andy McCright :

This is the expected behavior based on Section 3.3.4 (and 4.5.1) of the JAX-RS 2.0 Spec. These sections describe how exceptions from JAX-RS resources and providers are handled - in short:

  1. If the exception is a WebApplicationException, then it will automatically mapped to a Response.
  2. If there is an ExceptionMapper registered that can handle the thrown exception, then that will be used to generate the response.
  3. Unchecked exceptions are propagated to the container (i.e. Liberty's JAX-RS implementation code).
  4. Unmapped exceptions must be handled via a container-specific exception and then appropriately propagated to the underlying container - in this case a ServletException must be passed to the web container.

The JaxRsRuntimeException is used to satisfy step 4.

In this scenario the built-in JSON provider (based on Jackson 1.X) is throwing the EOFException. Since there are no exception mappers for the EOFException (or any of it's superclasses), it is ultimately mapped to a ServletException by way of the JaxRsRuntimeException.

In order for an application to handle this scenario, there are a few different options:

  1. You can register an ExceptionMapper that is specific to this exception type (EOFException or any of it's superclasses - i.e. IOException). You should not need to register a mapper for JaxRsRuntimeException as that exception is only used internally in Liberty - and should not be mapped. If you are seeing the JaxRsRuntimeException passed to an ExceptionMapper, then you should open a support case with IBM, as this is likely a bug.

With an ExceptionMapper<EOFException> you can return a specific response whenever an EOFException is thrown from a provider or resource.

  1. You can register your own MessageBodyReader that will convert JSON to objects (using Jackson or any other JSON serialization code) but that will handle empty message bodies in the way you want - for example, converting it to null or using some kind of default object instance. Because user-registered providers take priority over built-in providers, this MBR would be used instead of Liberty's Jackson-based MBR.

This approach definitely gives you more control over how the data is deserialized as well as the exception handling.

  1. Register a ContainerRequestFilter provider that will abort when the message body is empty. Here is an example:

    @Provider
    public class EmptyBodyCheckFilter implements ContainerRequestFilter {
    
        @Override
        public void filter(ContainerRequestContext crc) throws IOException {
            if (crc.getEntityStream().available() < 1) {
                crc.abortWith(Response.status(400).entity("Invalid request - empty message body").build());
            }
        }
    }
    

I've successfully tested options 1 and 3 using the WebSphere Liberty May 2018 Beta. I haven't personally tested option 2 for this scenario, but based on using custom MBRs in the past, this should work.

One thing to keep in mind is that when Liberty GAs the jaxrs-2.1 feature, it will use JSONB as the built-in provider for serializing/deserializing JSON instead of Jackson. I tested your scenario using JAX-RS 2.1 (also in the May Beta) and instead of an EOFException, the JSONB code throws a NoSuchElementException. If you think you might move to JAX-RS 2.1, then I would suggest option 2 or 3. Option 1 would require that you create a new ExceptionMapper for JAX-RS 2.1.

Hope this helps,

Andy

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=462752&siteId=1