From fcaaa737e75ac0e58bb68cf3ba3dfcfa93a304a6 Mon Sep 17 00:00:00 2001 From: lazerg Date: Mon, 19 Oct 2026 22:11:10 +0500 Subject: [PATCH] http: ignore trailing dot when matching NO_PROXY Assisted-by: Claude Code Signed-off-by: lazerg --- lib/internal/http.js | 9 ++++- ...est-http-proxy-request-no-proxy-domain.mjs | 40 ++++++++++++++++++- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/lib/internal/http.js b/lib/internal/http.js index 999ade29ed07..275c0d21fbf8 100644 --- a/lib/internal/http.js +++ b/lib/internal/http.js @@ -75,6 +75,11 @@ function ipToInt(ip) { return result >>> 0; } +// Follow curl's behavior: ignore a trailing dot in both the host and the NO_PROXY entries. +function stripTrailingDot(host) { + return host.length > 1 && host[host.length - 1] === '.' ? host.slice(0, -1) : host; +} + // There are two factors in play when proxying the request: // 1. What the request protocol is, that is, whether users are sending it via // http.request or https.request, or whether they are sending @@ -137,7 +142,7 @@ class ProxyConfig { this.auth = `Basic ${Buffer.from(auth).toString('base64')}`; } if (noProxyList) { - this.bypassList = noProxyList.split(',').map((entry) => entry.trim().toLowerCase()); + this.bypassList = noProxyList.split(',').map((entry) => stripTrailingDot(entry.trim().toLowerCase())); } else { this.bypassList = []; // No bypass list provided. } @@ -159,7 +164,7 @@ class ProxyConfig { return true; // No bypass list, always use the proxy. } - const host = hostname.toLowerCase(); + const host = stripTrailingDot(hostname.toLowerCase()); const hostWithPort = port ? `${host}:${port}` : host; for (let i = 0; i < bypassList.length; i++) { 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..6fb77aab342f 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,44 @@ await once(proxy, 'listening'); assert.strictEqual(signal, null); } +// Test NO_PROXY ignores a trailing dot in the request hostname. +{ + 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: 'test.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 ignores a trailing dot in the entry. +{ + 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: 'test.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 leading-dot entry should NOT match partial domain names. // Regression test: .example.com must not match notexample.com or badexample.com. {