Repository: quarkusio/quarkus (2025-07-14T09:17:50Z)

Issue:
BlockingNotAllowedException thrown in REST Client after #48807
### Describe the bug

After the changes introduced in https://github.com/quarkusio/quarkus/pull/48807, our tests involving REST Client started failing with `BlockingNotAllowedException` when calling endpoints that do blocking operations, even though they worked fine before.

TS CI run:
https://github.com/quarkus-qe/quarkus-test-suite/actions/runs/16211306867/job/45771859864

### Expected behavior

If this change is intentional, then it should be documented in the Migration Guide, as it causes tests using the Quarkus REST Client to fail unless they are adapted (e.g., by adding @Blocking). If this is not an intended regression, we would expect previous behavior (non-blocking client usage without mandatory @Blocking) to continue working.

### Actual behavior

```
025-07-11T04:01:25.3032414Z 04:01:25,278 INFO  [app] 04:01:23,507 A blocking operation occurred on the IO thread. This likely means you need to annotate io.quarkus.ts.http.advanced.reactive.ClientResource#get() with @io.smallrye.common.annotation.Blocking. Alternatively you can annotate the class io.quarkus.ts.http.advanced.reactive.ClientResource to make every method on the class blocking, or annotate your sub class of the jakarta.ws.rs.core.Application class to make the whole application blocking
2025-07-11T04:01:25.3046741Z 04:01:25,278 INFO  [app] 04:01:23,512 HTTP Request to /api/client failed, error id: 7d0c65a9-8328-4677-b21c-727815712ee6-1: org.jboss.resteasy.reactive.common.core.BlockingNotAllowedException: Attempting a blocking read on io thread
2025-07-11T04:01:25.3049070Z 04:01:25,278 INFO  [app] 	at org.jboss.resteasy.reactive.client.handlers.VertxClientInputStream$VertxBlockingInput.readBlocking(VertxClientInputStream.java:187)
2025-07-11T04:01:25.3051057Z 04:01:25,278 INFO  [app] 	at org.jboss.resteasy.reactive.client.handlers.VertxClientInputStream.readIntoBuffer(VertxClientInputStream.java:69)
2025-07-11T04:01:25.3053099Z 04:01:25,278 INFO  [app] 	at org.jboss.resteasy.reactive.client.handlers.VertxClientInputStream.close(VertxClientInputStream.java:97)
2025-07-11T04:01:25.3059544Z 04:01:25,278 INFO  [app] 	at org.jboss.resteasy.reactive.common.providers.serialisers.InputStreamMessageBodyHandler.writeTo(InputStreamMessageBodyHandler.java:41)
2025-07-11T04:01:25.3062108Z 04:01:25,278 INFO  [app] 	at org.jboss.resteasy.reactive.server.providers.serialisers.ServerInputStreamMessageBodyHandler.writeResponse(ServerInputStreamMessageBodyHandler.java:46)
2025-07-11T04:01:25.3065082Z 04:01:25,278 INFO  [app] 	at org.jboss.resteasy.reactive.server.providers.serialisers.ServerInputStreamMessageBodyHandler.writeResponse(ServerInputStreamMessageBodyHandler.java:17)
2025-07-11T04:01:25.3067175Z 04:01:25,278 INFO  [app] 	at org.jboss.resteasy.reactive.server.core.ServerSerialisers.invokeWriter(ServerSerialisers.java:218)
2025-07-11T04:01:25.3068852Z 04:01:25,278 INFO  [app] 	at org.jboss.resteasy.reactive.server.core.serialization.DynamicEntityWriter.write(DynamicEntityWriter.java:119)
2025-07-11T04:01:25.3070522Z 04:01:25,278 INFO  [app] 	at org.jboss.resteasy.reactive.server.handlers.ResponseWriterHandler.handle(ResponseWriterHandler.java:32)
2025-07-11T04:01:25.3072582Z 04:01:25,278 INFO  [app] 	at io.quarkus.resteasy.reactive.server.runtime.QuarkusResteasyReactiveRequestContext.invokeHandler(QuarkusResteasyReactiveRequestContext.java:147)
2025-07-11T04:01:25.3074907Z 04:01:25,278 INFO  [app] 	at org.jboss.resteasy.reactive.common.core.AbstractResteasyReactiveContext.run(AbstractResteasyReactiveContext.java:147)
2025-07-11T04:01:25.3076887Z 04:01:25,278 INFO  [app] 	at org.jboss.resteasy.reactive.server.vertx.VertxResteasyReactiveRequestContext$1$1.handle(VertxResteasyReactiveRequestContext.java:92)
2025-07-11T04:01:25.3078952Z 04:01:25,278 INFO  [app] 	at org.jboss.resteasy.reactive.server.vertx.VertxResteasyReactiveRequestContext$1$1.handle(VertxResteasyReactiveRequestContext.java:89)
2025-07-11T04:01:25.3080525Z 04:01:25,278 INFO  [app] 	at io.vertx.core.impl.ContextInternal.dispatch(ContextInternal.java:270)
2025-07-11T04:01:25.3081528Z 04:01:25,278 INFO  [app] 	at io.vertx.core.impl.ContextInternal.dispatch(ContextInternal.java:252)
2025-07-11T04:01:25.3082612Z 04:01:25,278 INFO  [app] 	at io.vertx.core.impl.ContextInternal.lambda$runOnContext$0(ContextInternal.java:50)
2025-07-11T04:01:25.3083841Z 04:01:25,278 INFO  [app] 	at io.netty.util.concurrent.AbstractEventExecutor.runTask(AbstractEventExecutor.java:173)
2025-07-11T04:01:25.3085404Z 04:01:25,279 INFO  [app] 	at io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java:166)
2025-07-11T04:01:25.3086932Z 04:01:25,279 INFO  [app] 	at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:472)
2025-07-11T04:01:25.3088143Z 04:01:25,279 INFO  [app] 	at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:566)
2025-07-11T04:01:25.3089530Z 04:01:25,279 INFO  [app] 	at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:998)
2025-07-11T04:01:25.3090764Z 04:01:25,279 INFO  [app] 	at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
2025-07-11T04:01:25.3091949Z 04:01:25,279 INFO  [app] 	at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
2025-07-11T04:01:25.3092961Z 04:01:25,279 INFO  [app] 	at java.base/java.lang.Thread.run(Thread.java:1583)
2025-07-11T04:01:25.3093625Z 04:01:25,279 INFO  [app] 04:01:24,674 Creating Netty gRPC channel ...
```

