JAX-RS exception handling on Websphere Liberty
Asked Answered
A

2

7

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
Ambulacrum answered 21/5, 2018 at 10:27 Comment(2)
Hi @alukes, Can you provide the source code for you resource method and the message body reader you are using (assuming you are registering your own)? It would also help to see the stack trace of the JaxRsRuntimeException. The JaxRsRuntimeException is intended to be the container-specific exception referenced in section 3.3.4 of the JAX-RS spec ( download.oracle.com/otn-pub/jcp/jaxrs-2_0-fr-eval-spec/… ), but it is possible that it is getting used incorrectly here.Aphorism
Hi Andy, I edited the question with some source code. I'm not using any custom message body reader. I wanted to keep things simple and let the JAX-RS implementation do the JSON->entity conversion for me using JAXB annotations. I also posted the stack trace, let me know if I should provide any more details. ThanksAmbulacrum
A
3

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

Aphorism answered 23/5, 2018 at 19:16 Comment(1)
Thanks Andy, I'll mark this as accepted, because it gave me enough insight to decide how to solve the issue. I like both options 1 and 2, regarding the fact that option 2 will not suffer with additional code changes when updating to jaxrs-2.1 I guess I'll go for it. BTW as you mentioned: "If you are seeing the JaxRsRuntimeException passed to an ExceptionMapper... this is likely a bug" - thanks for confirming, because this caused my confusion at the very beginning. I had a feeling it's not a right thing to be getting an internal Liberty exception.Ambulacrum
B
2

Not a direct answert on "why WLP wrap the exception ..etc" but maybe add an exception interceptor as you did but on"ExceptionMapper<Exception>"and recusrsively iterate on the "causes" to check if java.io.EOFExceptionis one of those...

Bandicoot answered 22/5, 2018 at 15:53 Comment(1)
Hi @titou10, this is actually my current solution - to have a very general mapper and then manually handle every single exception type passed into this mapper. But there are many types of runtime exceptions that are likely to happen and the code is complex. My point was - if I can identify an exact exception type that I except to get in the application (EOFException), then it seems more clean to me to build a mapper for the concrete type. And the reason I wanted to use the WebAppException is that it nicely wraps all the client fault casesAmbulacrum

© 2022 - 2024 — McMap. All rights reserved.