diff --git a/doc/api/http.md b/doc/api/http.md index f263469d807c..919b8a0a2bc7 100644 --- a/doc/api/http.md +++ b/doc/api/http.md @@ -215,9 +215,15 @@ changes: If undefined, no proxy is used for HTTPS requests. * `NO_PROXY` {string|undefined} Patterns specifying the endpoints that should not be routed through a proxy. - * `http_proxy` {string|undefined} Same as `HTTP_PROXY`. If both are set, `http_proxy` takes precedence. - * `https_proxy` {string|undefined} Same as `HTTPS_PROXY`. If both are set, `https_proxy` takes precedence. - * `no_proxy` {string|undefined} Same as `NO_PROXY`. If both are set, `no_proxy` takes precedence. + * `http_proxy` {string|undefined} Same as `HTTP_PROXY`. If both are set, `http_proxy` takes + precedence, including when `http_proxy` is explicitly set to an empty string (in which case + no proxy is used for HTTP requests, and `HTTP_PROXY` is not consulted). + * `https_proxy` {string|undefined} Same as `HTTPS_PROXY`. If both are set, `https_proxy` takes + precedence, including when `https_proxy` is explicitly set to an empty string (in which case + no proxy is used for HTTPS requests, and `HTTPS_PROXY` is not consulted). + * `no_proxy` {string|undefined} Same as `NO_PROXY`. If both are set, `no_proxy` takes + precedence, including when `no_proxy` is explicitly set to an empty string (in which case + `NO_PROXY` is not consulted, and no hosts are bypassed unless matched by other rules). * `defaultPort` {number} Default port to use when the port is not specified in requests. **Default:** `80`. * `protocol` {string} The protocol to use for the agent. **Default:** `'http:'`. @@ -4579,12 +4585,13 @@ or an object with specific setting overriding the environment. The following properties of the `proxyEnv` are checked to configure proxy support. -* `HTTP_PROXY` or `http_proxy`: Proxy server URL for HTTP requests. If both are set, - `http_proxy` takes precedence. -* `HTTPS_PROXY` or `https_proxy`: Proxy server URL for HTTPS requests. If both are set, - `https_proxy` takes precedence. -* `NO_PROXY` or `no_proxy`: Comma-separated list of hosts to bypass the proxy. If both are set, - `no_proxy` takes precedence. +#### Precedence and empty values + +For each pair above, the lower-cased variable takes precedence over the upper-cased one +whenever the lower-cased variable is explicitly set in the environment — **even if it is +set to an empty string**. An empty lower-cased value is not treated as unset: it overrides +the upper-cased variable rather than falling back to it. To have the upper-cased variable +apply, the lower-cased variable must be unset entirely (not merely empty). If the request is made to a Unix domain socket, the proxy settings will be ignored. diff --git a/lib/internal/http.js b/lib/internal/http.js index 999ade29ed07..62afbeb80bbf 100644 --- a/lib/internal/http.js +++ b/lib/internal/http.js @@ -203,7 +203,7 @@ function parseProxyUrl(env, protocol) { // Get the proxy url - following the most popular convention, lower case takes precedence. // See https://about.gitlab.com/blog/we-need-to-talk-no-proxy/#http_proxy-and-https_proxy const proxyUrl = (protocol === 'https:') ? - (env.https_proxy || env.HTTPS_PROXY) : (env.http_proxy || env.HTTP_PROXY); + (env.https_proxy || env.HTTPS_PROXY) : (env.http_proxy ?? env.HTTP_PROXY); // No proxy settings from the environment, ignore. if (!proxyUrl) { return null; @@ -234,7 +234,7 @@ function parseProxyConfigFromEnv(env, protocol, keepAlive) { return null; } - const noProxyList = env.no_proxy || env.NO_PROXY; + const noProxyList = env.no_proxy ?? env.NO_PROXY; return new ProxyConfig(proxyUrl, keepAlive, noProxyList); } diff --git a/test/parallel/test-http-proxy-env-empty-value.js b/test/parallel/test-http-proxy-env-empty-value.js new file mode 100644 index 000000000000..ef1ab50ecca9 --- /dev/null +++ b/test/parallel/test-http-proxy-env-empty-value.js @@ -0,0 +1,52 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const http = require('http'); +const { spawnSync } = require('child_process'); + +// Regression test for https://github.com/nodejs/node/issues/66202 +// fetch() and http.request() must agree on how to treat an explicit +// empty string in a lower-cased proxy env var. + +if (!common.hasCrypto) common.skip('missing crypto'); + +const proxy = http.createServer((req, res) => { + res.setHeader('x-via-proxy', '1'); + res.end('ok'); +}); + +proxy.listen(0, common.mustCall(() => { + const proxyUrl = `http://localhost:${proxy.address().port}`; + + const script = ` + const assert = require('assert'); + (async () => { + const usesProxyRequest = await new Promise((resolve) => { + require('http').get('http://localhost:1/', (res) => { + resolve(res.headers['x-via-proxy'] === '1'); + }).on('error', () => resolve(false)); + }); + + let usesProxyFetch = false; + try { + const res = await fetch('http://localhost:1/'); + usesProxyFetch = res.headers.get('x-via-proxy') === '1'; + } catch { /* direct connection refused is expected if no proxy used */ } + + assert.strictEqual(usesProxyFetch, usesProxyRequest, + 'fetch() and http.request() disagree on proxy usage'); + })(); + `; + + const result = spawnSync(process.execPath, ['--use-env-proxy', '-e', script], { + env: { + ...process.env, + http_proxy: '', + HTTP_PROXY: proxyUrl, + }, + }); + + assert.strictEqual(result.status, 0, result.stderr.toString()); + proxy.close(); +})); \ No newline at end of file