OpenNTF / org.openntf.xsp.jakartaee

XPages Jakarta EE support libraries
Apache License 2.0
21 stars 7 forks source link

Observed NPE when POSTing JSON with a JAX-RS client #403

Closed jesse-gallagher closed 1 year ago

jesse-gallagher commented 1 year ago

This was observed when the request was made via a Concurrency task, but I'm not sure if that is related, but it is readily reproducible with code like:

@Path("roundTripEcho")
@GET
@Produces(MediaType.APPLICATION_JSON)
public RestClientBean.JsonExampleObject getRoundTripEcho() throws InterruptedException, ExecutionException {
    FacesContext facesContext = FacesContext.getCurrentInstance();
    HttpServletRequest request = (HttpServletRequest)facesContext.getExternalContext().getRequest();

    return exec.submit(() -> {
        RestClientBean.JsonExampleObject foo = new RestClientBean.JsonExampleObject();
        foo.setFoo("sending from async");

        URI uri = URI.create(request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + "/");
        uri = uri.resolve(facesContext.getExternalContext().getRequestContextPath() + "/");
        uri = uri.resolve("xsp/app/jaxrsClient/echoExampleObject");

        Client client = ClientBuilder.newBuilder().build();
        WebTarget target = client.target(uri);
        Response response = target.request().post(Entity.json(foo));

        return response.readEntity(RestClientBean.JsonExampleObject.class);
    }).get();
}

This results in a stack trace of:

java.lang.NullPointerException: null
  at org.openntf.xsp.jsonapi.jaxrs.JsonBindingProvider.writeTo(JsonBindingProvider.java:120)
  at org.jboss.resteasy.core.interception.jaxrs.AbstractWriterInterceptorContext.writeTo(AbstractWriterInterceptorContext.java:284)
  at org.jboss.resteasy.core.interception.jaxrs.AbstractWriterInterceptorContext.syncProceed(AbstractWriterInterceptorContext.java:245)
  at org.jboss.resteasy.core.interception.jaxrs.AbstractWriterInterceptorContext.proceed(AbstractWriterInterceptorContext.java:224)
  at org.jboss.resteasy.client.jaxrs.internal.ClientInvocation.writeRequestBody(ClientInvocation.java:446)
  at org.jboss.resteasy.client.jaxrs.engines.ManualClosingApacheHttpClient43Engine.writeRequestBodyToOutputStream(ManualClosingApacheHttpClient43Engine.java:625)
  at org.jboss.resteasy.client.jaxrs.engines.ManualClosingApacheHttpClient43Engine.buildEntity(ManualClosingApacheHttpClient43Engine.java:584)
  at org.jboss.resteasy.client.jaxrs.engines.ManualClosingApacheHttpClient43Engine.loadHttpMethod(ManualClosingApacheHttpClient43Engine.java:489)
  at org.jboss.resteasy.client.jaxrs.engines.ManualClosingApacheHttpClient43Engine.invoke(ManualClosingApacheHttpClient43Engine.java:299)
  at org.jboss.resteasy.client.jaxrs.internal.ClientInvocation.invoke(ClientInvocation.java:494)
  at org.jboss.resteasy.client.jaxrs.internal.ClientInvocation.invoke(ClientInvocation.java:69)
  at org.jboss.resteasy.client.jaxrs.internal.ClientInvocationBuilder.post(ClientInvocationBuilder.java:226)
  at rest.JaxRsClientExample.lambda$0(JaxRsClientExample.java:89)
  at java.util.concurrent.FutureTask.run(FutureTask.java:266)
  at org.glassfish.enterprise.concurrent.internal.ManagedFutureTask.run(ManagedFutureTask.java:117)
  at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
  at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
  at java.lang.Thread.run(Thread.java:827)
  at org.glassfish.enterprise.concurrent.ManagedThreadFactoryImpl$ManagedThread.run(ManagedThreadFactoryImpl.java:227)
  at org.openntf.xsp.jakarta.concurrency.NotesManagedThreadFactory$ManagedNotesThread.run(NotesManagedThreadFactory.java:76)
jesse-gallagher commented 1 year ago

The trouble looks to come from @Context private Application application being null when used to populate a client request. It looks like this is indeed specific to async use: the trouble doesn't arise when used in a single thread.

I put some null guards in there to avoid the immediate trouble, but the true fix would be to propagate the Application to the new thread.

jesse-gallagher commented 1 year ago

This is an odd one. There's a class responsible for ferrying context between threads in the core RESTeasy already - RestEasyContext - that works in a way similar to Concurrency API extensions.

Though it should already be present, I also added in a "true" Concurrency participant to wrap it. I can see it working - it captures and pushes a contextual Map that contains the Application class. However, by the time the JsonBindingProvider is called, that map has been replaced by another one (or trimmed) containing only a Providers implementation.

What makes this all the more odd is that the test passes when it's the only one executed and fails when run in the full suite, so something must be poisoning it. To my knowledge, nothing in this project intentionally called this before now, so it must be some odd interaction or an effect of, say, the MicroProfile additions.

Internally, the context map is stored in a stack structure, so presumably what's happening is that the right data is being pushed in, but then something else pushes a lesser map in for some reason. I'm not sure why or what that'd be, though.

jesse-gallagher commented 1 year ago

I decided to check the theory that perhaps something in there was pushing another map to the stack and the "good" one was below. Unfortunately, that doesn't seem to be the case: in the two instances where the application object is null, the stack size is 1. Conversely, there's a case where it is 2, but the application object isn't null.

jesse-gallagher commented 1 year ago

At this point, I suspect the problem may lie in the messiness of bootstrapping the JAX-RS Client. With incoming JAX-RS requests, things can be cleanly set up, but it's possible that running clients in threads don't find the context one way or another - if the thread spawner didn't see the active app and thus the default context propagator, maybe that's the trouble. The inconsistency of it makes it tough to be sure, though.