Skip to content

Support HTTP/2 after proxy CONNECT#2250

Open
rampreeth wants to merge 3 commits into
AsyncHttpClient:mainfrom
rampreeth:codex/http2-after-proxy-connect
Open

Support HTTP/2 after proxy CONNECT#2250
rampreeth wants to merge 3 commits into
AsyncHttpClient:mainfrom
rampreeth:codex/http2-after-proxy-connect

Conversation

@rampreeth

@rampreeth rampreeth commented Jul 18, 2026

Copy link
Copy Markdown

Motivation

When an HTTPS request uses an HTTP proxy, AHC establishes a CONNECT tunnel and then performs TLS with the target. If ALPN selects h2, the tunnel path currently leaves the HTTP/1.1 codec in place. The target then receives HTTP/1.1 bytes on a connection that negotiated HTTP/2 and closes it.

Modification

  • Check the negotiated ALPN protocol after the target TLS handshake in both proxy tunnel paths.
  • When ALPN selects h2, upgrade the existing channel pipeline to HTTP/2 and register the physical tunnel connection with its existing pool partition key.
  • Preserve the existing HTTP/1.1 fallback and WebSocket behavior.
  • Add regression tests using local HTTP and HTTPS CONNECT proxies with a TLS HTTP/2 target.
  • Verify that the upgraded tunnel is registered and reused for a second request.
  • Preserve the existing public tunneling method signatures for binary compatibility.

Result

HTTPS requests through HTTP and HTTPS CONNECT proxies now send HTTP/2 frames when the target negotiates h2. The regression tests verify successful HTTP/2 responses and reuse of a single physical target connection.

Testing

  • ./mvnw -pl client -Dtest=BasicHttp2Test,HttpsProxyTest test — 60 tests passed.
  • Full client test suite passed as part of verify; the local run then stopped at artifact signing because gpg is not installed.
  • ./mvnw -pl client -DskipTests -Dgpg.skip=true verify — packaging and API compatibility checks passed.

Fixes #2241

@rampreeth
rampreeth marked this pull request as ready for review July 18, 2026 12:16

@hyperxpro hyperxpro left a comment

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.

Not in this PR's diff, so leaving it here: this change makes HTTP/2 after CONNECT work, which makes the workaround in client/src/test/java/org/asynchttpclient/proxy/HttpsProxyTestcontainersIntegrationTest.java stale and its comment misleading. Could you remove .setHttp2Enabled(false) and the // HTTP/2 ALPN upgrade after proxy CONNECT tunnel is not yet supported comment in both testHttpProxyToHttpsTarget (lines 149-150) and testHttpsProxyToHttpsTarget (lines 172-173)? That lets those external tests exercise the new HTTP/2-through-proxy path instead of forcing HTTP/1.1, and the comment stops claiming something that is no longer true.

return;
}

whenHandshaked.addListener(f -> {

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.

This upgrade has to run before the sendNextRequest listener that NettyRequestSender.drainChannelAndExecuteNextRequest adds to the same whenHandshaked future, otherwise writeRequest would see the pipeline still on HTTP/1.1 and send HTTP/1.1. It works today because this listener is registered first (during CONNECT handling) and the drain listener is registered later, when the CONNECT last-content is read, so Netty fires them in that order. That is a real correctness invariant, but it lives across two files and is easy to break by accident. Could we either add a short comment here spelling it out, or move the ALPN check and upgrade into the same listener that calls sendNextRequest so the ordering is guaranteed locally?

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.

Good catch. I moved the ALPN check and pipeline upgrade into the same handshake listener that sends the next request, so the ordering is local now.

return updatePipelineForHttpTunneling(pipeline, requestUri, null);
}

public Future<Channel> updatePipelineForHttpTunneling(ChannelPipeline pipeline, Uri requestUri, Object partitionKey) {

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.

Small design note: these methods were pure pipeline setup, and now they also schedule a post-handshake side effect. Passing partitionKey in just so it can be handed to registerHttp2Connection inside the listener works, but it does blur the responsibility a bit. Keeping the overloads for binary compatibility is a good call though.

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.

Agreed. I removed the new overloads and kept the pipeline setup methods focused on setup. The post-handshake upgrade now happens explicitly from NettyRequestSender, which already has the partition key.

ChannelPipeline pipeline,
Uri requestUri,
Object partitionKey) {
if (!config.isHttp2Enabled() || requestUri.isWebSocket()) {

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.

Minor: the config.isHttp2Enabled() half is belt and suspenders, since with HTTP/2 disabled the SSL engine never advertises h2 and applicationProtocol() will not equal h2. Harmless to keep, just noting it is not strictly required. The WebSocket guard is worth keeping.

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.

Yep, removed the redundant config check. We now rely on the negotiated ALPN result, with the WebSocket guard still in NettyRequestSender.

if (f.isSuccess() && ApplicationProtocolNames.HTTP_2.equals(sslHandler.applicationProtocol())) {
upgradePipelineToHttp2(pipeline);
if (partitionKey != null) {
registerHttp2Connection(partitionKey, pipeline.channel());

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.

future.getPartitionKey() is effectively never null here, so the partitionKey != null guard is defensive only. Fine to keep, just flagging that if it ever were null the tunnel would upgrade but never get registered for reuse, and that would be silent.

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.

Removed the null guard. Registration now always uses future.getPartitionKey(), so a bad null cannot silently leave an upgraded connection unregistered.

}

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.

@rampreeth

rampreeth commented Jul 19, 2026

Copy link
Copy Markdown
Author

Done - removed .setHttp2Enabled(false) and the stale comment from both proxy-to-HTTPS Testcontainers tests. They now use the default HTTP/2-enabled configuration.

@rampreeth
rampreeth requested a review from hyperxpro July 19, 2026 02:47
@rampreeth

Copy link
Copy Markdown
Author

Hi @hyperxpro, just a gentle reminder when you get a chance. I've addressed the review comments and would appreciate a re-review. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

HTTP/2 can be negotiated after proxy CONNECT, but AHC still sends HTTP/1.1

2 participants