Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -3903,6 +3903,25 @@ added: v7.1.0
When set to `1`, instructs the module loader to preserve symbolic links when
resolving and caching modules.

### `NODE_PROXY_TUNNEL`

<!-- YAML
added: REPLACEME
-->

> Stability: 1.1 - Active Development

When `NODE_USE_ENV_PROXY=1` or `--use-env-proxy` is enabled, controls whether
proxy connections use the HTTP CONNECT tunneling method.

When set to `true` or `1` (the default), undici uses CONNECT tunneling to establish
a proxy connection. When set to `false` or `0`, undici uses direct HTTP forwarding
instead, which may be required for proxies that do not support the CONNECT method.

Valid values: `true`, `1`, `false`, `0`.

See also [`NODE_USE_ENV_PROXY=1`][].

### `NODE_REDIRECT_WARNINGS=file`

<!-- YAML
Expand Down Expand Up @@ -3978,7 +3997,8 @@ added:

When enabled, Node.js parses the `HTTP_PROXY`, `HTTPS_PROXY` and `NO_PROXY`
environment variables during startup, and tunnels requests over the
specified proxy.
specified proxy. Use [`NODE_PROXY_TUNNEL`][] to disable CONNECT tunneling
for proxies that do not support it.

This can also be enabled using the [`--use-env-proxy`][] command-line flag.
When both are set, `--use-env-proxy` takes precedence.
Expand Down Expand Up @@ -4338,6 +4358,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
[`ERR_INVALID_TYPESCRIPT_SYNTAX`]: errors.md#err_invalid_typescript_syntax
[`ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX`]: errors.md#err_unsupported_typescript_syntax
[`NODE_OPTIONS`]: #node_optionsoptions
[`NODE_PROXY_TUNNEL`]: #node_proxy_tunnel
[`NODE_USE_ENV_PROXY=1`]: #node_use_env_proxy1
[`NO_COLOR`]: https://no-color.org
[`Web Storage`]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API
Expand Down
7 changes: 7 additions & 0 deletions doc/api/http.md
Original file line number Diff line number Diff line change
Expand Up @@ -4515,6 +4515,13 @@ Or the `--use-env-proxy` flag.
HTTP_PROXY=http://proxy.example.com:8080 NO_PROXY=localhost,127.0.0.1 node --use-env-proxy client.js
```

If the proxy does not support the HTTP CONNECT method, set `NODE_PROXY_TUNNEL=false`
to use direct HTTP forwarding instead:

```console
NODE_USE_ENV_PROXY=1 NODE_PROXY_TUNNEL=false HTTP_PROXY=http://proxy.example.com:8080 node client.js
```

To enable proxy support dynamically and globally with `process.env` (the default option of `http.setGlobalProxyFromEnv()`):

```cjs
Expand Down
10 changes: 10 additions & 0 deletions doc/node.1
Original file line number Diff line number Diff line change
Expand Up @@ -2232,6 +2232,16 @@ for connections. This setting applies to Windows only.
When set to \fB1\fR, instructs the module loader to preserve symbolic links when
resolving and caching modules.
.
.It Ev NODE_PROXY_TUNNEL
When \fBNODE_USE_ENV_PROXY=1\fR or \fB--use-env-proxy\fR is enabled, controls whether
proxy connections use the HTTP CONNECT tunneling method.

When set to \fBtrue\fR or \fB1\fR (the default), undici uses CONNECT tunneling to establish
a proxy connection. When set to \fBfalse\fR or \fB0\fR, undici uses direct HTTP forwarding
instead, which may be required for proxies that do not support the CONNECT method.

Valid values: \fBtrue\fR, \fB1\fR, \fBfalse\fR, \fB0\fR.
.
.It Ev NODE_REDIRECT_WARNINGS Ar file
When set, process warnings will be emitted to the given file instead of
printing to stderr. The file will be created if it does not exist, and will be
Expand Down
7 changes: 6 additions & 1 deletion lib/internal/process/pre_execution.js
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,12 @@ function setupHttpProxy() {
}

