diff --git a/doc/api/http.md b/doc/api/http.md index 4c1c5ad1321a..c2c86eb76822 100644 --- a/doc/api/http.md +++ b/doc/api/http.md @@ -4615,7 +4615,7 @@ Proxy URLs can use either HTTP or HTTPS protocols: The `NO_PROXY` environment variable supports several formats: * `*` - Bypass proxy for all hosts -* `example.com` - Exact host name match +* `example.com` - Host and subdomain match (matches `sub.example.com`) * `.example.com` - Domain suffix match (matches `sub.example.com`) * `*.example.com` - Wildcard domain match * `192.168.1.100` - Exact IP address match diff --git a/lib/internal/http.js b/lib/internal/http.js index 304ef04d6638..75e9fe4e8547 100644 --- a/lib/internal/http.js +++ b/lib/internal/http.js @@ -163,9 +163,10 @@ class ProxyConfig { if (entry === '*') return false; // * bypasses all hosts. if (entry === host || entry === hostWithPort) return false; // Matching host and host:port - // Follow curl's behavior: strip leading dot before matching suffixes. - if (entry[0] === '.') { - const suffix = entry.substring(1); + // Strip a leading "." if present, then match as a suffix with a + // label boundary. "*.example.com" is handled below (subdomains only). + if (!entry.startsWith('*.')) { + const suffix = entry[0] === '.' ? entry.substring(1) : entry; if (host === suffix || (host.endsWith(suffix) && host[host.length - suffix.length - 1] === '.')) return false; } diff --git a/test/client-proxy/test-http-proxy-request-no-proxy-domain.mjs b/test/client-proxy/test-http-proxy-request-no-proxy-domain.mjs index 1863f3bc94b2..c43032281705 100644 --- a/test/client-proxy/test-http-proxy-request-no-proxy-domain.mjs +++ b/test/client-proxy/test-http-proxy-request-no-proxy-domain.mjs @@ -10,7 +10,7 @@ import { runProxiedRequest } from '../common/proxy-server.js'; const server = http.createServer(common.mustCall((req, res) => { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('Hello World\n'); -}, 5)); +}, 7)); server.on('error', common.mustNotCall((err) => { console.error('Server error', err); })); server.listen(0, '127.0.0.1'); await once(server, 'listening'); @@ -78,6 +78,43 @@ await once(proxy, 'listening'); assert.strictEqual(signal, null); } +{ + // Test NO_PROXY with a plain domain also matching subdomains. + const { code, signal, stderr, stdout } = await runProxiedRequest({ + NODE_USE_ENV_PROXY: 1, + REQUEST_URL: `http://test.example.com:${server.address().port}/test`, + HTTP_PROXY: `http://localhost:${proxy.address().port}`, + RESOLVE_TO_LOCALHOST: 'test.example.com', + NO_PROXY: 'example.com', + }); + + // The request should succeed and bypass proxy. + assert.match(stdout, /Status Code: 200/); + assert.match(stdout, /Hello World/); + assert.match(stdout, /Resolving lookup for test\.example\.com/); + assert.strictEqual(stderr.trim(), ''); + assert.strictEqual(code, 0); + assert.strictEqual(signal, null); +} + +{ + // Test NO_PROXY with a plain domain should NOT match partial domain names. + const { code, signal, stderr, stdout } = await runProxiedRequest({ + NODE_USE_ENV_PROXY: 1, + REQUEST_URL: `http://badexample.com:${server.address().port}/test`, + HTTP_PROXY: `http://localhost:${server.address().port}`, + RESOLVE_TO_LOCALHOST: 'badexample.com', + NO_PROXY: 'example.com', + }); + + // The request should go through the proxy (not bypass it), + // because badexample.com is not a subdomain of example.com. + assert.match(stdout, /Status Code: 200/); + assert.strictEqual(stderr.trim(), ''); + assert.strictEqual(code, 0); + assert.strictEqual(signal, null); +} + // Test NO_PROXY with leading-dot entry should NOT match partial domain names. // Regression test: .example.com must not match notexample.com or badexample.com. {