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
25 changes: 16 additions & 9 deletions doc/api/http.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:'`.
Expand Down Expand Up @@ -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.

Expand Down
4 changes: 2 additions & 2 deletions lib/internal/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This line still uses ||, so after the patch http_proxy='' and no_proxy='' override the upper-cased variable while https_proxy='' still falls back to HTTPS_PROXY. The doc change in this PR https://github.com/barathraj048/node-js/blob/371a26a772ba62d4e9bb07c7994aba62245fb0c5/doc/api/http.md?plain=1#L221 states that https_proxy behaves like the other two, so the code and the docs here disagree.

// No proxy settings from the environment, ignore.
if (!proxyUrl) {
return null;
Expand Down Expand Up @@ -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);
}

Expand Down
52 changes: 52 additions & 0 deletions test/parallel/test-http-proxy-env-empty-value.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
'use strict';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The proxy tests live in test/client-proxy/ (72 of them), and
test/common/proxy-server.js already provides createProxyServer(),
checkProxiedRequest() and checkProxiedFetch(), which spawn the child
asynchronously and keep the servers responsive. test-http-proxy-fetch.mjs
there also shows the common.isWindows skip these cases need. The
common.hasCrypto check on L12 looks unnecessary as well: the only
client-proxy tests that need it are the TLS ones, and there is no TLS here.


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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This assertion passes without the proxy ever being used. With the patch,
env.http_proxy ?? env.HTTP_PROXY selects '', parseProxyUrl() returns
null, both clients connect directly to http://localhost:1/, the connection
is refused for both, and the comparison is false === false.

Running the test on main with both http_proxy and HTTP_PROXY empty, which
reproduces that same state, and with a request counter added to the proxy
server, prints:

assertion passed; requests received by the proxy server: 0

So the proxy server this test sets up is never contacted, and the assertion
would hold with it removed.

'fetch() and http.request() disagree on proxy usage');
})();
`;

const result = spawnSync(process.execPath, ['--use-env-proxy', '-e', script], {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

spawnSync blocks the parent event loop, so the proxy server created above
cannot answer the child it is supposed to proxy for. Running this test on
main, without the patch, it does not fail, it hangs:

$ timeout 45 out/Release/node test/parallel/test-http-proxy-env-empty-value.js; echo $?
124

In CI that is a job timeout rather than a test failure. On Windows the test
cannot express this case at all: environment variables are case-insensitive
there, so http_proxy and HTTP_PROXY collapse into a single value, which is
why the existing proxy tests skip these cases with common.isWindows.

env: {
...process.env,
http_proxy: '',
HTTP_PROXY: proxyUrl,
},
});

assert.strictEqual(result.status, 0, result.stderr.toString());
proxy.close();
}));

Check failure on line 52 in test/parallel/test-http-proxy-env-empty-value.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Newline required at end of file but not found
Loading