Uploaded image for project: 'Undertow'
  1. Undertow
  2. UNDERTOW-2338

NullPointerException in io.undertow.servlet.spec.ServletOutputStreamImpl.setWriteListener

XMLWordPrintable

      It looks like there is a potential race in the io.undertow.servlet.spec.HttpServletRequestImpl. The asyncStarted is set to true, then the AsyncContextImpl is created. I'm seeing this failure occasionally with a RESTEasy test. I'm not able to duplicate it locally, but it happens periodically on CI. Below is an example stack trace:

      07:49:50,752 ERROR [org.jboss.resteasy.core.providerfactory.DefaultExceptionMapper] (XNIO-1 task-3) RESTEASY002375: Error processing request POST /test/multi-injected - org.jboss.resteasy.plugins.providers.multipart.MultipartEntityPartProviderTest$TestResource.multipleInjectable: java.lang.NullPointerException: Cannot invoke "io.undertow.servlet.spec.AsyncContextImpl.addAsyncTask(java.lang.Runnable)" because "this.asyncContext" is null
          at io.undertow.servlet.spec.ServletOutputStreamImpl.setWriteListener(ServletOutputStreamImpl.java:807)
          at org.jboss.resteasy.plugins.server.servlet.HttpServletResponseWrapper$WrappedServletOutputStream.setWriteListener(HttpServletResponseWrapper.java:591)
          at org.jboss.resteasy.plugins.server.servlet.HttpServletResponseWrapper$DeferredOutputStream.queue(HttpServletResponseWrapper.java:298)
          at org.jboss.resteasy.plugins.server.servlet.HttpServletResponseWrapper$DeferredOutputStream.asyncWrite(HttpServletResponseWrapper.java:281)
          at org.jboss.resteasy.util.CommitHeaderAsyncOutputStream.asyncWrite(CommitHeaderAsyncOutputStream.java:80)
          at org.jboss.resteasy.spi.AsyncOutputStream.asyncWrite(AsyncOutputStream.java:25)
          at org.jboss.resteasy.plugins.providers.multipart.AbstractMultipartWriter.asyncWritePart(AbstractMultipartWriter.java:202)
          at org.jboss.resteasy.plugins.providers.multipart.AbstractMultipartFormDataWriter.lambda$asyncWriteParts$0(AbstractMultipartFormDataWriter.java:62)
          at java.base/java.util.concurrent.CompletableFuture.uniComposeStage(CompletableFuture.java:1187)
          at java.base/java.util.concurrent.CompletableFuture.thenCompose(CompletableFuture.java:2341)
          at java.base/java.util.concurrent.CompletableFuture.thenCompose(CompletableFuture.java:144)
          at org.jboss.resteasy.plugins.providers.multipart.AbstractMultipartFormDataWriter.asyncWriteParts(AbstractMultipartFormDataWriter.java:62)
          at org.jboss.resteasy.plugins.providers.multipart.AbstractMultipartWriter.asyncWrite(AbstractMultipartWriter.java:137)
          at org.jboss.resteasy.plugins.providers.multipart.MultipartEntityPartWriter.lambda$asyncWriteTo$1(MultipartEntityPartWriter.java:58)
          at java.base/java.util.concurrent.CompletableFuture$UniCompose.tryFire(CompletableFuture.java:1150)
          at java.base/java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:510)
          at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1773)
          at org.jboss.resteasy.concurrent.ContextualExecutors.lambda$runnable$2(ContextualExecutors.java:312)
          at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
          at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
          at java.base/java.lang.Thread.run(Thread.java:1583)
      

      The ServletOutputStreamImpl.setWriterListener() looks like the following:

      asyncContext = (AsyncContextImpl) servletRequest.getAsyncContext();
      listener = writeListener;
      //we register the write listener on the underlying connection
      //so we don't have to force the creation of the response channel
      //under normal circumstances this will break write listener delegation
      this.internalListener = new WriteChannelListener();
      if (this.channel != null) {
          this.channel.getWriteSetter().set(internalListener);
      }
      //we resume from an async task, after the request has been dispatched
      asyncContext.addAsyncTask(new Runnable() {
          @Override
          public void run() {
              asyncIoStarted = true;
              if (channel == null) {
                  servletRequestContext.getExchange().getIoThread().execute(new Runnable() {
                      @Override
                      public void run() {
                          internalListener.handleEvent(null);
                      }
                  });
              } else {
                  channel.resumeWrites();
              }
          }
      });
      

      The servletRequest.getAsyncContext() should throw an IllegalStateException if startAsync() was not invoked, so that must have been done. However, the returned asyncContext ends up being null.

      I'm not really sure what the best non-blocking/non-locking fix is. The first thought was to use a try/finally where we set the asyncStarted in the finally in the io.undertow.servlet.spec.HttpServletRequestImpl. However, if an error occurs creating the AsyncContextImpl, then we leave the request in an invalid state. That still leaves a potential for the async context to get started twice as well.

              jperkins-rhn James Perkins
              jperkins-rhn James Perkins
              Votes:
              0 Vote for this issue
              Watchers:
              1 Start watching this issue

                Created:
                Updated:
                Resolved: