Skip to content

Commit 4b0007d

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 4b0007d

5 files changed

Lines changed: 117 additions & 2 deletions

File tree

doc/api/cli.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3903,6 +3903,25 @@ added: v7.1.0
39033903
When set to `1`, instructs the module loader to preserve symbolic links when
39043904
resolving and caching modules.
39053905

3906+
### `NODE_PROXY_TUNNEL`
3907+
3908+
<!-- YAML
3909+
added: REPLACEME
3910+
-->
3911+
3912+
> Stability: 1.1 - Active Development
3913+
3914+
When `NODE_USE_ENV_PROXY=1` or `--use-env-proxy` is enabled, controls whether
3915+
proxy connections use the HTTP CONNECT tunneling method.
3916+
3917+
When set to `true` or `1` (the default), undici uses CONNECT tunneling to establish
3918+
a proxy connection. When set to `false` or `0`, undici uses direct HTTP forwarding
3919+
instead, which may be required for proxies that do not support the CONNECT method.
3920+
3921+
Valid values: `true`, `1`, `false`, `0`.
3922+
3923+
See also [`NODE_USE_ENV_PROXY=1`][].
3924+
39063925
### `NODE_REDIRECT_WARNINGS=file`
39073926

39083927
<!-- YAML
@@ -3978,7 +3997,8 @@ added:
39783997
39793998
When enabled, Node.js parses the `HTTP_PROXY`, `HTTPS_PROXY` and `NO_PROXY`
39803999
environment variables during startup, and tunnels requests over the
3981-
specified proxy.
4000+
specified proxy. Use [`NODE_PROXY_TUNNEL`][] to disable CONNECT tunneling
4001+
for proxies that do not support it.
39824002

39834003
This can also be enabled using the [`--use-env-proxy`][] command-line flag.
39844004
When both are set, `--use-env-proxy` takes precedence.
@@ -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

doc/node.1

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2232,6 +2232,16 @@ for connections. This setting applies to Windows only.
22322232
When set to \fB1\fR, instructs the module loader to preserve symbolic links when
22332233
resolving and caching modules.
22342234
.
2235+
.It Ev NODE_PROXY_TUNNEL
2236+
When \fBNODE_USE_ENV_PROXY=1\fR or \fB--use-env-proxy\fR is enabled, controls whether
2237+
proxy connections use the HTTP CONNECT tunneling method.
2238+
2239+
When set to \fBtrue\fR or \fB1\fR (the default), undici uses CONNECT tunneling to establish
2240+
a proxy connection. When set to \fBfalse\fR or \fB0\fR, undici uses direct HTTP forwarding
2241+
instead, which may be required for proxies that do not support the CONNECT method.
2242+
2243+
Valid values: \fBtrue\fR, \fB1\fR, \fBfalse\fR, \fB0\fR.
2244+
.
22352245
.It Ev NODE_REDIRECT_WARNINGS Ar file
22362246
When set, process warnings will be emitted to the given file instead of
22372247
printing to stderr. The file will be created if it does not exist, and will be

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)