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

IllegalAccessError when POSTing entity larger than in-memory threshold via ManualClosingApacheHttpClient43Engine

XMLWordPrintable

    • Icon: Bug Bug
    • Resolution: Unresolved
    • Icon: Blocker Blocker
    • 6.2.16.Final, 7.0.2.Final
    • 7.0.1.Final
    • None
    • None
    • Hide

      This only triggers when the request body exceeds the in-memory threshold (default 1MB, controlled by dev.resteasy.entity.memory.threshold), because that's when toEntity() creates a PathHttpEntity instead of a ByteArrayEntity.

      1. Use RESTEasy 7.0.1.Final as the JAX-RS client implementation
      2. POST a body larger than 1MB (the default dev.resteasy.entity.memory.threshold)
      3. The ManualClosingApacheHttpClient43Engine will write the body to a ClientEntityOutputStream, which spills to a temp file, and then toEntity() tries to create a PathHttpEntity -> IllegalAccessError

      JUnit Reproducer (AI generated):

      import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
      
      import jakarta.ws.rs.client.Client;
      import jakarta.ws.rs.client.ClientBuilder;
      import jakarta.ws.rs.client.Entity;
      import jakarta.ws.rs.core.MediaType;
      import java.io.File;
      import java.nio.file.Files;
      import org.junit.jupiter.api.Test;
      
      /**
       * Reproduces IllegalAccessError in RESTEasy 7.0.1.Final when POSTing a request
       * body larger than the default 1MB in-memory entity threshold.
       *
       * <p>The error occurs because ClientEntityOutputStream$PathHttpEntity (a private
       * static inner class in resteasy-client) tries to instantiate
       * EntityOutputStream$EntityInputStream (a protected static inner class in
       * resteasy-core-spi). Since PathHttpEntity does not extend EntityOutputStream,
       * the JVM denies access to the protected class.
       */
      class IllegalAccessErrorReproducerTest {
      
          @Test
          void postLargeEntityShouldNotThrowIllegalAccessError() throws Exception {
              File file = File.createTempFile("resteasy-repro", ".bin");
              file.deleteOnExit();
              // 2MB exceeds the default 1MB in-memory threshold, forcing RESTEasy to
              // use the file-backed PathHttpEntity code path
              byte[] data = new byte[2 * 1024 * 1024];
              Files.write(file.toPath(), data);
      
              try (Client client = ClientBuilder.newClient()) {
                  // The IllegalAccessError is thrown during entity serialization in
                  // ManualClosingApacheHttpClient43Engine.buildEntity(), before any
                  // network I/O. A ConnectException is the expected outcome when the
                  // bug is fixed (no server is listening).
                  assertDoesNotThrow(() -> {
                      try {
                          client.target("http://localhost:1/test")
                                  .request()
                                  .post(Entity.entity(file, MediaType.APPLICATION_OCTET_STREAM));
                      } catch (jakarta.ws.rs.ProcessingException e) {
                          if (e.getCause() instanceof java.net.ConnectException) {
                              // Expected: no server listening. The important thing is
                              // that we got past entity serialization without
                              // IllegalAccessError.
                              return;
                          }
                          throw e;
                      }
                  });
              }
          }
      }
      
      Show
      This only triggers when the request body exceeds the in-memory threshold (default 1MB, controlled by dev.resteasy.entity.memory.threshold), because that's when toEntity() creates a PathHttpEntity instead of a ByteArrayEntity. 1. Use RESTEasy 7.0.1.Final as the JAX-RS client implementation 2. POST a body larger than 1MB (the default dev.resteasy.entity.memory.threshold) 3. The ManualClosingApacheHttpClient43Engine will write the body to a ClientEntityOutputStream, which spills to a temp file, and then toEntity() tries to create a PathHttpEntity -> IllegalAccessError JUnit Reproducer (AI generated): import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import jakarta.ws.rs.client.Client; import jakarta.ws.rs.client.ClientBuilder; import jakarta.ws.rs.client.Entity; import jakarta.ws.rs.core.MediaType; import java.io.File; import java.nio.file.Files; import org.junit.jupiter.api.Test; /** * Reproduces IllegalAccessError in RESTEasy 7.0.1.Final when POSTing a request * body larger than the default 1MB in-memory entity threshold. * * <p>The error occurs because ClientEntityOutputStream$PathHttpEntity (a private * static inner class in resteasy-client) tries to instantiate * EntityOutputStream$EntityInputStream (a protected static inner class in * resteasy-core-spi). Since PathHttpEntity does not extend EntityOutputStream, * the JVM denies access to the protected class. */ class IllegalAccessErrorReproducerTest { @Test void postLargeEntityShouldNotThrowIllegalAccessError() throws Exception { File file = File.createTempFile( "resteasy-repro" , ".bin" ); file.deleteOnExit(); // 2MB exceeds the default 1MB in-memory threshold, forcing RESTEasy to // use the file-backed PathHttpEntity code path byte [] data = new byte [2 * 1024 * 1024]; Files.write(file.toPath(), data); try (Client client = ClientBuilder.newClient()) { // The IllegalAccessError is thrown during entity serialization in // ManualClosingApacheHttpClient43Engine.buildEntity(), before any // network I/O. A ConnectException is the expected outcome when the // bug is fixed (no server is listening). assertDoesNotThrow(() -> { try { client.target( "http: //localhost:1/test" ) .request() .post(Entity.entity(file, MediaType.APPLICATION_OCTET_STREAM)); } catch (jakarta.ws.rs.ProcessingException e) { if (e.getCause() instanceof java.net.ConnectException) { // Expected: no server listening. The important thing is // that we got past entity serialization without // IllegalAccessError. return ; } throw e; } }); } } }
    • Hide

      I didn't find any workaround, but not using 7.0.1.Final.

      Show
      I didn't find any workaround, but not using 7.0.1.Final.

      RESTEASY-3670 introduced ClientEntityOutputStream$PathHttpEntity, a private static inner class that instantiates EntityOutputStream$EntityInputStream directly:

      // ClientEntityOutputStream.java (resteasy-client)
      private static class PathHttpEntity extends AbstractHttpEntity {
          private PathHttpEntity(final Path file) {
              this.file = file;
              this.content = new EntityInputStream(file);  // <-- fails here
          }
      }
      

      EntityInputStream was changed from private to protected in the same PR:

      // EntityOutputStream.java (resteasy-core-spi)
      protected static class EntityInputStream extends InputStream { ... }
      

      The problem is that protected access in Java means the member is visible to:
      1. Classes in the same package (org.jboss.resteasy.spi)
      2. Subclasses of the declaring class (EntityOutputStream)

      ClientEntityOutputStream extends EntityOutputStream, so it can access EntityInputStream. However, PathHttpEntity is a static inner class of ClientEntityOutputStream – it does not extend EntityOutputStream. Therefore, the JVM denies access at runtime with:

      java.lang.IllegalAccessError: failed to access class
        org.jboss.resteasy.spi.EntityOutputStream$EntityInputStream
        from class
        org.jboss.resteasy.client.jaxrs.engines.ClientEntityOutputStream$PathHttpEntity
        (... are in unnamed module of loader 'app')
      

              jperkins-rhn James Perkins
              jcarvaja@redhat.com Jose Carvajal Hilario
              Votes:
              0 Vote for this issue
              Watchers:
              1 Start watching this issue

                Created:
                Updated: