2026-01-20 00:09:14 -05:00
|
|
|
const isCloud = typeof caches !== "undefined" && typeof caches === "object";
|
|
|
|
|
|
|
|
|
|
const originalFetch = globalThis.fetch;
|
2026-02-27 22:11:53 -05:00
|
|
|
let proxyDispatcher = null;
|
|
|
|
|
let proxyDispatcherUrl = null;
|
2026-01-20 00:09:14 -05:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get proxy URL from environment
|
|
|
|
|
*/
|
|
|
|
|
function getProxyUrl(targetUrl) {
|
|
|
|
|
const noProxy = process.env.NO_PROXY || process.env.no_proxy;
|
|
|
|
|
|
|
|
|
|
if (noProxy) {
|
|
|
|
|
const hostname = new URL(targetUrl).hostname.toLowerCase();
|
|
|
|
|
const patterns = noProxy.split(",").map(p => p.trim().toLowerCase());
|
|
|
|
|
|
|
|
|
|
const shouldBypass = patterns.some(pattern => {
|
|
|
|
|
if (pattern === "*") return true;
|
|
|
|
|
if (pattern.startsWith(".")) return hostname.endsWith(pattern) || hostname === pattern.slice(1);
|
|
|
|
|
return hostname === pattern || hostname.endsWith(`.${pattern}`);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (shouldBypass) return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const protocol = new URL(targetUrl).protocol;
|
|
|
|
|
|
|
|
|
|
if (protocol === "https:") {
|
|
|
|
|
return process.env.HTTPS_PROXY || process.env.https_proxy ||
|
|
|
|
|
process.env.ALL_PROXY || process.env.all_proxy;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return process.env.HTTP_PROXY || process.env.http_proxy ||
|
|
|
|
|
process.env.ALL_PROXY || process.env.all_proxy;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-27 22:11:53 -05:00
|
|
|
* Normalize proxy URL (allow host:port)
|
2026-01-20 00:09:14 -05:00
|
|
|
*/
|
2026-02-27 22:11:53 -05:00
|
|
|
function normalizeProxyUrl(proxyUrl) {
|
|
|
|
|
if (!proxyUrl) return null;
|
|
|
|
|
try {
|
|
|
|
|
// eslint-disable-next-line no-new
|
|
|
|
|
new URL(proxyUrl);
|
|
|
|
|
return proxyUrl;
|
|
|
|
|
} catch {
|
|
|
|
|
// Allow "127.0.0.1:7890" style values
|
|
|
|
|
return `http://${proxyUrl}`;
|
2026-01-20 00:09:14 -05:00
|
|
|
}
|
2026-02-27 22:11:53 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create proxy dispatcher lazily (undici-compatible)
|
|
|
|
|
* Closes old dispatcher when proxy URL changes to prevent connection pool leak
|
|
|
|
|
*/
|
|
|
|
|
async function getDispatcher(proxyUrl) {
|
|
|
|
|
const normalized = normalizeProxyUrl(proxyUrl);
|
|
|
|
|
if (!normalized) return null;
|
|
|
|
|
|
|
|
|
|
if (!proxyDispatcher || proxyDispatcherUrl !== normalized) {
|
|
|
|
|
try { proxyDispatcher?.close?.(); } catch { /* ignore */ }
|
|
|
|
|
const { ProxyAgent } = await import("undici");
|
|
|
|
|
proxyDispatcher = new ProxyAgent({ uri: normalized });
|
|
|
|
|
proxyDispatcherUrl = normalized;
|
2026-01-20 00:09:14 -05:00
|
|
|
}
|
2026-02-27 22:11:53 -05:00
|
|
|
|
|
|
|
|
return proxyDispatcher;
|
2026-01-20 00:09:14 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Patched fetch with proxy support and fallback to direct connection
|
|
|
|
|
*/
|
|
|
|
|
async function patchedFetch(url, options = {}) {
|
|
|
|
|
const targetUrl = typeof url === "string" ? url : url.toString();
|
2026-02-27 22:11:53 -05:00
|
|
|
const proxyUrl = normalizeProxyUrl(getProxyUrl(targetUrl));
|
2026-01-20 00:09:14 -05:00
|
|
|
|
|
|
|
|
if (proxyUrl) {
|
|
|
|
|
try {
|
2026-02-27 22:11:53 -05:00
|
|
|
const dispatcher = await getDispatcher(proxyUrl);
|
|
|
|
|
return await originalFetch(url, { ...options, dispatcher });
|
2026-01-20 00:09:14 -05:00
|
|
|
} catch (proxyError) {
|
|
|
|
|
// Fallback to direct connection if proxy fails
|
|
|
|
|
console.warn(`[ProxyFetch] Proxy failed, falling back to direct: ${proxyError.message}`);
|
|
|
|
|
return originalFetch(url, options);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return originalFetch(url, options);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-27 22:11:53 -05:00
|
|
|
// Idempotency guard — only patch once to avoid wrapping multiple times
|
|
|
|
|
if (!isCloud && globalThis.fetch !== patchedFetch) {
|
2026-01-20 00:09:14 -05:00
|
|
|
globalThis.fetch = patchedFetch;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default isCloud ? originalFetch : patchedFetch;
|