fix: auto-detect HTTP proxy tunneling#5116
Conversation
be4138f to
d6abdf6
Compare
|
Should - setGlobalDispatcher(new EnvHttpProxyAgent())
+ setGlobalDispatcher(new EnvHttpProxyAgent({ proxyTunnel: true })) |
|
Thanks for submitting this! Do we have a sense of when this might make it into to a published branch? The reason I ask is that we're trying to replace request with fetch, but our outbound proxy rejects CONNECT requests for https so this is a blocker... trying to decide whether to revert all of it and go back to request we're in a deployable state, or if it's reasonable to wait for this change. |
ed8fadc to
b6a6e97
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #5116 +/- ##
==========================================
- Coverage 93.22% 93.20% -0.03%
==========================================
Files 110 110
Lines 36603 36653 +50
==========================================
+ Hits 34123 34162 +39
- Misses 2480 2491 +11 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
I investigated the remaining CI failures after the rebase. All regular undici jobs are passing. The only failures left are the What is happening there:
I checked
That last file is especially telling because it currently expects:
So the failing shared-builtin jobs are not showing an undici regression in the main test matrix; they are showing that Node's fetch-side proxy tests need to be updated to match the new behavior. I think the follow-up should be a |
| } | ||
|
|
||
| function shouldProxyTunnel (proxyProtocol, requestProtocol, proxyTunnel) { | ||
| return proxyTunnel === true || proxyProtocol !== 'http:' || requestProtocol !== 'http:' |
There was a problem hiding this comment.
I think the rules mostly depend on request protocol, not the proxy protocol? In the case of "http request over https proxy", 9112 3.2.2 says
When making a request to a proxy, other than a CONNECT or server-wide OPTIONS request (as detailed below), a client MUST send the target URI in "absolute-form" as the request-target.
(Basically the same as 7230 5.3.2) so e.g. a GET HTTP request over HTTPS proxy should be sent as
- TLS handshake to proxy server
- Sent the rewritten HTTP request directly to the proxy server
And according to 9110 9.3.6
Tunnels are commonly used to create an end-to-end virtual connection, through one or more proxies, which can then be secured using TLS (Transport Layer Security)
A HTTPS request over HTTP proxy should be sent as
- Establish tunnel to proxy server (without TLS handshake to the proxy server)
- TLS handshake to the target server through the tunnel
- Sent the original HTTP request to the target server.
This is also what the builtin https/http.request implements.
(Note: I don't think socks5 count as a proxy defined in https://datatracker.ietf.org/doc/html/rfc9110, and socks5 is always effectively tunneled because the stuff in https://datatracker.ietf.org/doc/html/rfc1928 sits in a lower layer)
|
The failing Node.js tests have this comment exactly for what this is fixing:
(which just means the tests are only documenting what undici currently does, and needs to be updated once undici fixes it, so failures here is to be expected). |
Signed-off-by: Matteo Collina <hello@matteocollina.com>
Signed-off-by: Matteo Collina <hello@matteocollina.com>
Tunnel decision now depends on the request protocol only. HTTP requests through an HTTPS proxy use absolute-form request-target over TLS to the proxy instead of CONNECT, matching Node's built-in http.request and RFC 9112. HTTPS requests still tunnel via CONNECT. Http1ProxyWrapper pins the proxy SNI so the inner Client does not derive it from the rewritten Host header, and wraps ERR_TLS_CERT_ALTNAME_INVALID into SecureProxyConnectionError for parity with the tunneling path. Signed-off-by: Matteo Collina <hello@matteocollina.com>
b6a6e97 to
ae8fbdd
Compare
The forwarding fix from #5116 is in undici main but the embedded undici in nodejs/node still expects the old semantics, so the shared-builtin build fails until Node.js 26 ships a release embedding the updated undici. Comment is left in place to re-enable for Node.js 26 at that point. Node.js 24 stays off as the change is not being backported. Signed-off-by: Matteo Collina <hello@matteocollina.com>
This relates to...
Fixes #5093
Rationale
EnvHttpProxyAgentcurrently inheritsProxyAgent's tunneling behavior. For plain HTTP targets reached through an HTTP proxy, defaulting to CONNECT can break proxies that do not implement tunneling and can lead to the looping behavior reported in #5093.Per RFC 9112 §3.2.2, the tunnel-vs-forward decision depends only on the request protocol, not the proxy protocol. This matches Node's built-in
http.request/https.request.Behavior
proxyTunnel: truestill forces CONNECT for plain HTTP requests.Changes
shouldProxyTunnelkeys on the request protocol onlyHttp1ProxyWrapperaccepts aproxyServernameto pin SNI to the proxy (otherwise the innerClientderives it from the rewritten Host header, which points at the target)ERR_TLS_CERT_ALTNAME_INVALIDasSecureProxyConnectionError, matching the tunneling pathEnvHttpProxyAgentusinghttp_proxy(Use of HTTP_PROXY & NODE_USE_ENV_PROXY fails with an infinite loop #5093) and for the new HTTPS-proxy-to-HTTP-target forwarding caseBug Fixes
EnvHttpProxyAgentcase behind Use of HTTP_PROXY & NODE_USE_ENV_PROXY fails with an infinite loop #5093Breaking Changes and Deprecations
HTTP request through an HTTPS proxy now forwards instead of tunneling by default. Matches Node's built-in behavior and the RFC. Pass
proxyTunnel: trueto restore the previous behavior.Status