Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
import io.netty.handler.proxy.ProxyHandler;
import io.netty.handler.proxy.Socks4ProxyHandler;
import io.netty.handler.proxy.Socks5ProxyHandler;
import io.netty.handler.ssl.ApplicationProtocolNames;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
import io.netty.handler.timeout.IdleStateHandler;
Expand Down Expand Up @@ -121,6 +122,7 @@ public class ChannelManager {
public static final String HTTP2_FRAME_CODEC = "http2-frame-codec";
public static final String HTTP2_MULTIPLEX = "http2-multiplex";
public static final String AHC_HTTP2_HANDLER = "ahc-http2";
private static final String TARGET_SSL_HANDLER = "target-ssl";
private static final Logger LOGGER = LoggerFactory.getLogger(ChannelManager.class);
// Guards the one-time WARN emitted when a native transport was requested but is unavailable and we
// fall back to NIO. Logged once per JVM to avoid spamming logs when many clients are created.
Expand Down Expand Up @@ -687,13 +689,13 @@ public Future<Channel> updatePipelineForHttpsTunneling(ChannelPipeline pipeline,
// This creates a nested SSL setup: Target SSL -> Proxy SSL -> Network
if (isSslHandlerConfigured(pipeline)) {
// Insert target SSL handler after the proxy SSL handler
pipeline.addAfter(SSL_HANDLER, "target-ssl", sslHandler);
pipeline.addAfter(SSL_HANDLER, TARGET_SSL_HANDLER, sslHandler);
} else {
// This shouldn't happen for HTTPS proxy, but fallback
pipeline.addBefore(INFLATER_HANDLER, SSL_HANDLER, sslHandler);
}

pipeline.addAfter("target-ssl", HTTP_CLIENT_CODEC, newHttpClientCodec());
pipeline.addAfter(TARGET_SSL_HANDLER, HTTP_CLIENT_CODEC, newHttpClientCodec());

} else {
// For HTTPS proxy to HTTP target, just add HTTP codec
Expand Down Expand Up @@ -965,6 +967,23 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception
}
}

/**
* Upgrades and registers a proxy tunnel when the target TLS handshake negotiated HTTP/2.
* The caller controls when this runs so the upgrade can happen immediately before the
* tunneled request is sent.
*/
public void upgradePipelineToHttp2AfterProxyConnect(ChannelPipeline pipeline, Object partitionKey) {
SslHandler targetSslHandler = (SslHandler) pipeline.get(TARGET_SSL_HANDLER);
if (targetSslHandler == null) {
targetSslHandler = (SslHandler) pipeline.get(SSL_HANDLER);
}
if (targetSslHandler != null
&& ApplicationProtocolNames.HTTP_2.equals(targetSslHandler.applicationProtocol())) {
upgradePipelineToHttp2(pipeline);
registerHttp2Connection(partitionKey, pipeline.channel());
}
}