const { setGlobalDispatcher, EnvHttpProxyAgent } = require('internal/deps/undici/undici');
const envHttpProxyAgent = new EnvHttpProxyAgent();
const envProxyTunnel = process.env.NODE_PROXY_TUNNEL;
let proxyTunnel;
if (envProxyTunnel !== undefined) {
proxyTunnel = envProxyTunnel === 'true' || envProxyTunnel === '1';
}
const envHttpProxyAgent = new EnvHttpProxyAgent({ proxyTunnel });
Copy link
Copy Markdown
Member

@joyeecheung joyeecheung Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this disable tunneling for HTTPS requests too? If so, that sounds like an insecure behavior - HTTPS should always be tunneled while HTTP doesn't need tunneling. That's what curl has and also what http/https.request implements. If it only disables tunneling for HTTP, I think we can just unconditionally set it to false and don't need to add an env var for this unless anyone specifically requests being able to tunnel pure HTTP requests (I doubt anyone would actually need it considering that's not what curl and most other runtimes do)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good spot. I totally misunderstood this problem. I can autodetect this.

setGlobalDispatcher(envHttpProxyAgent);
// For fetch, we need to set the global dispatcher from here.
// For http/https agents, we'll configure the global agent when they are
Expand Down
72 changes: 72 additions & 0 deletions test/client-proxy/test-node-proxy-tunnel.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Tests that NODE_PROXY_TUNNEL=false disables CONNECT tunneling
// and uses direct HTTP forwarding instead.

import * as common from '../common/index.mjs';
import assert from 'node:assert';
import http from 'node:http';
import { once } from 'events';
import { createProxyServer, checkProxiedFetch } from '../common/proxy-server.js';

// Start a minimal proxy server.
const { proxy, logs } = createProxyServer();
proxy.listen(0);
await once(proxy, 'listening');

delete process.env.NODE_USE_ENV_PROXY; // Ensure the environment variable is not set.

// Start a HTTP server to process the final request.
const server = http.createServer(common.mustCall((req, res) => {
res.end('Hello world');
}, 2));
server.on('error', common.mustNotCall((err) => { console.error('Server error', err); }));
server.listen(0);
await once(server, 'listening');

const serverHost = `localhost:${server.address().port}`;
const requestUrl = `http://${serverHost}/test`;

// Test: with NODE_PROXY_TUNNEL=false, fetch should NOT use CONNECT tunneling
// but instead use direct HTTP forwarding.
{
await checkProxiedFetch({
FETCH_URL: requestUrl,
HTTP_PROXY: `http://localhost:${proxy.address().port}`,
NODE_USE_ENV_PROXY: '1',
NODE_PROXY_TUNNEL: 'false',
}, {
stdout: 'Hello world',
});

// With proxyTunnel: false, undici uses Http1ProxyWrapper which rewrites
// the request path to include the origin (e.g. "localhost:PORT/test")
// and sends it as a normal GET request instead of CONNECT.
assert.strictEqual(logs[0].method, 'GET');
assert.strictEqual(logs[0].url, requestUrl);
assert.strictEqual(logs[0].headers.host, serverHost);
}

// Test: without NODE_PROXY_TUNNEL (default), fetch still uses CONNECT tunneling.
{
logs.splice(0, logs.length);
await checkProxiedFetch({
FETCH_URL: requestUrl,
HTTP_PROXY: `http://localhost:${proxy.address().port}`,
NODE_USE_ENV_PROXY: '1',
}, {
stdout: 'Hello world',
});

// Without NODE_PROXY_TUNNEL set, CONNECT tunneling is used by default.
assert.deepStrictEqual(logs, [{
method: 'CONNECT',
url: serverHost,
headers: {
'connection': 'close',
'proxy-connection': 'keep-alive',
'host': serverHost,
},
}]);
}

server.close();
proxy.close();
Loading