Skip to content

Quarkus REST Client hangs when sending InputStream in multipart, "works" in RESTeasy Client #47884

@abjugard

Description

@abjugard

Describe the bug

When doing streaming file uploads, unless the uploaded file is very very small (rendering the whole point of streaming the upload moot), Quarkus REST Client hangs (at line 5 of private <T> T unwrap(CompletableFuture<T> c), the line return c.get(), in org.jboss.resteasy.reactive.client.impl.InvocationBuilderImpl) when providing an InputStream created from an incoming multipart request in an multipart message to the downstream API.

Expected behavior

Request is streamed to downstream API.

Actual behavior

Request hangs.

How to Reproduce?

Example code that consistently demonstrates the issue:
streamed-uploads.zip
streamed-uploads-resteasy.zip

Example Quarkus REST server to represent our downstream API that will eventually receive the uploaded file.
downstream-api.zip

The above two projects do the same thing, they accept an incoming multipart request, create a FileData containing an InputStream representing the as of yet not fully received HttpServletRequest (via quarkus-undertow and commons-fileupload2-jakarta-servlet6), they then build a UploadRequest containing a piece of form data, and the previously mentioned InputStream. One uses Quarkus REST and REST Client, the other uses Quarkus RESTeasy and RESTeasy Client. The latter works (sort of) as expected, but the former does not.

I have attached some sample data here, one is a 1x1 bmp file that's only 58 bytes, the other is a 5000x5000 jpg at about 98kb.
sample-data.zip

The code can be tested using e.g.

curl http://localhost:8080/upload -F "contentType=image/bmp" -F "fileName=image.bmp" -F "file=@1x1.bmp"
curl http://localhost:8080/upload -F "contentType=image/jpeg" -F "fileName=image.jpg" -F "file=@5000x5000.jpg"

Suggested test:

  1. Start the downstream-api
  2. Start the streamed-uploads-resteasy application (this is the Quarkus RESTeasy + RESTeasy Client version)
  3. Run both curl commands above, they should execute fine.
  4. Kill the streamed-uploads-resteasy application and start the streamed-uploads application (this is the Quarkus REST + REST Client version)
  5. Run both curl commands above, the first will execute as it should, but the second will not.
  6. Comment line 31 and uncomment lines 32-33 in UploadController of the streamed-uploads project. This just reads the same 5000x5000 jpg image from disk and uses that as the InputStream to the downstream-api.
  7. Try calling the endpoint again, it should now run fine, even though it's sending the same 98kb jpg image as an InputStream to the downstream-api as it was in the second curl command above, which hangs.

Output of uname -a or ver

Darwin bullen.local 24.5.0 Darwin Kernel Version 24.5.0: Tue Apr 22 19:54:25 PDT 2025; root:xnu-11417.121.6~2/RELEASE_ARM64_T6020 arm64 arm Darwin

Output of java -version

openjdk version "21.0.5" 2024-10-15 LTS
OpenJDK Runtime Environment Corretto-21.0.5.11.1 (build 21.0.5+11-LTS)
OpenJDK 64-Bit Server VM Corretto-21.0.5.11.1 (build 21.0.5+11-LTS, mixed mode, sharing)

Quarkus version or git rev

3.22.2

Build tool (ie. output of mvnw --version or gradlew --version)

Apache Maven 3.9.9 (8e8579a9e76f7d015ee5ec7bfcdc97d260186937)
Maven home: /opt/homebrew/Cellar/maven/3.9.9/libexec
Java version: 21.0.5, vendor: Amazon.com Inc., runtime: /Users/rekoil/.sdkman/candidates/java/21.0.5-amzn
Default locale: en_GB, platform encoding: UTF-8
OS name: "mac os x", version: "15.5", arch: "aarch64", family: "mac"

Additional information

If you have python and python-flask installed here's a simpler downstream API that just prints all the received headers and file length to the console:
downstream-api-python.zip

When I did this, I noticed that both versions are sending a Content-Length header (which they shouldn't), whereas when performing the test with the FileInputStream then it is correctly sending a Transfer-Encoding: chunked header (because REST/RESTeasy Client can't know the full Content-Length value until it's too late), so I guess that it is actually resolving the InputStream to bytes before starting the request?

The goal here is that it should not be doing that, but should be setting Transfer-Encoding: chunked and not necessarily resolving the InputStream until potentially long after the downstream API has been called.

This is where my understanding of the inner workings of Quarkus limit me from figuring out how to proceed. Hopefully someone here can help me figure this out!

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions