Uploaded image for project: 'RESTEasy'
  1. RESTEasy
  2. RESTEASY-3126

Multipart request together with the RX API and an AsyncHttpClientEngine does not work (Cannot find Providers)

    XMLWordPrintable

Details

    • Bug
    • Resolution: Unresolved
    • Major
    • None
    • 5.0.3.Final, 6.0.1.Final
    • jaxrs
    • None
    • Hide

      I originally encountered this issue with the 5.x version, but I can reproduce the error with 6.0.1. Download the attached ZIP file, which contains a simple maven project adapted from https://github.com/resteasy/resteasy-examples/tree/main/jaxrs-2.0/simple-client

       

      Unzip, cd into the directory with the pom.xml, make sure you are using Java 17 and run

      mvn verify

      That will run 2 tests, once with the sync ApacheHttpClient43Engine and once with the ApacheHttpAsyncClient4Engine. The latter one fails with the error as described below in the description.

      Show
      I originally encountered this issue with the 5.x version, but I can reproduce the error with 6.0.1. Download the attached ZIP file, which contains a simple maven project adapted from https://github.com/resteasy/resteasy-examples/tree/main/jaxrs-2.0/simple-client   Unzip, cd into the directory with the pom.xml, make sure you are using Java 17 and run mvn verify That will run 2 tests, once with the sync ApacheHttpClient43Engine and once with the ApacheHttpAsyncClient4Engine. The latter one fails with the error as described below in the description.

    Description

      I came across this issue when I was trying to use RestEasy with an application originally generated by swagger, though that does not seem to be relevant since I can reproduce with a simple test project (see step to reproduce below).

      Consider the following code that tries to send a POST request with multipart content via the reactive RX API:

      MultipartFormDataOutput multipart = new MultipartFormDataOutput();
      multipart.addFormData("key1", "val1", MediaType.TEXT_PLAIN_TYPE);
      multipart.addFormData("key2", "val2", MediaType.TEXT_PLAIN_TYPE);
      multipart.addFormData("key3", "val3", MediaType.TEXT_PLAIN_TYPE);
      Entity<?> entity = Entity.entity(multipart, MediaType.MULTIPART_FORM_DATA_TYPE);
      WebTarget target = client.target("http://localhost:9095/customers");
      target.request().rx().post(entity).handle((response, error) -> {
        // do something with the response...
        return null;
      }

      That code works as long as we use a synchronous ClientHttpEngine such as ApacheHttpClient43Engine. But once we use an asynchronous AsyncClientHttpEngine such as ApacheHttpAsyncClient4Engine (we are using a custom engine implementation, but again, does not seem to be relevant, since I can reproduce with the default Apache engine), the code fails with the following error:

      java.util.concurrent.CompletionException: org.jboss.resteasy.spi.LoggableFailure: RESTEASY003880: Unable to find contextual data of type: jakarta.ws.rs.ext.Providers
          at java.base/java.util.concurrent.CompletableFuture.encodeThrowable(CompletableFuture.java:315)
          at java.base/java.util.concurrent.CompletableFuture.completeThrowable(CompletableFuture.java:320)
          at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1770)
          at org.jboss.resteasy.concurrent.ContextualExecutors.lambda$runnable$2(ContextualExecutors.java:316)
          at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
          at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
          at java.base/java.lang.Thread.run(Thread.java:833)
       

      After a little big of debugging, the issue seems to be in the way the context is handled by org.jboss.resteasy.client.jaxrs.internal.ClientInvocation.

      When the engine does not implement HttpClientEngine, it uses the method executorSubmit which calls invoke which then pushes the provider via pushProvidersContext and then calls httpEngine.invoke(). Now when the engine reads the request body, the Providers are available.

      But when the engine implements AsyncHttpClientEngine, it uses the method asyncSubmit

      https://github.com/resteasy/resteasy/blob/main/resteasy-client/src/main/java/org/jboss/resteasy/client/jaxrs/internal/ClientInvocation.java#L814-L843

       

         private <Q extends Future<T>, T> Q asyncSubmit(
                 final Function<ResultExtractor<T>, Q> asyncHttpEngineSubmitFn,
                 final ResultExtractor<T> extractor,
                 final Function<T, Q> abortedFn,
                 final Function<Exception, Q> exceptionFn)
         {
            final ClientRequestContextImpl requestContext = new ClientRequestContextImpl(this);
            try(CloseableContext ctx = pushProvidersContext())
            {
               ClientResponse aborted = filterRequest(requestContext);
               if (aborted != null)
               {
                  // spec requires that aborted response go through filter/interceptor chains.
                  aborted = filterResponse(requestContext, aborted);
                  T result = extractor.extractResult(aborted);
                  return abortedFn.apply(result);
               }
            }
            catch (Exception ex)
            {
               return exceptionFn.apply(ex);
            }      return asyncHttpEngineSubmitFn.apply(response -> {
               try(CloseableContext ctx = pushProvidersContext())
               {
                  return extractor.extractResult(filterResponse(requestContext, response));
               }
            });
         }

      As you can see, that method pushes the providers when it applies the filters, and also before it evaluates the respones – but, and that seems to be the issue, not when it calls the engine via exceptionFn.apply. So when the engine reads the request body, it does not have the Providers, resulting in the above error.

      The AsyncHttpClientEngine is passed an asyncInvocationExecutor, but that does not help, since that executor only copies the context to the new thead, but the current context does not have such a context to begin with.

      You can also readily verify this during debugging via the following expression, which evaluates to null when the async engine is called.

      org.jboss.resteasy.core.ResteasyContext.getContextData(jakarta.ws.rs.ext.Providers.class) 

       Is this a real bug or am I doing something wrong?

      Attachments

        Activity

          People

            dkafe Dimitris Kafetzis
            andrewachsmuth Andre Wachsmuth (Inactive)
            Votes:
            0 Vote for this issue
            Watchers:
            2 Start watching this issue

            Dates

              Created:
              Updated: