Search code examples
jakarta-eeresteasyundertowwildfly-10

How do I make Wildfly 10 / Resteasy indicate a serialization exception to the client


When an exception occurs during json serialization of a jax-rs response in Wildfly 10 the response is committed and the HTTP return code cannot be changed anymore. An exception of this is logged on the server side:

10:19:56,148 ERROR [io.undertow.request] (default task-21) UT005023: Exception handling request to /ca00cefe-efd8-496c-a874-de0af20cad42/rest/: org.jboss.resteasy.spi.UnhandledException: RESTEASY003770: Response is committed, can't handle exception
    at org.jboss.resteasy.core.SynchronousDispatcher.writeException(SynchronousDispatcher.java:167)
    at org.jboss.resteasy.core.SynchronousDispatcher.writeResponse(SynchronousDispatcher.java:471)
    at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:415)
    at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:202)
    at org.jboss.resteasy.plugins.server.servlet.ServletContainerDispatcher.service(ServletContainerDispatcher.java:221)
    at org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher.service(HttpServletDispatcher.java:56)
    at org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher.service(HttpServletDispatcher.java:51)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:790)
    at io.undertow.servlet.handlers.ServletHandler.handleRequest(ServletHandler.java:85)
    at io.undertow.servlet.handlers.security.ServletSecurityRoleHandler.handleRequest(ServletSecurityRoleHandler.java:62)
    at io.undertow.servlet.handlers.ServletDispatchingHandler.handleRequest(ServletDispatchingHandler.java:36)
    at org.wildfly.extension.undertow.security.SecurityContextAssociationHandler.handleRequest(SecurityContextAssociationHandler.java:78)
    at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
    at io.undertow.servlet.handlers.security.SSLInformationAssociationHandler.handleRequest(SSLInformationAssociationHandler.java:131)
    at io.undertow.servlet.handlers.security.ServletAuthenticationCallHandler.handleRequest(ServletAuthenticationCallHandler.java:57)
    at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
    at io.undertow.security.handlers.AbstractConfidentialityHandler.handleRequest(AbstractConfidentialityHandler.java:46)
    at io.undertow.servlet.handlers.security.ServletConfidentialityConstraintHandler.handleRequest(ServletConfidentialityConstraintHandler.java:64)
    at io.undertow.security.handlers.AuthenticationMechanismsHandler.handleRequest(AuthenticationMechanismsHandler.java:60)
    at io.undertow.servlet.handlers.security.CachedAuthenticatedSessionHandler.handleRequest(CachedAuthenticatedSessionHandler.java:77)
    at io.undertow.security.handlers.NotificationReceiverHandler.handleRequest(NotificationReceiverHandler.java:50)
    at io.undertow.security.handlers.AbstractSecurityContextAssociationHandler.handleRequest(AbstractSecurityContextAssociationHandler.java:43)
    at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
    at org.wildfly.extension.undertow.security.jacc.JACCContextIdHandler.handleRequest(JACCContextIdHandler.java:61)
    at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
    at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
    at io.undertow.servlet.handlers.ServletInitialHandler.handleFirstRequest(ServletInitialHandler.java:284)
    at io.undertow.servlet.handlers.ServletInitialHandler.dispatchRequest(ServletInitialHandler.java:263)
    at io.undertow.servlet.handlers.ServletInitialHandler.access$000(ServletInitialHandler.java:81)
    at io.undertow.servlet.handlers.ServletInitialHandler$1.handleRequest(ServletInitialHandler.java:174)
    at io.undertow.server.Connectors.executeRootHandler(Connectors.java:202)
    at io.undertow.server.HttpServerExchange$1.run(HttpServerExchange.java:793)
    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)

Also the json serializer nicely closes all open tags, so for the client the response is http code 200 with valid json. No indication of a failure whatsoever, only missing data.

How can I convince Wildfly / Resteasy / Undertow stop giving completely valid responses to the client when an exception occurs. Either a 500 response or not closing the json would be fine.

During my testing I found that when the exception is thrown no data has actually been send to the client, a Content-Length header is still added to the response. This might leave an opportunity to change the response code, however I was unable to find where this happens or why the reponse is marked as committed when nothing has been send yet.

HTTP/1.1 200 OK
Connection: keep-alive
X-Powered-By: Undertow/1
Server: WildFly/10
Content-Type: application/json
Content-Length: 10
Date: Wed, 23 Mar 2016 09:37:19 GMT

{"a":"ok"}

Test case

TestActivator.java

package test;

import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

@ApplicationPath("/rest")
public class TestActivator extends Application {
}

TestResource.java

package test;

import java.io.Serializable;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.xml.bind.annotation.XmlRootElement;

@Path("/")
public class TestResource {
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public TestObject test() {
        return new TestObject();
    }

    @XmlRootElement
    public class TestObject implements Serializable {

        private static final long serialVersionUID = 1L;

        public TestObject() {
        }

        public String getA() {
            return "ok";
        }

        public String getB() {
            throw new RuntimeException("error");
        }
    }
}

TestCase.java

package test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Response;

import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.extension.rest.client.ArquillianResteasyResource;
import org.jboss.arquillian.junit.Arquillian;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.json.JSONObject;
import org.json.JSONTokener;
import org.junit.Test;
import org.junit.runner.RunWith;

@RunWith(Arquillian.class)
public class TestCase {

    @Deployment(testable = false)
    public static WebArchive createTestArchive() {
        return ShrinkWrap.create(WebArchive.class)
                .addPackage(TestResource.class.getPackage());
    }

    /**
     * This test should fail, either the response should not be 200 or a json exception should occur
     */
    @Test
    public void test(@ArquillianResteasyResource WebTarget webTarget) {
        Response response = webTarget.path("/").request().get();
        assertEquals(200, response.getStatus());
        JSONObject object = new JSONObject(new JSONTokener(response.readEntity(String.class)));
        assertEquals("ok", object.get("a"));
        assertTrue(object.isNull("b"));
    }
}

Solution

  • We figured out a way to achieve this.

    Disabling AUTO_CLOSE_TARGET will result in a http 500 error on failure in most cases. Only exception I found is when a response exceeds a certain size, in that case something will start flushing the output. For that situation disabling AUTO_CLOSE_JSON_CONTENT will make sure the json document is not correctly closed on error.

    package test;
    
    import javax.ws.rs.ext.ContextResolver;
    import javax.ws.rs.ext.Provider;
    
    import com.fasterxml.jackson.core.JsonGenerator;
    import com.fasterxml.jackson.databind.ObjectMapper;
    
    @Provider
    public class MyObjectContextResolver implements ContextResolver<ObjectMapper> {
        private ObjectMapper objectMapper;
    
        public MyObjectContextResolver() {
            objectMapper = new ObjectMapper();
    
            objectMapper.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false);
            objectMapper.configure(JsonGenerator.Feature.AUTO_CLOSE_JSON_CONTENT, false);
        }
    
        @Override
        public ObjectMapper getContext(final Class<?> arg0) {
            return objectMapper;
        }
    }