cli: add --use-env-proxy

This does the same as NODE_USE_ENV_PROXY. When both are set,
like other options that can be configured from both sides,
the CLI flag takes precedence.

PR-URL: https://github.com/nodejs/node/pull/59151
Fixes: https://github.com/nodejs/node/issues/59100
Reviewed-By: Ilyas Shabi <ilyasshabi94@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
This commit is contained in:
Joyee Cheung 2025-07-26 22:43:10 +02:00 committed by GitHub
parent 5e1a4fa3e4
commit 0259df9faf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 347 additions and 30 deletions

View file

@ -2999,6 +2999,21 @@ environment variables.
See `SSL_CERT_DIR` and `SSL_CERT_FILE`.
### `--use-env-proxy`
<!-- YAML
added: REPLACEME
-->
> Stability: 1.1 - Active Development
When enabled, Node.js parses the `HTTP_PROXY`, `HTTPS_PROXY` and `NO_PROXY`
environment variables during startup, and tunnels requests over the
specified proxy.
This is equivalent to setting the [`NODE_USE_ENV_PROXY=1`][] environment variable.
When both are set, `--use-env-proxy` takes precedence.
### `--use-largepages=mode`
<!-- YAML
@ -3498,6 +3513,7 @@ one is included in the list below.
* `--track-heap-objects`
* `--unhandled-rejections`
* `--use-bundled-ca`
* `--use-env-proxy`
* `--use-largepages`
* `--use-openssl-ca`
* `--use-system-ca`
@ -3653,8 +3669,8 @@ When enabled, Node.js parses the `HTTP_PROXY`, `HTTPS_PROXY` and `NO_PROXY`
environment variables during startup, and tunnels requests over the
specified proxy.
This currently only affects requests sent over `fetch()`. Support for other
built-in `http` and `https` methods is under way.
This can also be enabled using the [`--use-env-proxy`][] command-line flag.
When both are set, `--use-env-proxy` takes precedence.
### `NODE_V8_COVERAGE=dir`
@ -3984,12 +4000,14 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
[`--print`]: #-p---print-script
[`--redirect-warnings`]: #--redirect-warningsfile
[`--require`]: #-r---require-module
[`--use-env-proxy`]: #--use-env-proxy
[`AsyncLocalStorage`]: async_context.md#class-asynclocalstorage
[`Buffer`]: buffer.md#class-buffer
[`CRYPTO_secure_malloc_init`]: https://www.openssl.org/docs/man3.0/man3/CRYPTO_secure_malloc_init.html
[`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_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
[`YoungGenerationSizeFromSemiSpaceSize`]: https://chromium.googlesource.com/v8/v8.git/+/refs/tags/10.3.129/src/heap/heap.cc#328

View file

@ -2501,7 +2501,7 @@ Failed to proxy a request because the proxy configuration is invalid.
### `ERR_PROXY_TUNNEL`
Failed to establish proxy tunnel when `NODE_USE_ENV_PROXY` is enabled.
Failed to establish proxy tunnel when `NODE_USE_ENV_PROXY` or `--use-env-proxy` is enabled.
<a id="ERR_QUIC_APPLICATION_ERROR"></a>

View file

@ -4273,10 +4273,9 @@ added: REPLACEME
> Stability: 1.1 - Active development
When Node.js creates the global agent, it checks the `NODE_USE_ENV_PROXY`
environment variable. If it is set to `1`, the global agent will be constructed
When Node.js creates the global agent, if the `NODE_USE_ENV_PROXY` environment variable is
set to `1` or `--use-env-proxy` is enabled, the global agent will be constructed
with `proxyEnv: process.env`, enabling proxy support based on the environment variables.
Custom agents can also be created with proxy support by passing a
`proxyEnv` option when constructing the agent. The value can be `process.env`
if they just want to inherit the configuration from the environment variables,
@ -4318,13 +4317,20 @@ Multiple entries should be separated by commas.
### Example
Starting a Node.js process with proxy support enabled for all requests sent
through the default global agent:
To start a Node.js process with proxy support enabled for all requests sent
through the default global agent, either use the `NODE_USE_ENV_PROXY` environment
variable:
```console
NODE_USE_ENV_PROXY=1 HTTP_PROXY=http://proxy.example.com:8080 NO_PROXY=localhost,127.0.0.1 node client.js
```
Or the `--use-env-proxy` flag.
```console
HTTP_PROXY=http://proxy.example.com:8080 NO_PROXY=localhost,127.0.0.1 node --use-env-proxy client.js
```
To create a custom agent with built-in proxy support:
```cjs

View file

@ -541,6 +541,9 @@
"use-bundled-ca": {
"type": "boolean"
},
"use-env-proxy": {
"type": "boolean"
},
"use-largepages": {
"type": "string"
},

View file

@ -603,6 +603,9 @@ See
and
.Ev SSL_CERT_FILE .
.
.It Fl -use-env-proxy
Parse proxy settings from HTTP_PROXY/HTTPS_PROXY/NO_PROXY environment variables and apply the setting in global HTTP/HTTPS clients.
.
.It Fl -use-largepages Ns = Ns Ar mode
Re-map the Node.js static code to large memory pages at startup. If supported on
the target system, this will cause the Node.js static code to be moved onto 2

View file

@ -54,6 +54,7 @@ const {
validateString,
} = require('internal/validators');
const assert = require('internal/assert');
const { getOptionValue } = require('internal/options');
const kOnKeylog = Symbol('onkeylog');
const kRequestOptions = Symbol('requestOptions');
@ -622,6 +623,7 @@ module.exports = {
Agent,
globalAgent: new Agent({
keepAlive: true, scheduling: 'lifo', timeout: 5000,
proxyEnv: process.env.NODE_USE_ENV_PROXY ? filterEnvForProxies(process.env) : undefined,
// This normalized from both --use-env-proxy and NODE_USE_ENV_PROXY settings.
proxyEnv: getOptionValue('--use-env-proxy') ? filterEnvForProxies(process.env) : undefined,
}),
};

View file

@ -70,6 +70,7 @@ const { URL, urlToHttpOptions, isURL } = require('internal/url');
const { validateObject } = require('internal/validators');
const { isIP, isIPv6 } = require('internal/net');
const assert = require('internal/assert');
const { getOptionValue } = require('internal/options');
function Server(opts, requestListener) {
if (!(this instanceof Server)) return new Server(opts, requestListener);
@ -599,7 +600,8 @@ Agent.prototype._evictSession = function _evictSession(key) {
const globalAgent = new Agent({
keepAlive: true, scheduling: 'lifo', timeout: 5000,
proxyEnv: process.env.NODE_USE_ENV_PROXY ? filterEnvForProxies(process.env) : undefined,
// This normalized from both --use-env-proxy and NODE_USE_ENV_PROXY settings.
proxyEnv: getOptionValue('--use-env-proxy') ? filterEnvForProxies(process.env) : undefined,
});
/**

View file

@ -166,19 +166,24 @@ function prepareExecution(options) {
}
function setupHttpProxy() {
if (process.env.NODE_USE_ENV_PROXY &&
(process.env.HTTP_PROXY || process.env.HTTPS_PROXY ||
process.env.http_proxy || process.env.https_proxy)) {
// This normalized from both --use-env-proxy and NODE_USE_ENV_PROXY settings.
if (!getOptionValue('--use-env-proxy')) {
return;
}
if (!process.env.HTTP_PROXY && !process.env.HTTPS_PROXY &&
!process.env.http_proxy && !process.env.https_proxy) {
return;
}
const { setGlobalDispatcher, EnvHttpProxyAgent } = require('internal/deps/undici/undici');
const envHttpProxyAgent = new EnvHttpProxyAgent();
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
// actually created, in lib/_http_agent.js and lib/https.js.
// TODO(joyeecheung): This is currently guarded with NODE_USE_ENV_PROXY. Investigate whether
// it's possible to enable it by default without stepping on other existing libraries that
// sets the global dispatcher or monkey patches the global agent.
}
// TODO(joyeecheung): This is currently guarded with NODE_USE_ENV_PROXY and --use-env-proxy.
// Investigate whether it's possible to enable it by default without stepping on other
// existing libraries that sets the global dispatcher or monkey patches the global agent.
}
function setupUserModules(forceDefaultLoader = false) {

View file

@ -664,6 +664,12 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
"emit pending deprecation warnings",
&EnvironmentOptions::pending_deprecation,
kAllowedInEnvvar);
AddOption("--use-env-proxy",
"parse proxy settings from HTTP_PROXY/HTTPS_PROXY/NO_PROXY"
"environment variables and apply the setting in global HTTP/HTTPS "
"clients",
&EnvironmentOptions::use_env_proxy,
kAllowedInEnvvar);
AddOption("--preserve-symlinks",
"preserve symbolic links when resolving",
&EnvironmentOptions::preserve_symlinks,
@ -1878,6 +1884,8 @@ void HandleEnvOptions(std::shared_ptr<EnvironmentOptions> env_options,
env_options->preserve_symlinks_main =
opt_getter("NODE_PRESERVE_SYMLINKS_MAIN") == "1";
env_options->use_env_proxy = opt_getter("NODE_USE_ENV_PROXY") == "1";
if (env_options->redirect_warnings.empty())
env_options->redirect_warnings = opt_getter("NODE_REDIRECT_WARNINGS");
}

View file

@ -242,6 +242,7 @@ class EnvironmentOptions : public Options {
bool force_repl = false;
bool insecure_http_parser = false;
bool use_env_proxy = false;
bool tls_min_v1_0 = false;
bool tls_min_v1_1 = false;

View file

@ -78,8 +78,6 @@ if (!common.isWindows) {
REQUEST_URL: requestUrl,
http_proxy: `http://localhost:${proxy.address().port}`,
HTTP_PROXY: `http://localhost:${proxy2.address().port}`,
}, {
stdout: 'Hello world',
});
assert.deepStrictEqual(logs, expectedLogs);
assert.strictEqual(stderr.trim(), '');

View file

@ -92,8 +92,6 @@ if (!common.isWindows) {
https_proxy: `http://localhost:${proxy.address().port}`,
HTTPS_PROXY: `http://localhost:${proxy2.address().port}`,
NODE_EXTRA_CA_CERTS: fixtures.path('keys', 'fake-startcom-root-cert.pem'),
}, {
stdout: 'Hello world',
});
assert.deepStrictEqual(logs, expectedLogs);
assert.strictEqual(stderr.trim(), '');

View file

@ -0,0 +1,75 @@
// This tests that --use-env-proxy works the same as NODE_USE_ENV_PROXY=1
// for HTTP requests using the built-in http module and fetch API.
import * as common from '../common/index.mjs';
import assert from 'node:assert';
import http from 'node:http';
import { once } from 'events';
import { createProxyServer, runProxiedRequest, 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`;
// Tests --use-env-proxy works with http builtins.
{
const { code, signal, stderr, stdout } = await runProxiedRequest({
REQUEST_URL: requestUrl,
HTTP_PROXY: `http://localhost:${proxy.address().port}`,
}, ['--use-env-proxy']);
assert.strictEqual(stderr.trim(), '');
assert.match(stdout, /Hello world/);
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
assert.deepStrictEqual(logs, [{
method: 'GET',
url: requestUrl,
headers: {
'connection': 'keep-alive',
'proxy-connection': 'keep-alive',
'host': serverHost,
},
}]);
}
// Tests --use-env-proxy works with fetch and http.
{
logs.splice(0, logs.length);
await checkProxiedFetch({
FETCH_URL: requestUrl,
HTTP_PROXY: `http://localhost:${proxy.address().port}`,
}, {
stdout: 'Hello world',
}, ['--use-env-proxy']);
// FIXME(undici:4083): undici currently always tunnels the request over
// CONNECT if proxyTunnel is not explicitly set to false, but what we
// need is for it to be automatically false for HTTP requests to be
// consistent with curl.
assert.deepStrictEqual(logs, [{
method: 'CONNECT',
url: serverHost,
headers: {
'connection': 'close',
'proxy-connection': 'keep-alive',
'host': serverHost,
},
}]);
}
server.close();
proxy.close();

View file

@ -0,0 +1,81 @@
// This tests that --use-env-proxy works the same as NODE_USE_ENV_PROXY=1
// for HTTPS requests using the built-in https module and fetch API.
import * as common from '../common/index.mjs';
import fixtures from '../common/fixtures.js';
import assert from 'node:assert';
import { once } from 'events';
import { createProxyServer, runProxiedRequest, checkProxiedFetch } from '../common/proxy-server.js';
if (!common.hasCrypto) {
common.skip('missing crypto');
}
// https must be dynamically imported so that builds without crypto support
// can skip it.
const { default: https } = await import('node:https');
// 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 HTTPS server to process the final request.
const server = https.createServer({
cert: fixtures.readKey('agent8-cert.pem'),
key: fixtures.readKey('agent8-key.pem'),
}, 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 = `https://${serverHost}/test`;
// Tests --use-env-proxy works with https builtins.
{
const { code, signal, stderr, stdout } = await runProxiedRequest({
REQUEST_URL: requestUrl,
HTTPS_PROXY: `http://localhost:${proxy.address().port}`,
NODE_EXTRA_CA_CERTS: fixtures.path('keys', 'fake-startcom-root-cert.pem'),
}, ['--use-env-proxy']);
assert.strictEqual(stderr.trim(), '');
assert.match(stdout, /Hello world/);
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
assert.deepStrictEqual(logs, [{
method: 'CONNECT',
url: serverHost,
headers: {
'proxy-connection': 'keep-alive',
'host': serverHost,
},
}]);
}
// Tests --use-env-proxy works with fetch and https.
{
logs.splice(0, logs.length);
await checkProxiedFetch({
FETCH_URL: `https://${serverHost}/test`,
HTTPS_PROXY: `http://localhost:${proxy.address().port}`,
NODE_EXTRA_CA_CERTS: fixtures.path('keys', 'fake-startcom-root-cert.pem'),
}, {
stdout: 'Hello world',
}, ['--use-env-proxy']);
assert.deepStrictEqual(logs, [{
method: 'CONNECT',
url: serverHost,
headers: {
'connection': 'close',
'proxy-connection': 'keep-alive',
'host': serverHost,
},
}]);
}
server.close();
proxy.close();

View file

@ -0,0 +1,117 @@
// This tests the precedence and interaction between --use-env-proxy CLI flag
// and NODE_USE_ENV_PROXY environment variable when both are set.
import * as common from '../common/index.mjs';
import assert from 'node:assert';
import http from 'node:http';
import { once } from 'events';
import { createProxyServer, runProxiedRequest } from '../common/proxy-server.js';
// Start a proxy server for testing
const { proxy, logs } = createProxyServer();
proxy.listen(0);
await once(proxy, 'listening');
// Start a HTTP server to process the final request
const server = http.createServer(common.mustCall((req, res) => {
res.end('Hello world');
}, 4));
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`;
delete process.env.NODE_USE_ENV_PROXY; // Ensure the environment variable is not set.
// NODE_USE_ENV_PROXY=1 and --use-env-proxy can be used at the same time.
{
const { code, signal, stderr, stdout } = await runProxiedRequest({
NODE_USE_ENV_PROXY: '1',
REQUEST_URL: requestUrl,
HTTP_PROXY: `http://localhost:${proxy.address().port}`,
}, ['--use-env-proxy']);
assert.strictEqual(stderr.trim(), '');
assert.match(stdout, /Hello world/);
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
// Should use the proxy
assert.strictEqual(logs.length, 1);
assert.deepStrictEqual(logs[0], {
method: 'GET',
url: requestUrl,
headers: {
'connection': 'keep-alive',
'proxy-connection': 'keep-alive',
'host': serverHost,
},
});
logs.splice(0, logs.length);
}
// NODE_USE_ENV_PROXY=0 and --no-use-env-proxy can be used at the same time.
{
const { code, signal, stderr, stdout } = await runProxiedRequest({
NODE_USE_ENV_PROXY: '0',
REQUEST_URL: requestUrl,
HTTP_PROXY: `http://localhost:${proxy.address().port}`,
}, ['--no-use-env-proxy']);
assert.strictEqual(stderr.trim(), '');
assert.match(stdout, /Hello world/);
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
// Should NOT use the proxy
assert.strictEqual(logs.length, 0);
}
// --use-env-proxy CLI flag takes precedence over NODE_USE_ENV_PROXY=0.
{
const { code, signal, stderr, stdout } = await runProxiedRequest({
NODE_USE_ENV_PROXY: '0',
REQUEST_URL: requestUrl,
HTTP_PROXY: `http://localhost:${proxy.address().port}`,
}, ['--use-env-proxy']);
assert.strictEqual(stderr.trim(), '');
assert.match(stdout, /Hello world/);
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
// Should use the proxy because CLI flag takes precedence
assert.strictEqual(logs.length, 1);
assert.deepStrictEqual(logs[0], {
method: 'GET',
url: requestUrl,
headers: {
'connection': 'keep-alive',
'proxy-connection': 'keep-alive',
'host': serverHost,
},
});
logs.splice(0, logs.length);
}
// --no-use-env-proxy CLI flag disables the proxy even if NODE_USE_ENV_PROXY=1.
{
const { code, signal, stderr, stdout } = await runProxiedRequest({
NODE_USE_ENV_PROXY: '1',
REQUEST_URL: requestUrl,
HTTP_PROXY: `http://localhost:${proxy.address().port}`,
}, ['--no-use-env-proxy']);
// Should NOT use the proxy because CLI flag takes precedence.
assert.strictEqual(stderr.trim(), '');
assert.match(stdout, /Hello world/);
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
assert.strictEqual(logs.length, 0);
}
proxy.close();
server.close();

View file

@ -142,11 +142,11 @@ function spawnPromisified(...args) {
});
}
exports.checkProxiedFetch = async function(envExtension, expectation) {
exports.checkProxiedFetch = async function(envExtension, expectation, cliArgsExtension = []) {
const fixtures = require('./fixtures');
const { code, signal, stdout, stderr } = await spawnPromisified(
process.execPath,
[fixtures.path('fetch-and-log.mjs')], {
[...cliArgsExtension, fixtures.path('fetch-and-log.mjs')], {
env: {
...process.env,
...envExtension,
@ -166,11 +166,11 @@ exports.checkProxiedFetch = async function(envExtension, expectation) {
});
};
exports.runProxiedRequest = async function(envExtension) {
exports.runProxiedRequest = async function(envExtension, cliArgsExtension = []) {
const fixtures = require('./fixtures');
return spawnPromisified(
process.execPath,
[fixtures.path('request-and-log.js')], {
[...cliArgsExtension, fixtures.path('request-and-log.js')], {
env: {
...process.env,
...envExtension,