diff --git a/README.md b/README.md index b19bef162..1bbe2803c 100644 --- a/README.md +++ b/README.md @@ -339,6 +339,17 @@ AsyncHttpClient client = asyncHttpClient(config() .setHttp2CleartextEnabled(true)); // h2c prior knowledge ``` +When a handler suspends a response with `ResponseBodyControl`, the HTTP/2 +per-stream window remains the buffering bound for that response. While at least +one response on a connection is suspended, AHC continues returning +connection-level credit so it cannot stall sibling streams. Connections with no +active suspension retain the normal 65,535-byte shared connection-window bound. +Once the last suspension ends, normal connection accounting resumes, although +credit already returned and data already queued cannot be revoked. Aggregate +queued response data during suspension can scale with the number of concurrent +streams. Use `http2InitialWindowSize` and `http2MaxConcurrentStreams` together +when an application needs a tighter aggregate bound. + To force HTTP/1.1, disable HTTP/2: ```java diff --git a/client/src/main/java/org/asynchttpclient/AsyncHandler.java b/client/src/main/java/org/asynchttpclient/AsyncHandler.java index 22451fe09..b2eb82f17 100644 --- a/client/src/main/java/org/asynchttpclient/AsyncHandler.java +++ b/client/src/main/java/org/asynchttpclient/AsyncHandler.java @@ -34,6 +34,7 @@ *
+ * The control is thread-safe and remains valid until its response completes. Calls made after completion have no + * effect. + *
+ * A control is also supplied when the final response headers end the response without a body. In that case, + * {@link #suspend()} cannot defer completion: the control becomes inactive when + * {@link AsyncHandler#onResponseBodyStart(ResponseBodyControl)} returns, and later calls have no effect. + * + * @since 3.0.14 + */ +public interface ResponseBodyControl { + + /** + * Stops requesting additional response bytes from the transport. Body parts that were already read may still be + * delivered to the {@link AsyncHandler}. + * If the final response headers already ended the response, this call has no effect on completion. + *
+ * While reads are suspended, the read timeout is paused but the request timeout remains active. If the request + * timeout is disabled, failing to resume or cancel the response can retain its transport resources indefinitely. + *
+ * For HTTP/2, while any response on a connection is suspended, AHC continues returning connection-level
+ * flow-control credit so a suspended stream cannot block sibling streams. Responses on connections with no active
+ * suspension retain the normal shared connection-window bound. The per-stream window always applies, so roughly
+ * {@link AsyncHttpClientConfig#getHttp2InitialWindowSize()} bytes can be queued for each suspended stream. Aggregate
+ * buffering during suspension can therefore scale with the number of concurrent streams. Once the last suspension
+ * ends, normal connection accounting resumes; credit already returned and data already queued cannot be revoked.
+ * Applications can bound buffering with {@link AsyncHttpClientConfig#getHttp2InitialWindowSize()} and
+ * {@link AsyncHttpClientConfig#getHttp2MaxConcurrentStreams()}.
+ */
+ void suspend();
+
+ /**
+ * Resumes requesting response bytes after a call to {@link #suspend()}.
+ */
+ void resume();
+
+ /**
+ * Stops processing the response body. As with {@link AsyncHandler.State#ABORT}, the handler is completed normally.
+ * Returning {@code ABORT} is the preferred way to stop from within an {@link AsyncHandler} callback; this method is
+ * intended for cancellation after the callback has returned, including from another thread.
+ */
+ void cancel();
+}
diff --git a/client/src/main/java/org/asynchttpclient/netty/NettyResponseBodyControl.java b/client/src/main/java/org/asynchttpclient/netty/NettyResponseBodyControl.java
new file mode 100644
index 000000000..65fae4f8b
--- /dev/null
+++ b/client/src/main/java/org/asynchttpclient/netty/NettyResponseBodyControl.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.asynchttpclient.netty;
+
+import io.netty.channel.Channel;
+import org.asynchttpclient.ResponseBodyControl;
+import org.jetbrains.annotations.ApiStatus;
+
+import java.util.Objects;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Consumer;
+
+/**
+ * Netty implementation of {@link ResponseBodyControl}.
+ */
+@ApiStatus.Internal
+public final class NettyResponseBodyControl implements ResponseBodyControl {
+
+ private final NettyResponseFuture> future;
+ private final Channel channel;
+ private final Runnable suspensionStartedAction;
+ private final Runnable suspensionEndedAction;
+ private final Runnable resumeAction;
+ private final Consumer The implementation follows {@code DefaultHttp2LocalFlowController} as released in Netty 4.2.17.Final. Netty's
+ * connection auto-refill state is private and fixed at construction time, so it cannot be enabled only for the
+ * lifetime of a suspended response by composition or subclassing. Because AHC owns this package-private adaptation,
+ * Netty upgrades must compare it with the corresponding upstream implementation for correctness and security fixes.
+ * An upstream API for changing connection auto-refill at runtime would allow this class to be removed. This class is not thread safe. All methods are invoked on the HTTP/2 connection event loop.