### How to Reproduce?

1. `git clone git@github.com:quarkus-qe/quarkus-test-suite.git`
2. `cd quarkus-test-suite/http/http-advanced-reactive`
3. `mvn clean verify -Dit.test=HttpAdvancedReactiveIT`

### Output of `uname -a` or `ver`

Linux fedora 6.13.9-200.fc41.x86_64

### Output of `java -version`

OpenJDK 64-Bit Server VM Temurin-21.0.7+6 (build 21.0.7+6-LTS, mixed mode, sharing)

### Quarkus version or git rev

999-SNAPSHOT

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

Apache Maven 3.9.6 (Red Hat 3.9.6-7)

### Additional information

_No response_


Discussion:
/cc @cescoffier (rest-client), @geoand (rest-client)
Thanks for raising this!

I'll have a look on Monday
The scenario being tested is very much an edge case as you are getting a `Uni<Response>` from the client passing it on as the response of the server, while on the event loop. 
I understand what you are trying to do, but I want to think about it a little more
Since my update from version 3.24.1 to 3.25.0 I see a similar problem, but I am not using Mutiny return types. My setup is a simple as the following:

```
        final AttachmentDTO attachment = attachmentClient.uploadReceipt(request.getAttachment());
        final Response attachmentData = attachmentsClient.attachments().download(attachment.getId());
        final InputStream streamData = attachmentData.readEntity(InputStream.class);
        final ParseReceiptMultipartFormDTO payload = new ParseReceiptMultipartFormDTO();
        payload.setReceipt(streamData);
        final ReceiptDTO receipt = parseClient.receipts().parse(payload);
```

