fix: honor ipv6 no_proxy entries

This commit is contained in:
Peter Steinberger
2026-05-28 12:50:55 -04:00
parent 53475c21b8
commit e205888fa7
2 changed files with 81 additions and 6 deletions

View File

@@ -0,0 +1,42 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { resolveHttpProxyUrlForTarget } from "./node-http-proxy.js";
function clearProxyEnv(): void {
for (const key of [
"http_proxy",
"HTTP_PROXY",
"https_proxy",
"HTTPS_PROXY",
"all_proxy",
"ALL_PROXY",
"no_proxy",
"NO_PROXY",
]) {
vi.stubEnv(key, "");
}
}
describe("node HTTP proxy resolution", () => {
afterEach(() => {
vi.unstubAllEnvs();
});
it("honors unbracketed IPv6 literals in NO_PROXY", () => {
clearProxyEnv();
vi.stubEnv("HTTP_PROXY", "http://proxy.example:8080");
vi.stubEnv("NO_PROXY", "::1");
expect(resolveHttpProxyUrlForTarget("http://[::1]:11434/v1")).toBeUndefined();
});
it("honors bracketed IPv6 literals with matching NO_PROXY ports", () => {
clearProxyEnv();
vi.stubEnv("HTTP_PROXY", "http://proxy.example:8080");
vi.stubEnv("NO_PROXY", "[::1]:11434");
expect(resolveHttpProxyUrlForTarget("http://[::1]:11434/v1")).toBeUndefined();
expect(resolveHttpProxyUrlForTarget("http://[::1]:11435/v1")?.href).toBe(
"http://proxy.example:8080/",
);
});
});

View File

@@ -36,7 +36,40 @@ function parseProxyTargetUrl(targetUrl: string | URL): URL | undefined {
}
}
function normalizeProxyHostname(hostname: string): string {
let normalized = hostname.toLowerCase();
if (normalized.startsWith("[") && normalized.endsWith("]")) {
normalized = normalized.slice(1, -1);
}
return normalized.endsWith(".") ? normalized.slice(0, -1) : normalized;
}
function parseNoProxyEntry(entry: string): { hostname: string; port: number } {
const bracketed = entry.match(/^\[([^\]]+)\](?::(\d+))?$/);
if (bracketed) {
return {
hostname: normalizeProxyHostname(bracketed[1] ?? ""),
port: bracketed[2] ? Number.parseInt(bracketed[2], 10) : 0,
};
}
const firstColon = entry.indexOf(":");
const lastColon = entry.lastIndexOf(":");
if (firstColon > -1 && firstColon === lastColon) {
const portRaw = entry.slice(lastColon + 1);
if (/^\d+$/.test(portRaw)) {
return {
hostname: normalizeProxyHostname(entry.slice(0, lastColon)),
port: Number.parseInt(portRaw, 10),
};
}
}
return { hostname: normalizeProxyHostname(entry), port: 0 };
}
function shouldProxyHostname(hostname: string, port: number): boolean {
const normalizedHostname = normalizeProxyHostname(hostname);
const noProxy = getProxyEnv("no_proxy").toLowerCase();
if (!noProxy) {
return true;
@@ -50,21 +83,21 @@ function shouldProxyHostname(hostname: string, port: number): boolean {
return true;
}
const parsedProxy = proxy.match(/^(.+):(\d+)$/);
let proxyHostname = parsedProxy ? parsedProxy[1] : proxy;
const proxyPort = parsedProxy ? Number.parseInt(parsedProxy[2], 10) : 0;
const parsedProxy = parseNoProxyEntry(proxy);
let proxyHostname = parsedProxy.hostname;
const proxyPort = parsedProxy.port;
if (proxyPort && proxyPort !== port) {
return true;
}
if (!/^[.*]/.test(proxyHostname)) {
return hostname !== proxyHostname;
return normalizedHostname !== proxyHostname;
}
if (proxyHostname.startsWith("*")) {
proxyHostname = proxyHostname.slice(1);
}
return !hostname.endsWith(proxyHostname);
return !normalizedHostname.endsWith(proxyHostname);
});
}
@@ -75,7 +108,7 @@ function getProxyForUrl(targetUrl: string | URL): string {
}
const protocol = parsedUrl.protocol.split(":", 1)[0];
const hostname = parsedUrl.host.replace(/:\d*$/, "");
const hostname = parsedUrl.hostname;
const port = Number.parseInt(parsedUrl.port, 10) || DEFAULT_PROXY_PORTS[protocol] || 0;
if (!shouldProxyHostname(hostname, port)) {
return "";