fix(gateway): proxy passthrough via globalThis.fetch and fix NO_PROXY parsing

Replace setGlobalDispatcher with globalThis.fetch wrapper to work around
bundled undici instance isolation in ai-gateway subprocess. Also:

- Extract shared readEnvProxyUrl() to eliminate duplicated env proxy logic
- Fix IPv6 loopback detection (URL.hostname returns [::1] with brackets)
- Fix URL object handling in preload bypass check
- Fix *.domain wildcard and host:port NO_PROXY matching
- Fix inverted fail-safe: bypass on parse error instead of proxying
- Add writable guard for globalThis.fetch assignment
This commit is contained in:
810senpai114514 2026-07-03 00:20:26 +08:00
parent 0f54b7d7a3
commit 5f7345fdc8
2 changed files with 47 additions and 78 deletions

View file

@ -38,10 +38,18 @@ export async function fetchWithSystemProxy(input: RequestInfo | URL, init?: Requ
} as FetchInitWithDispatcher);
}
export function readEnvProxyUrl(): string | undefined {
const envProxy = process.env.HTTPS_PROXY || process.env.https_proxy ||
process.env.HTTP_PROXY || process.env.http_proxy;
return envProxy ? envProxy.trim() : undefined;
}
export async function getSystemProxyUrlForProtocol(protocol: "http" | "https" = "https"): Promise<string | undefined> {
const cache = await readSystemProxy();
const server = proxyServerForRequest(cache.upstreamProxy, protocol);
return server ? formatProxyUrl(server) : undefined;
if (server) return formatProxyUrl(server);
return readEnvProxyUrl();
}
async function systemProxyUrlForRequest(url: URL): Promise<string | undefined> {
@ -49,9 +57,7 @@ async function systemProxyUrlForRequest(url: URL): Promise<string | undefined> {
const server = proxyServerForRequest(cache.upstreamProxy, url.protocol === "https:" ? "https" : "http");
if (server) return formatProxyUrl(server);
const envProxy = process.env.HTTPS_PROXY || process.env.https_proxy ||
process.env.HTTP_PROXY || process.env.http_proxy;
return envProxy ? envProxy.trim() : undefined;
return readEnvProxyUrl();
}
async function readSystemProxy(): Promise<SystemProxyCache> {

View file

@ -6054,82 +6054,45 @@ function writeGatewayProxyPreloadFile(config: AppConfig, upstreamProxyUrl: strin
file,
[
"\"use strict\";",
"const proxyUrl = process.env.CCR_UPSTREAM_PROXY_URL;",
"if (proxyUrl) {",
" const undiciModule = process.env.CCR_UNDICI_MODULE || \"undici\";",
" const { Agent, ProxyAgent, setGlobalDispatcher } = require(undiciModule);",
" const directAgent = new Agent();",
" const proxyAgent = new ProxyAgent(proxyUrl);",
" const noProxyEntries = String(process.env.NO_PROXY || process.env.no_proxy || \"\")",
" .split(\",\")",
" .map((entry) => entry.trim())",
" .filter(Boolean);",
" function normalizeHost(host) {",
" return String(host || \"\").replace(/^\\[/, \"\").replace(/\\]$/, \"\").replace(/\\.$/, \"\").toLowerCase();",
" }",
" function isLoopbackHost(host) {",
" return host === \"localhost\" || host === \"::1\" || host === \"0:0:0:0:0:0:0:1\" || host === \"127.0.0.1\" || host.startsWith(\"127.\");",
" }",
" function splitNoProxyEntry(entry) {",
" let value = String(entry || \"\").trim().toLowerCase();",
" if (!value || value === \"*\") {",
" return { host: value, port: \"\" };",
" }",
" if (value.startsWith(\"[\")) {",
" const end = value.indexOf(\"]\");",
" if (end >= 0) {",
" const remainder = value.slice(end + 1);",
" return { host: normalizeHost(value.slice(1, end)), port: remainder.startsWith(\":\") ? remainder.slice(1) : \"\" };",
" }",
" }",
" const colon = value.lastIndexOf(\":\");",
" if (colon > 0 && value.indexOf(\":\") === colon) {",
" return { host: normalizeHost(value.slice(0, colon)), port: value.slice(colon + 1) };",
" }",
" return { host: normalizeHost(value), port: \"\" };",
" }",
" function noProxyMatches(host, port, entry) {",
" const parsed = splitNoProxyEntry(entry);",
" if (parsed.host === \"*\") {",
" return true;",
" }",
" if (!parsed.host || (parsed.port && parsed.port !== port)) {",
" return false;",
" }",
" if (parsed.host.startsWith(\"*.\")) {",
" const suffix = parsed.host.slice(1);",
" return host.endsWith(suffix);",
" }",
" if (parsed.host.startsWith(\".\")) {",
" return host === parsed.host.slice(1) || host.endsWith(parsed.host);",
" }",
" return host === parsed.host || host.endsWith(`.${parsed.host}`);",
" }",
" function shouldBypassProxy(origin) {",
"const up = process.env.CCR_UPSTREAM_PROXY_URL;",
"const um = process.env.CCR_UNDICI_MODULE;",
"if (up && um) {",
" const { ProxyAgent } = require(um);",
" const agent = new ProxyAgent(up);",
" const realFetch = globalThis.fetch.bind(globalThis);",
" const raw = (process.env.NO_PROXY || process.env.no_proxy || '').toLowerCase();",
" const byp = raw.split(',').map((s) => s.trim()).filter(Boolean);",
" const norm = (h) => h.replace(/^\\[/, '').replace(/\\]$/, '').replace(/\\.$/, '');",
" const isLP = (h) => h === 'localhost' || h === '127.0.0.1' || h === '::1' || h === '0:0:0:0:0:0:0:1' || h === '0.0.0.0' || h.startsWith('127.');",
" const shouldBypass = (input) => {",
" let h;",
" try {",
" const url = new URL(String(origin));",
" const host = normalizeHost(url.hostname);",
" const port = url.port || (url.protocol === \"https:\" ? \"443\" : url.protocol === \"http:\" ? \"80\" : \"\");",
" return isLoopbackHost(host) || noProxyEntries.some((entry) => noProxyMatches(host, port, entry));",
" } catch {",
" return false;",
" }",
" const u = typeof input === 'string' ? new URL(input) : input instanceof URL ? input : new URL(input && input.url ? input.url : String(input));",
" h = norm(u.hostname);",
" } catch { return true; }",
" if (!h) return false;",
" if (isLP(h)) return true;",
" return byp.some((p) => {",
" if (p === '*') return true;",
" const s = p.split(':');",
" const ph = norm(s[0]);",
" if (s.length === 2 && s[1]) {",
" if (h !== ph) return false;",
" try { return new URL(input).port === s[1]; } catch { return false; }",
" }",
" if (ph.startsWith('*.')) return h.endsWith(ph.slice(1));",
" if (ph.startsWith('.')) return h.endsWith(ph) || h === ph.slice(1);",
" return h === ph;",
" });",
" };",
" const patched = function(input, init) {",
" if (init && init.dispatcher) return realFetch(input, init);",
" if (shouldBypass(input)) return realFetch(input, init);",
" return realFetch(input, Object.assign({}, init, { dispatcher: agent }));",
" };",
" if (Object.getOwnPropertyDescriptor(globalThis, 'fetch')?.writable) {",
" globalThis.fetch = patched;",
" }",
" setGlobalDispatcher({",
" dispatch(options, handler) {",
" return shouldBypassProxy(options && options.origin)",
" ? directAgent.dispatch(options, handler)",
" : proxyAgent.dispatch(options, handler);",
" },",
" close(callback) {",
" Promise.all([directAgent.close(), proxyAgent.close()]).then(() => callback && callback(), callback);",
" },",
" destroy(error, callback) {",
" directAgent.destroy(error);",
" proxyAgent.destroy(error);",
" if (callback) callback();",
" }",
" });",
"}"
].join("\n"),
"utf8"