Repository navigation
Keep the hop to a proxy on HTTP/1.1 - #2369
Merged
Merged
Conversation
Rebased onto main by Claude Code on behalf of Matthias Kurz. main already has the addSslHandler overload that takes http2Allowed, so the conflict in ChannelManager was resolved by keeping main's code: of this commit's ChannelManager change, only the Javadoc is left. (cherry picked from commit 04f167c) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rebased onto main by Claude Code on behalf of Matthias Kurz. The conflict in NettyConnectListener was resolved by joining the check with main's handling of connections that a login authenticates, and the test imports Http2Headers, since main rejects fully qualified names. (cherry picked from commit 0b4f999) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Now that h2c is never spoken to an HTTP proxy, a cleartext origin behind one can never register an HTTP/2 connection, just as behind an HTTPS proxy. A request refused a connection permit still armed a waiter for one, so it was held for connectTimeout and then failed with the permit exception it already had. Fail it at once, as AsyncHttpClient#2334 does when h2c is disabled. The comment there said this rests on every hop to a cleartext origin being cleartext, which the hop to an HTTPS proxy is not. What it rests on is that no proxy is ever spoken to over HTTP/2. Claude Code on behalf of Matthias Kurz Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The stub proxy recorded the first line and hung up, so the test showed what was sent, but not that the request still works. Answer with an HTTP/1.1 response and check that the request completes with it. The test no longer waits out its 10 second timeout either. Claude Code on behalf of Matthias Kurz Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The TLS test proxy's Javadoc said that it checks the HTTP/2 connection preface, but it drops the connection whatever arrived, which is all the test needs. Also replace the non-ASCII punctuation in both tests, since AGENTS.md asks for ASCII only. Claude Code on behalf of Matthias Kurz Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hyperxpro
requested changes
Nov 5, 2026
When the TLS connection to an HTTPS proxy still negotiates h2, as an engine from a user-supplied SslContext can, the proxy rejects the HTTP/1.1 bytes written next. This was only logged: the request then repeated the handshake once per retry and failed with a bare "Remotely closed". Fail the connect at once instead, with an exception that names the cause and writes nothing to the proxy. It is not retried, also not after an earlier retry or a closed pooled connection, where onFailure() would retry other failures: a new connection would select h2 again. The hint points to an SslEngineFactory, since an SslContext that offers only http/1.1 would also turn off h2 to the origins. Claude Code on behalf of Matthias Kurz Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The waiter tests only covered a cleartext origin behind an HTTP proxy: dropping the SOCKS exemption, or letting HTTPS proxies through, still passed all of them. Add an over-cap h2c request through SOCKS, which must wait for the first connection's HTTP/2 and then complete on it, and an http target behind an HTTPS proxy, which must be refused at once. Each fails with the corresponding change to the check. Claude Code on behalf of Matthias Kurz Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SslEngineFactory and DefaultSslEngineFactory still described http2Allowed = false as WebSocket only. List all three cases there: a WebSocket, the connection to an HTTPS proxy, and one that a login authenticates, that is an NTLM, Kerberos or SPNEGO login, or any proxy login on a CONNECT tunnel or through SOCKS. A user-supplied SslContext is reused because it is used as-is for every connection, not because it is known to negotiate http/1.1. The h2c comments now say that prior knowledge in RFC 9113 is about the immediate peer, and that it is http2CleartextEnabled that limits it to origins. The test comments now match tinyproxy's versions and what the h2c stub echoes. Claude Code on behalf of Matthias Kurz Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
5 tasks done
hyperxpro
approved these changes
Nov 6, 2026
hyperxpro
pushed a commit
that referenced
this pull request
Nov 6, 2026
## Summary - Upgrades Netty from 4.2.19.Final to 4.2.20.Final. Its [security fixes](https://github.com/netty/netty/releases/tag/netty-4.2.20.Final) include several in modules AHC uses: three out-of-bounds reads in `netty-buffer`, unbounded memory usage in `netty-codec-http2`, insufficient control character neutralization in `netty-codec-http`, insufficient entropy in `netty-resolver-dns`, and an mTLS bypass via session resumption in `netty-handler`. - On 4.2.20, two of AHC's tests fail, both because of Netty changes. This PR adapts AHC to them. ## Change - **Upgrade Netty to 4.2.20.Final, and check the second gzip magic byte:** `JdkZlibDecoder` now rejects a gzip header with a wrong second magic byte (netty/netty#17972). The gzip inflate loop from #2357 follows `JdkZlibDecoder`, and `Http1ContentDecompressorTest` compares the two, so it now checks that byte too. Both bytes are read unsigned, since `0x8b` is negative as a signed byte. - **Keep zstd working at a non-positive threshold:** since netty/netty#17896, the `ZstdDecoder` that `HttpContentDecompressor` creates sizes its output buffers by `io.netty.compression.defaultMaxForwardBytes`. At 0 it hands on an empty body without an error; below 0 it fails with `IllegalArgumentException: initialCapacity : -1`. AHC leaves zstd to that decoder while the property is not positive (#2362), so it now replaces it with a `ZstdDecoder` whose buffers hold `MAX_PART_SIZE`. The size limit still applies, and the body is still decompressed eagerly there, as before. At the default value of the property nothing changes. netty/netty#18289 fixes the decoder in Netty; this workaround is harmless with that fix and can go once AHC requires a Netty version that has it. ## Notes - Netty 4.2.0 to 4.2.4 have no `ZstdDecoder(int)` constructor. Overriding AHC's Netty with one of those and also setting the property to 0 or below would fail zstd responses with a `NoSuchMethodError`. ## AI disclosure Claude Code on behalf of Matthias Kurz. Both commits include `Co-Authored-By` per `AGENTS.md`. ## Test plan - [x] `main` at `5542c9fc4` with `-Dnetty.version=4.2.20.Final`: 1,857 tests, 2 failures: - `Http1ContentDecompressorTest.decodesLikeNettyForEveryBodyAndSplit`, case "gzip second magic byte ignored": Netty now fails that body, AHC did not. - `Http1ContentDecompressorForwardThresholdTest`: "the zstd body differs". - [x] `Http1ContentDecompressorForwardThresholdTest` now runs at thresholds 0 and -1, each in a fresh JVM, and also checks the size limit for zstd and that every exchange releases its input and buffers. On 4.2.20 without the zstd commit it fails at both ("the zstd body differs", "the zstd body did not end"). With it, it passes on 4.2.19 and 4.2.20. - [x] `./mvnw -B -ntp -Dgpg.skip=true clean verify` on JDK 11, on `main` `3e95ec8c2`: 1,905 tests, 0 failures, 0 errors, 11 skipped; Revapi passed. No test-skipping flags were used. - [x] `Http1ContentDecompressorForwardThresholdTest` and `Http1ContentDecompressorTest` compiled on JDK 11 and run on JDK 17, 21 and 25 (Maven Surefire's `-Djvm`): 36 tests each, 0 failures. - [x] This branch merged with #2369: `clean verify` on JDK 11, 1,914 tests, 0 failures, 0 errors, 11 skipped; Revapi passed (run before #2359, #2360 and #2361 were merged, with those three merged in as well). --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
fix/proxy-hop-alpnbranch #2358. This is thefix/proxy-hop-alpnbranch rebased ontomain, with its two commits and their authorship kept, plus six commits of mine, three of them after review.h2in ALPN, and h2c prior knowledge is no longer sent to an HTTP proxy. Both connections only ever carry HTTP/1.1.Problem
HTTPSproxy derivedhttp2Allowedfrom the proxy's URI, so with HTTP/2 enabled it offeredh2. A proxy that acceptsh2then receives the HTTP/1.1CONNECT, or the absolute-URI request, and drops the connection (RemotelyClosedException). Proxies that don't accepth2are not affected.http2CleartextEnabled, anhttp://target behind an HTTP proxy got the HTTP/2 connection preface (PRI * HTTP/2.0) sent to the proxy, which never agreed to HTTP/2. SOCKS is not affected, since there the peer is the origin.Both are reproduced by the tests below on
main, against test proxies (an h2-capable TLSCONNECTproxy and a stub HTTP proxy) rather than a real proxy product.Change
04f167c, rebased: the HTTPS proxy connection passeshttp2Allowed = false, soDefaultSslEngineFactoryuses its http/1.1-only context. If the connection still negotiatesh2, which a user-supplied context can, this commit logged a warning; the request now fails with the reason instead (see below).mainalready has the 5-argumentaddSslHandler(Keep connection-authenticated sockets off the HTTP/2 registry #2342), so of theChannelManagerchange only the Javadoc is left.0b4f999, rebased: h2c is only used when the socket's peer is the origin, that is without a proxy or through SOCKS. The check joinsmain's handling of connections a login authenticates (Keep connection-authenticated sockets off the HTTP/2 registry #2342), so its "instead of h2c" warning is only logged when h2c would otherwise be used. The test importsHttp2Headers, sincemainrejects fully qualified names (Reject fully qualified names and wildcard imports #2338).main, the check only looked athttp2CleartextEnabled, so with it enabled such a request waited outconnectTimeoutbehind either proxy type.After review:
SslContextcan, the connect now fails at once, before anything is written to the proxy, with an exception that names the cause. Before, this was only logged, and the request repeated the handshake once per retry and then failed with a bareRemotely closed. The failure is not retried in any state, also not after an earlier retry or a closed pooled connection, since a new connection would select h2 again.httptarget behind an HTTPS proxy must be refused at once.SslEngineFactoryandDefaultSslEngineFactorynow list all three cases ofhttp2Allowed = false: WebSocket, the connection to an HTTPS proxy, and a connection that a login authenticates (NTLM, Kerberos or SPNEGO, or any proxy login on aCONNECTtunnel or through SOCKS). The h2c comments say that prior knowledge in RFC 9113 is about the immediate peer, and that it ishttp2CleartextEnabledthat limits it to origins. Test comments corrected.Unchanged:
CONNECTtunnel still negotiatesh2, also behind an HTTPS proxy;Not in scope
A user-supplied
SslContextis used as-is for every connection, so if it offersh2, the proxy connection can still negotiate it, as awss://connection already can. The request then fails with the reason (see above). To keep h2 to origins, a customSslEngineFactorycan offer only http/1.1 when it receiveshttp2Allowed = false, which it does for the proxy connection.Release note
With
http2CleartextEnabled, requests through an HTTP forward proxy now use HTTP/1.1 rather than h2c, including when that proxy supports h2c.AI disclosure
Claude Code on behalf of Matthias Kurz. The two rebased commits keep their original authorship; Claude Code did the rebase and the conflict resolution, which their messages describe. All eight commits include
Co-Authored-ByperAGENTS.md.Test plan
main, pass with this PR:ProxyTunnelHttp2Test.httpsProxyHopMustNotNegotiateHttp2: onmainthe proxy negotiatesh2, thenRemotely closed. With the PR, it also checks that the origin behind the HTTPS proxy still gets HTTP/2.ProxyH2cTest.h2cIsNotSpokenToAnHttpProxy: onmainthe proxy receivesPRI * HTTP/2.0.Http2ConnectionWaiterGateTest.permitExhaustedOnCleartextOriginBehindAnHttpProxyFailsWithoutWaitingOutConnectTimeoutand...BehindAnHttpsProxy...: fail without my first commit, with the refused request still pending.ProxyTunnelHttp2Test.httpsProxyHopThatSelectsHttp2FailsTheConnect: before the fail-fast commit, the request failed withRemotelyClosedException. Checks the exception, that the proxy receives no bytes after selecting h2, one handshake per request, and that the connection permit is returned.ProxyHopAlpnConnectListenerTest: drives the connect listener with a proxy connection that selected h2, in theNEW,RECONNECTEDandPOOLEDstates. With the failure going throughonFailure(), the latter two were retried. Checks that nothing is retried, the exception, that the channel is closed, and that the permit is returned once.isSocks()exemption,ProxyH2cTest.overCapH2cRequestThroughASocksProxyWaitsForTheFirstConnectionfails; with HTTPS proxies let through, the HTTPS-proxy waiter test fails.ProxyTunnelHttp2Testtests (HTTP/2 to the origin through aCONNECTproxy, tunnel reuse, concurrent requests, SOCKS,maxConnectionsPerHost) andProxyH2cTest.h2cIsSpokenThroughASocksProxy../mvnw -B -ntp -Dgpg.skip=true clean verifyon JDK 11, merged withmain3e95ec8c2: 1,919 tests, 0 failures, 0 errors, 11 skipped; Revapi passed. No test-skipping flags were used.ProxyTunnelHttp2Test,ProxyH2cTest,Http2ConnectionWaiterGateTest,ConnectionOrientedAuthConnectListenerTestandProxyHopAlpnConnectListenerTestcompiled on JDK 11 and run on JDK 17, 21 and 25 (Maven Surefire's-Djvm): 21 tests each, 0 failures.