On Quarkus 3.24.1, this is working fine and I get a response within `receipt`, but when updating to 2.25.0, the very same code throws the following exception on the `parseClient.receipts().parse(payload)` line (see below).

I tried already several different things, adding annotations (@Blocking, @Nonblocking, etc.), wrap it in reactive behavior, changing the client to return Uni. Nothing of this worked.

Not sure if it fits into this issue. 


```
2025-08-03 16:57:02,105 SEVERE [com.cyc.eam.jax.rs.ext.map.AbstractExceptionMapper] (executor-thread-4) Uncaught exception: http://localhost:3333/business/command/accounting/accounts/b3913534-aecc-4922-a9ca-4bf8d8e8ce00/transactions/receipt-transaction (POST) [Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryrwyorucGBZZOBsUp; Accept: application/json, text/plain, */*]: org.jboss.resteasy.reactive.common.core.BlockingNotAllowedException: Attempting a blocking read on io thread
	at org.jboss.resteasy.reactive.client.handlers.VertxClientInputStream$VertxBlockingInput.readBlocking(VertxClientInputStream.java:187)
	at org.jboss.resteasy.reactive.client.handlers.VertxClientInputStream.readIntoBuffer(VertxClientInputStream.java:69)
	at org.jboss.resteasy.reactive.client.handlers.VertxClientInputStream.close(VertxClientInputStream.java:97)
	at org.jboss.resteasy.reactive.common.providers.serialisers.InputStreamMessageBodyHandler.writeTo(InputStreamMessageBodyHandler.java:41)
	at org.jboss.resteasy.reactive.common.providers.serialisers.InputStreamMessageBodyHandler.writeTo(InputStreamMessageBodyHandler.java:33)
	at org.jboss.resteasy.reactive.common.providers.serialisers.InputStreamMessageBodyHandler.writeTo(InputStreamMessageBodyHandler.java:15)
	at org.jboss.resteasy.reactive.client.impl.ClientSerialisers.invokeClientWriter(ClientSerialisers.java:122)
	at org.jboss.resteasy.reactive.client.impl.multipart.QuarkusMultipartForm.preparePojos(QuarkusMultipartForm.java:55)
	at org.jboss.resteasy.reactive.client.handlers.ClientSendRequestHandler.setMultipartHeadersAndPrepareBody(ClientSendRequestHandler.java:494)
	at org.jboss.resteasy.reactive.client.handlers.ClientSendRequestHandler$1.accept(ClientSendRequestHandler.java:118)
	at org.jboss.resteasy.reactive.client.handlers.ClientSendRequestHandler$1.accept(ClientSendRequestHandler.java:101)
	at io.smallrye.context.impl.wrappers.SlowContextualConsumer.accept(SlowContextualConsumer.java:21)
	at io.smallrye.mutiny.helpers.UniCallbackSubscriber.onItem(UniCallbackSubscriber.java:73)
	at io.smallrye.mutiny.operators.uni.UniOnItemTransformToUni$UniOnItemTransformToUniProcessor.onItem(UniOnItemTransformToUni.java:60)
	at org.jboss.resteasy.reactive.client.AsyncResultUni.lambda$subscribe$1(AsyncResultUni.java:35)
	at io.vertx.core.impl.future.FutureImpl$4.onSuccess(FutureImpl.java:176)
	at io.vertx.core.impl.future.FutureBase.emitSuccess(FutureBase.java:66)
	at io.vertx.core.impl.future.FutureImpl.tryComplete(FutureImpl.java:259)
	at io.vertx.core.http.impl.HttpClientImpl.lambda$null$3(HttpClientImpl.java:392)
	at io.vertx.core.impl.future.FutureImpl$4.onSuccess(FutureImpl.java:176)
	at io.vertx.core.impl.future.FutureBase.lambda$emitSuccess$0(FutureBase.java:60)
	at io.vertx.core.impl.ContextImpl.execute(ContextImpl.java:312)
	at io.vertx.core.impl.DuplicatedContext.execute(DuplicatedContext.java:168)
	at io.vertx.core.impl.future.FutureBase.emitSuccess(FutureBase.java:57)
	at io.vertx.core.impl.future.FutureImpl.addListener(FutureImpl.java:231)
	at io.vertx.core.impl.future.FutureImpl.onComplete(FutureImpl.java:199)
	at io.vertx.core.http.impl.Http1xClientConnection.createStream(Http1xClientConnection.java:1294)
	at io.vertx.core.http.impl.Http1xClientConnection.lambda$createStream$9(Http1xClientConnection.java:1300)
	at io.netty.util.concurrent.AbstractEventExecutor.runTask(AbstractEventExecutor.java:173)
	at io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java:166)
	at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:472)
	at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:569)
	at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:998)
	at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
	at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
	at java.base/java.lang.Thread.run(Thread.java:1583)
```
Could you open an issue with a small Maven reproducer? Thanks!



