From 5e8f9e0a037f4f2904fb74d8e268d3570334fa8e Mon Sep 17 00:00:00 2001 From: Mohit Kumar <91405114+mohitduhan19@users.noreply.github.com> Date: Fri, 16 Oct 2026 00:13:38 +0530 Subject: [PATCH 1/2] grpc-js: Fix no_proxy matching hosts that only share a suffix An entry like "local.com" also matched "notlocal.com". Entries now match only the host itself or its subdomains, as curl does. Fixes #3039 --- packages/grpc-js/src/http_proxy.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/grpc-js/src/http_proxy.ts b/packages/grpc-js/src/http_proxy.ts index cd1d533af..6095b4c3a 100644 --- a/packages/grpc-js/src/http_proxy.ts +++ b/packages/grpc-js/src/http_proxy.ts @@ -17,7 +17,7 @@ import { log } from './logging'; import { LogVerbosity } from './constants'; -import { isIPv4, Socket } from 'net'; +import { isIP, isIPv4, Socket } from 'net'; import * as http from 'http'; import * as logging from './logging'; import { @@ -169,13 +169,25 @@ function isIpInCIDR(cidr: CIDRNotation, serverHost: string) { return (hostIP & mask) === (ip & mask); } +function hostMatchesNoProxyEntry(serverHost: string, entry: string): boolean { + const domain = entry.startsWith('.') ? entry.substring(1) : entry; + if (serverHost === domain) { + return true; + } + return !isIP(serverHost) && serverHost.endsWith('.' + domain); +} + function hostMatchesNoProxyList(serverHost: string): boolean { - for (const host of getNoProxyHostList()) { + for (const entry of getNoProxyHostList()) { + const host = entry.trim(); + if (!host) { + continue; + } const parsedCIDR = parseCIDR(host); // host is a CIDR and serverHost is an IP address if (isIPv4(serverHost) && parsedCIDR && isIpInCIDR(parsedCIDR, serverHost)) { return true; - } else if (serverHost.endsWith(host)) { + } else if (hostMatchesNoProxyEntry(serverHost, host)) { // host is a single IP or a domain name suffix return true; } From 644040e5b45e3d257755adada84e72f5d3486902 Mon Sep 17 00:00:00 2001 From: Mohit Kumar <91405114+mohitduhan19@users.noreply.github.com> Date: Fri, 16 Oct 2026 00:14:49 +0530 Subject: [PATCH 2/2] grpc-js: Add tests for no_proxy host matching --- packages/grpc-js/test/test-http-proxy.ts | 110 +++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 packages/grpc-js/test/test-http-proxy.ts diff --git a/packages/grpc-js/test/test-http-proxy.ts b/packages/grpc-js/test/test-http-proxy.ts new file mode 100644 index 000000000..1f6cf037a --- /dev/null +++ b/packages/grpc-js/test/test-http-proxy.ts @@ -0,0 +1,110 @@ +/* + * Copyright 2026 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import * as assert from 'assert'; +import { mapProxyName } from '../src/http_proxy'; +import { parseUri } from '../src/uri-parser'; + +const PROXY_OPTIONS = { 'grpc.http_proxy': 'http://proxy.example:3128' }; + +function usesProxy(target: string): boolean { + const parsedTarget = parseUri(target); + assert(parsedTarget, `Failed to parse target ${target}`); + const result = mapProxyName(parsedTarget, PROXY_OPTIONS); + return 'grpc.http_connect_target' in result.extraOptions; +} + +describe('HTTP proxy no_proxy matching', () => { + let savedNoGrpcProxy: string | undefined; + let savedNoProxy: string | undefined; + + beforeEach(() => { + savedNoGrpcProxy = process.env.no_grpc_proxy; + savedNoProxy = process.env.no_proxy; + delete process.env.no_grpc_proxy; + delete process.env.no_proxy; + }); + + afterEach(() => { + if (savedNoGrpcProxy === undefined) { + delete process.env.no_grpc_proxy; + } else { + process.env.no_grpc_proxy = savedNoGrpcProxy; + } + if (savedNoProxy === undefined) { + delete process.env.no_proxy; + } else { + process.env.no_proxy = savedNoProxy; + } + }); + + it('Uses the proxy when no_proxy is not set', () => { + assert.strictEqual(usesProxy('dns:local.com:443'), true); + }); + + it('Bypasses the proxy for an exact domain match', () => { + process.env.no_grpc_proxy = 'local.com,apple.com'; + assert.strictEqual(usesProxy('dns:local.com:443'), false); + assert.strictEqual(usesProxy('dns:apple.com:443'), false); + }); + + it('Bypasses the proxy for subdomains of an entry', () => { + process.env.no_grpc_proxy = 'local.com,apple.com'; + assert.strictEqual(usesProxy('dns:www.local.com:443'), false); + assert.strictEqual(usesProxy('dns:a.b.apple.com:443'), false); + }); + + it('Does not bypass the proxy for hosts that only share a suffix', () => { + process.env.no_grpc_proxy = 'local.com,apple.com'; + assert.strictEqual(usesProxy('dns:notlocal.com:443'), true); + assert.strictEqual(usesProxy('dns:www.notlocal.com:443'), true); + assert.strictEqual(usesProxy('dns:www.snapple.com:443'), true); + }); + + it('Treats a leading dot the same as a bare domain', () => { + process.env.no_grpc_proxy = '.local.com'; + assert.strictEqual(usesProxy('dns:local.com:443'), false); + assert.strictEqual(usesProxy('dns:www.local.com:443'), false); + assert.strictEqual(usesProxy('dns:notlocal.com:443'), true); + }); + + it('Ignores whitespace and empty entries', () => { + process.env.no_grpc_proxy = ' local.com , ,apple.com,'; + assert.strictEqual(usesProxy('dns:www.local.com:443'), false); + assert.strictEqual(usesProxy('dns:apple.com:443'), false); + assert.strictEqual(usesProxy('dns:example.com:443'), true); + }); + + it('Matches IP addresses exactly, not by suffix', () => { + process.env.no_grpc_proxy = '0.0.1'; + assert.strictEqual(usesProxy('dns:10.0.0.1:443'), true); + process.env.no_grpc_proxy = '10.0.0.1'; + assert.strictEqual(usesProxy('dns:10.0.0.1:443'), false); + }); + + it('Still supports CIDR ranges', () => { + process.env.no_grpc_proxy = '192.168.0.0/16'; + assert.strictEqual(usesProxy('dns:192.168.10.20:443'), false); + assert.strictEqual(usesProxy('dns:192.169.10.20:443'), true); + }); + + it('Falls back to no_proxy when no_grpc_proxy is unset', () => { + process.env.no_proxy = 'local.com'; + assert.strictEqual(usesProxy('dns:www.local.com:443'), false); + assert.strictEqual(usesProxy('dns:notlocal.com:443'), true); + }); +});