public void upgradePipelineForWebSockets(ChannelPipeline pipeline) {
pipeline.addAfter(HTTP_CLIENT_CODEC, WS_ENCODER_HANDLER, new WebSocket08FrameEncoder(true));
pipeline.addAfter(WS_ENCODER_HANDLER, WS_DECODER_HANDLER, new WebSocket08FrameDecoder(false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1289,6 +1289,10 @@ public void drainChannelAndExecuteNextRequest(final Channel channel, final Netty
public void call() {
whenHandshaked.addListener(f -> {
if (f.isSuccess()) {
if (!nextRequest.getUri().isWebSocket()) {
channelManager.upgradePipelineToHttp2AfterProxyConnect(
channel.pipeline(), future.getPartitionKey());
}
sendNextRequest(nextRequest, future);
} else {
future.abort(f.cause());
Expand Down
54 changes: 54 additions & 0 deletions client/src/test/java/org/asynchttpclient/BasicHttp2Test.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,12 @@
import io.netty.pkitesting.CertificateBuilder;
import io.netty.pkitesting.X509Bundle;
import io.netty.util.concurrent.GlobalEventExecutor;
import org.asynchttpclient.proxy.ProxyServer;
import org.asynchttpclient.proxy.ProxyType;
import org.asynchttpclient.test.EventCollectingHandler;
import org.eclipse.jetty.proxy.ConnectHandler;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -80,7 +85,10 @@
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.asynchttpclient.Dsl.asyncHttpClient;
import static org.asynchttpclient.Dsl.config;
import static org.asynchttpclient.Dsl.proxyServer;
import static org.asynchttpclient.test.TestUtils.AsyncCompletionHandlerAdapter;
import static org.asynchttpclient.test.TestUtils.addHttpConnector;
import static org.asynchttpclient.test.TestUtils.addHttpsConnector;
import static org.asynchttpclient.util.DateUtils.unpreciseMillisTime;
import static org.asynchttpclient.util.ThrowableUtil.unknownStackTrace;
import static org.junit.jupiter.api.Assertions.*;
Expand Down Expand Up @@ -430,6 +438,52 @@ private AsyncHttpClient http2ClientWithConfig(Consumer<DefaultAsyncHttpClientCon
return asyncHttpClient(builder);
}

@Test
public void httpsRequestsThroughHttpProxyNegotiateAndReuseHttp2AfterConnect() throws Exception {
assertHttpsRequestsThroughProxyNegotiateAndReuseHttp2AfterConnect(false);
}

@Test
public void httpsRequestsThroughHttpsProxyNegotiateAndReuseHttp2AfterConnect() throws Exception {
assertHttpsRequestsThroughProxyNegotiateAndReuseHttp2AfterConnect(true);
}

private void assertHttpsRequestsThroughProxyNegotiateAndReuseHttp2AfterConnect(boolean secureProxy) throws Exception {
Server proxy = new Server();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

proxy.start() is outside the try/finally. Nothing between here and the try throws today, so it is fine, but moving the start inside the try (or the Server proxy creation into a try-with-resources style teardown) would make the cleanup airtight if setup ever grows.

@rampreeth rampreeth Jul 19, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done -proxy.start() is now inside the try, so proxy.stop() still runs from finally if setup fails.

ServerConnector proxyConnector = secureProxy ? addHttpsConnector(proxy) : addHttpConnector(proxy);
proxy.setHandler(new ConnectHandler());

try {
proxy.start();
try (AsyncHttpClient client = http2Client()) {
ProxyServer.Builder proxyBuilder = proxyServer("127.0.0.1", proxyConnector.getLocalPort());
if (secureProxy) {
proxyBuilder.setProxyType(ProxyType.HTTPS);
}
ProxyServer proxyServer = proxyBuilder.build();
Response firstResponse = client.prepareGet(httpsUrl("/hello"))
.setProxyServer(proxyServer)
.execute()
.get(30, SECONDS);
Response secondResponse = client.prepareGet(httpsUrl("/hello"))
.setProxyServer(proxyServer)
.execute()
.get(30, SECONDS);

assertNotNull(firstResponse);
assertEquals(200, firstResponse.getStatusCode());
assertEquals(HttpProtocol.HTTP_2, firstResponse.getProtocol());
assertNotNull(secondResponse);
assertEquals(200, secondResponse.getStatusCode());
assertEquals(HttpProtocol.HTTP_2, secondResponse.getProtocol());
assertEquals(1, serverChildChannels.size(),
"the HTTP/2 tunnel connection should be registered and reused");
}
} finally {
proxy.stop();
}
}

/**
* With {@link LoadBalance#ROUND_ROBIN}, a host resolving to several IPs gets one HTTP/2
* connection per IP (the registry is keyed by the IP-aware partition key), so requests are spread
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,6 @@ public void testHttpProxyToHttpsTarget() throws Exception {
.setProxyType(ProxyType.HTTP)
.build())
.setUseInsecureTrustManager(true)
// HTTP/2 ALPN upgrade after proxy CONNECT tunnel is not yet supported
.setHttp2Enabled(false)
.setConnectTimeout(Duration.ofMillis(10000))
.setRequestTimeout(Duration.ofMillis(30000))
.build();
Expand All @@ -169,8 +167,6 @@ public void testHttpsProxyToHttpsTarget() throws Exception {
.setProxyType(ProxyType.HTTPS)
.build())
.setUseInsecureTrustManager(true)
// HTTP/2 ALPN upgrade after proxy CONNECT tunnel is not yet supported
.setHttp2Enabled(false)
.setConnectTimeout(Duration.ofMillis(10000))
.setRequestTimeout(Duration.ofMillis(30000))
.build();
Expand Down
Loading