Patch:
diff --git a/extensions/resteasy-reactive/rest-client/deployment/pom.xml b/extensions/resteasy-reactive/rest-client/deployment/pom.xml
index cbed3dd2a3de7..a793cb435e3ea 100644
--- a/extensions/resteasy-reactive/rest-client/deployment/pom.xml
+++ b/extensions/resteasy-reactive/rest-client/deployment/pom.xml
@@ -67,6 +67,11 @@
             <artifactId>quarkus-junit5-internal</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>io.quarkus</groupId>
+            <artifactId>quarkus-test-vertx</artifactId>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>org.junit-pioneer</groupId>
             <artifactId>junit-pioneer</artifactId>
diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientSendRequestHandler.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientSendRequestHandler.java
index 6847a79223fd4..b7954e35c1428 100644
--- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientSendRequestHandler.java
+++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientSendRequestHandler.java
@@ -343,7 +343,12 @@ public void handle(AsyncResult<Void> event) {
                                         }
                                     });
 
-                        } else if (requestContext.isInputStreamDownload() || requestContext.isJakartaResponseDownload()) {
+                        } else if (requestContext.isInputStreamDownload() ||
+                        // when returning Response (and not Uni<Response>) we don't buffer the result
+                        // but instead set up the proper InputStream
+                        // for Uni<Response> we can't buffer because setting up the InputSteam would result in blocking the event loop
+                                (requestContext.isJakartaResponseDownload()
+                                        && !requestContext.invokedMethodReturnsAsyncType())) {
                             if (loggingScope != LoggingScope.NONE) {
                                 clientLogger.logResponse(clientResponse, false);
                             }
diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/RestClientRequestContext.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/RestClientRequestContext.java
index f825aba782461..604fa41271c40 100644
--- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/RestClientRequestContext.java
+++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/RestClientRequestContext.java
@@ -14,6 +14,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
 import java.util.concurrent.Executor;
 import java.util.concurrent.atomic.AtomicBoolean;
 
@@ -44,6 +45,7 @@
 
 import io.netty.handler.codec.http.multipart.InterfaceHttpData;
 import io.smallrye.mutiny.Multi;
+import io.smallrye.mutiny.Uni;
 import io.smallrye.stork.api.ServiceInstance;
 import io.vertx.core.Context;
 import io.vertx.core.MultiMap;
@@ -174,6 +176,16 @@ public Method getInvokedMethod() {
         return null;
     }
 
+    public boolean invokedMethodReturnsAsyncType() {
+        Method invokedMethod = getInvokedMethod();
+        if (invokedMethod == null) {
+            return false;
+        }
+        Class<?> returnType = invokedMethod.getReturnType();
+        return Uni.class.isAssignableFrom(returnType) || Multi.class.isAssignableFrom(returnType)
+                || CompletionStage.class.isAssignableFrom(returnType);
+    }
+
     public Annotation[] getMethodDeclaredAnnotationsSafe() {
         Method invokedMethod = getInvokedMethod();
         if (invokedMethod != null) {


Test patch:
diff --git a/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/ResponseConsumptionTest.java b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/ResponseConsumptionTest.java
index 0b831553582be..11e7831804e7e 100644
--- a/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/ResponseConsumptionTest.java
+++ b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/ResponseConsumptionTest.java
@@ -4,6 +4,7 @@
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 import java.net.URI;
+import java.util.Random;
 
 import jakarta.ws.rs.GET;
 import jakarta.ws.rs.Path;
@@ -17,6 +18,9 @@
 
 import io.quarkus.test.QuarkusUnitTest;
 import io.quarkus.test.common.http.TestHTTPResource;
+import io.quarkus.test.vertx.RunOnVertxContext;
+import io.quarkus.test.vertx.UniAsserter;
+import io.smallrye.mutiny.Uni;
 
 public class ResponseConsumptionTest {
 
@@ -28,12 +32,17 @@ public class ResponseConsumptionTest {
     URI uri;
 
     @Test
-    public void test() throws InterruptedException {
-        Client client = RestClientBuilder.newBuilder()
-                .baseUri(uri)
-                .build(Client.class);
+    public void testBlockingOk() {
+        Client client = createClient();
+        Response response200 = client.get200();
+        assertThat(response200.getStatus()).isEqualTo(200);
+        assertThat(response200.readEntity(byte[].class)).hasSize(Resource.OK_RESPONSE_SIZE);
+    }
 
-        assertThatThrownBy(client::get).isInstanceOfSatisfying(WebApplicationException.class, w -> {
+    @Test
+    public void testBlockingError() {
+        Client client = createClient();
+        assertThatThrownBy(client::get401).isInstanceOfSatisfying(WebApplicationException.class, w -> {
             Response response = w.getResponse();
             // the response should have the response code from the api call
             assertThat(response.getStatus()).isEqualTo(401);
@@ -42,19 +51,76 @@ public void test() throws InterruptedException {
         });
     }
 
+    @Test
+    @RunOnVertxContext
+    public void testEventLoopOk(UniAsserter asserter) {
+        Client client = createClient();
+
+        asserter.assertThat(client::uniGet200, (res) -> {
+            assertThat(res.getStatus()).isEqualTo(200);
+            assertThat(res.readEntity(byte[].class)).hasSize(Resource.OK_RESPONSE_SIZE);
+        });
+    }
+
+    @Test
+    @RunOnVertxContext
+    public void testEventLoopError(UniAsserter asserter) {
+        Client client = createClient();
+
+        asserter.assertThat(() -> {
+            return client.uniGet401().onFailure(WebApplicationException.class).recoverWithItem(wae -> {
+                return ((WebApplicationException) wae).getResponse();
+            });
+        }, (res) -> {
+            assertThat(res.getStatus()).isEqualTo(401);
+            assertThat(res.readEntity(String.class)).isEqualTo("Unauthorized");
+        });
+    }
+
+    private Client createClient() {
+        return RestClientBuilder.newBuilder()
+                .baseUri(uri)
+                .build(Client.class);
+    }
+
     @Path("resource")
     public interface Client {
 
         @GET
-        Response get();
+        @Path("401")
+        Response get401();
+
+        @GET
+        @Path("401")
+        Uni<Response> uniGet401();
+
+        @GET
+        @Path("200")
+        Response get200();
+
+        @GET
+        @Path("200")
+        Uni<Response> uniGet200();
     }
 
     @Path("resource")
     public static class Resource {
 
+        public static final int OK_RESPONSE_SIZE = 2 * 1024 * 1024;
+
+        @Path("401")
         @GET
-        public RestResponse<String> get() throws InterruptedException {
+        public RestResponse<String> get401() {
             return RestResponse.status(RestResponse.Status.UNAUTHORIZED, "Unauthorized");
         }
+
+        @Path("200")
+        @GET
+        public RestResponse<byte[]> get200() {
+            byte[] byteArray = new byte[OK_RESPONSE_SIZE];
+            Random random = new Random();
+            random.nextBytes(byteArray);
+            return RestResponse.ok(byteArray);
+        }
     }
 }