From e121d8c8d3a55f44d184b97b625a7bb9f2ca9e18 Mon Sep 17 00:00:00 2001 From: Pavel Ptashyts <49400901+pavel-ptashyts@users.noreply.github.com> Date: Mon, 3 Aug 2026 15:26:14 +0200 Subject: [PATCH 1/2] Read only when the channel does not auto-read AsyncHttpClientHandler requested a read from both channelActive and channelReadComplete. Netty's HeadContext already calls Channel#read() right after firing either event whenever autoRead is on, so every read cycle traversed the outbound pipeline and reached doBeginRead twice instead of once. AsyncHttpClient never clears autoRead and Netty defaults it to on, so that was the normal path for every connection, and the waste grew with the number of read cycles a response took. Drive the read from here only when autoRead is off. That keeps working for a caller who disables autoRead through a channel option, which is also a fix in its own right: such a caller previously had the setting silently defeated by these two unconditional reads. Verified against netty 4.2.16.Final rather than assumed. HeadContext calls readIfIsAutoRead() from both channelActive and channelReadComplete, DefaultChannelConfig initialises autoRead to on, and HTTP/2 stream channels share both behaviours: their pipeline extends DefaultChannelPipeline and Http2StreamChannelConfig extends DefaultChannelConfig without overriding isAutoRead. The change therefore holds for all three subclasses, HTTP/1.1, WebSocket and HTTP/2. Claude Code on behalf of Pavel Ptashyts Co-Authored-By: Claude Opus 5 --- .../netty/handler/AsyncHttpClientHandler.java | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/AsyncHttpClientHandler.java b/client/src/main/java/org/asynchttpclient/netty/handler/AsyncHttpClientHandler.java index aeecbef553..93a5f02e7e 100755 --- a/client/src/main/java/org/asynchttpclient/netty/handler/AsyncHttpClientHandler.java +++ b/client/src/main/java/org/asynchttpclient/netty/handler/AsyncHttpClientHandler.java @@ -173,12 +173,26 @@ public void exceptionCaught(ChannelHandlerContext ctx, Throwable e) { @Override public void channelActive(ChannelHandlerContext ctx) { - ctx.read(); + readIfNotAutoRead(ctx); } @Override public void channelReadComplete(ChannelHandlerContext ctx) { - ctx.read(); + readIfNotAutoRead(ctx); + } + + /** + * Requests the next read only when the channel will not do it by itself. Netty's HeadContext already + * calls Channel#read() after firing channelActive and channelReadComplete whenever autoRead is on, so + * reading here as well only repeated the outbound pipeline traversal and doBeginRead. AsyncHttpClient + * never clears autoRead, which defaults to on, so this is the usual path; a caller that turns it off + * through a channel option still needs reads to be driven from here, which is why the call is kept + * rather than dropped. + */ + private static void readIfNotAutoRead(ChannelHandlerContext ctx) { + if (!ctx.channel().config().isAutoRead()) { + ctx.read(); + } } void finishUpdate(NettyResponseFuture future, Channel channel, boolean close) { From 1dff61f8985512ccdcbca8d436e63cdcb5133b03 Mon Sep 17 00:00:00 2001 From: Pavel Ptashyts <49400901+pavel-ptashyts@users.noreply.github.com> Date: Tue, 4 Aug 2026 08:23:09 +0200 Subject: [PATCH 2/2] Name the read guard after Netty's Review feedback on #2302. Netty spells this guard readIfNeeded in SslHandler and inlines the same test in Http2ConnectionHandler.channelReadComplete0, so take that name and point at both. It makes clear the shape is Netty's rather than something invented here. Drop the sentence stating that AsyncHttpClient never clears autoRead. It is a survey of current usage rather than of the mechanism, and it goes stale the day a config grows an AUTO_READ option. Claude Code on behalf of Pavel Ptashyts Co-Authored-By: Claude Opus 5 --- .../netty/handler/AsyncHttpClientHandler.java | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/AsyncHttpClientHandler.java b/client/src/main/java/org/asynchttpclient/netty/handler/AsyncHttpClientHandler.java index 93a5f02e7e..62b8cb5e56 100755 --- a/client/src/main/java/org/asynchttpclient/netty/handler/AsyncHttpClientHandler.java +++ b/client/src/main/java/org/asynchttpclient/netty/handler/AsyncHttpClientHandler.java @@ -173,23 +173,22 @@ public void exceptionCaught(ChannelHandlerContext ctx, Throwable e) { @Override public void channelActive(ChannelHandlerContext ctx) { - readIfNotAutoRead(ctx); + readIfNeeded(ctx); } @Override public void channelReadComplete(ChannelHandlerContext ctx) { - readIfNotAutoRead(ctx); + readIfNeeded(ctx); } /** - * Requests the next read only when the channel will not do it by itself. Netty's HeadContext already - * calls Channel#read() after firing channelActive and channelReadComplete whenever autoRead is on, so - * reading here as well only repeated the outbound pipeline traversal and doBeginRead. AsyncHttpClient - * never clears autoRead, which defaults to on, so this is the usual path; a caller that turns it off - * through a channel option still needs reads to be driven from here, which is why the call is kept - * rather than dropped. + * Requests the next read only when the channel will not do it by itself: Netty's {@code HeadContext} + * calls {@code Channel#read()} once channelActive and channelReadComplete have been fired whenever + * autoRead is on, so reading here as well would just repeat the outbound traversal and doBeginRead. + * Same guard as {@code SslHandler#readIfNeeded}, which {@code Http2ConnectionHandler} + * inlines in {@code channelReadComplete0}. */ - private static void readIfNotAutoRead(ChannelHandlerContext ctx) { + private static void readIfNeeded(ChannelHandlerContext ctx) { if (!ctx.channel().config().isAutoRead()) { ctx.read(); }