Skip to content

Commit 20d97d0

Browse files
committed
lib: add NODE_PROXY_TUNNEL env var to disable CONNECT tunneling
When NODE_USE_ENV_PROXY or --use-env-proxy is enabled and the proxy does not support the HTTP CONNECT method, undici loops forever retrying CONNECT requests. Add NODE_PROXY_TUNNEL env var to control whether proxy connections use CONNECT tunneling. When set to false/0, EnvHttpProxyAgent passes proxyTunnel: false to ProxyAgent, which uses Http1ProxyWrapper for direct HTTP forwarding instead of CONNECT tunneling. Refs: nodejs/undici#5093
1 parent cee146f commit 20d97d0

4 files changed

Lines changed: 107 additions & 2 deletions

File tree

doc/api/cli.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3978,11 +3978,31 @@ added:
39783978
39793979
When enabled, Node.js parses the `HTTP_PROXY`, `HTTPS_PROXY` and `NO_PROXY`
39803980
environment variables during startup, and tunnels requests over the
3981-
specified proxy.
3981+
specified proxy. Use [`NODE_PROXY_TUNNEL`][] to disable CONNECT tunneling
3982+
for proxies that do not support it.
39823983

39833984
This can also be enabled using the [`--use-env-proxy`][] command-line flag.
39843985
When both are set, `--use-env-proxy` takes precedence.
39853986

3987+
### `NODE_PROXY_TUNNEL`
3988+
3989+
<!-- YAML
3990+
added: REPLACEME
3991+
-->
3992+
3993+
> Stability: 1.1 - Active Development
3994+
3995+
When `NODE_USE_ENV_PROXY=1` or `--use-env-proxy` is enabled, controls whether
3996+
proxy connections use the HTTP CONNECT tunneling method.
3997+
3998+
When set to `true` or `1` (the default), undici uses CONNECT tunneling to establish
3999+
a proxy connection. When set to `false` or `0`, undici uses direct HTTP forwarding
4000+
instead, which may be required for proxies that do not support the CONNECT method.
4001+
4002+
Valid values: `true`, `1`, `false`, `0`.
4003+
4004+
See also [`NODE_USE_ENV_PROXY=1`][].
4005+
39864006
### `NODE_USE_SYSTEM_CA=1`
39874007

39884008
<!-- YAML
@@ -4338,6 +4358,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
43384358
[`ERR_INVALID_TYPESCRIPT_SYNTAX`]: errors.md#err_invalid_typescript_syntax
43394359
[`ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX`]: errors.md#err_unsupported_typescript_syntax
43404360
[`NODE_OPTIONS`]: #node_optionsoptions
4361+
[`NODE_PROXY_TUNNEL`]: #node_proxy_tunnel
43414362
[`NODE_USE_ENV_PROXY=1`]: #node_use_env_proxy1
43424363
[`NO_COLOR`]: https://no-color.org
43434364
[`Web Storage`]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API

doc/api/http.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4515,6 +4515,13 @@ Or the `--use-env-proxy` flag.
45154515
HTTP_PROXY=http://proxy.example.com:8080 NO_PROXY=localhost,127.0.0.1 node --use-env-proxy client.js
45164516
```
45174517
4518+
If the proxy does not support the HTTP CONNECT method, set `NODE_PROXY_TUNNEL=false`
4519+
to use direct HTTP forwarding instead:
4520+
4521+
```console
4522+
NODE_USE_ENV_PROXY=1 NODE_PROXY_TUNNEL=false HTTP_PROXY=http://proxy.example.com:8080 node client.js
4523+
```
4524+
45184525
To enable proxy support dynamically and globally with `process.env` (the default option of `http.setGlobalProxyFromEnv()`):
45194526
45204527
```cjs

lib/internal/process/pre_execution.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,12 @@ function setupHttpProxy() {
206206
}
207207

208208
const { setGlobalDispatcher, EnvHttpProxyAgent } = require('internal/deps/undici/undici');
209-
const envHttpProxyAgent = new EnvHttpProxyAgent();
209+
const envProxyTunnel = process.env.NODE_PROXY_TUNNEL;
210+
let proxyTunnel;
211+
if (envProxyTunnel !== undefined) {
212+
proxyTunnel = envProxyTunnel === 'true' || envProxyTunnel === '1';
213+
}
214+
const envHttpProxyAgent = new EnvHttpProxyAgent({ proxyTunnel });
210215
setGlobalDispatcher(envHttpProxyAgent);
211216
// For fetch, we need to set the global dispatcher from here.
212217
// For http/https agents, we'll configure the global agent when they are
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Tests that NODE_PROXY_TUNNEL=false disables CONNECT tunneling
2+
// and uses direct HTTP forwarding instead.
3+
4+
import * as common from '../common/index.mjs';
5+
import assert from 'node:assert';
6+
import http from 'node:http';
7+
import { once } from 'events';
8+
import { createProxyServer, checkProxiedFetch } from '../common/proxy-server.js';
9+
10+
// Start a minimal proxy server.
11+
const { proxy, logs } = createProxyServer();
12+
proxy.listen(0);
13+
await once(proxy, 'listening');
14+
15+
delete process.env.NODE_USE_ENV_PROXY; // Ensure the environment variable is not set.
16+
17+
// Start a HTTP server to process the final request.
18+
const server = http.createServer(common.mustCall((req, res) => {
19+
res.end('Hello world');
20+
}, 2));
21+
server.on('error', common.mustNotCall((err) => { console.error('Server error', err); }));
22+
server.listen(0);
23+
await once(server, 'listening');
24+
25+
const serverHost = `localhost:${server.address().port}`;
26+
const requestUrl = `http://${serverHost}/test`;
27+
28+
// Test: with NODE_PROXY_TUNNEL=false, fetch should NOT use CONNECT tunneling
29+
// but instead use direct HTTP forwarding.
30+
{
31+
await checkProxiedFetch({
32+
FETCH_URL: requestUrl,
33+
HTTP_PROXY: `http://localhost:${proxy.address().port}`,
34+
NODE_USE_ENV_PROXY: '1',
35+
NODE_PROXY_TUNNEL: 'false',
36+
}, {
37+
stdout: 'Hello world',
38+
});
39+
40+
// With proxyTunnel: false, undici uses Http1ProxyWrapper which rewrites
41+
// the request path to include the origin (e.g. "localhost:PORT/test")
42+
// and sends it as a normal GET request instead of CONNECT.
43+
assert.strictEqual(logs[0].method, 'GET');
44+
assert.strictEqual(logs[0].url, requestUrl);
45+
assert.strictEqual(logs[0].headers.host, serverHost);
46+
}
47+
48+
// Test: without NODE_PROXY_TUNNEL (default), fetch still uses CONNECT tunneling.
49+
{
50+
logs.splice(0, logs.length);
51+
await checkProxiedFetch({
52+
FETCH_URL: requestUrl,
53+
HTTP_PROXY: `http://localhost:${proxy.address().port}`,
54+
NODE_USE_ENV_PROXY: '1',
55+
}, {
56+
stdout: 'Hello world',
57+
});
58+
59+
// Without NODE_PROXY_TUNNEL set, CONNECT tunneling is used by default.
60+
assert.deepStrictEqual(logs, [{
61+
method: 'CONNECT',
62+
url: serverHost,
63+
headers: {
64+
'connection': 'close',
65+
'proxy-connection': 'keep-alive',
66+
'host': serverHost,
67+
},
68+
}]);
69+
}
70+
71+
server.close();
72+
proxy.close();

0 commit comments

Comments
 (0)