fix(proxy): auto-fallback to insecure TLS on self-signed cert errors

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
decoluaandClaude Code committed 2026-09-28 18:51:31 +07:00
1 parent aafe300226
commit b58bd80406
1 file changed
+48 -12
+48 -12
View File
@@ -99,6 +99,20 @@ async function tryGotScrapingFetch(url, options) {
// DNS cache — use Map to avoid prototype pollution via malformed hostnames
const DNS_CACHE = new Map();
const TLS_CERT_ERRORS = new Set([
"SELF_SIGNED_CERT_IN_CHAIN",
"DEPTH_ZERO_SELF_SIGNED_CERT",
"UNABLE_TO_VERIFY_LEAF_SIGNATURE",
"UNABLE_TO_GET_ISSUER_CERT",
"UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
"CERT_HAS_EXPIRED",
"ERR_TLS_CERT_ALTNAME_INVALID",
]);
function isTlsCertError(err) {
const code = err?.cause?.code || err?.code;
return TLS_CERT_ERRORS.has(code);
}
const MITM_BYPASS_HOSTS = [
"cloudcode-pa.googleapis.com",
"daily-cloudcode-pa.googleapis.com",
@@ -216,20 +230,44 @@ function resolveConnectionProxyUrl(targetUrl, proxyOptions) {
/**
* Create proxy dispatcher lazily (undici-compatible)
*/
async function getDispatcher(proxyUrl) {
async function getDispatcher(proxyUrl, insecure = false) {
const normalized = normalizeProxyUrl(proxyUrl);
if (!normalized) return null;
if (!normalized && !insecure) return null;
if (!proxyDispatchers.has(normalized)) {
const key = `${normalized || "direct"}::${insecure ? "insecure" : "secure"}`;
if (!proxyDispatchers.has(key)) {
// Evict oldest entry if max size reached
if (proxyDispatchers.size >= MEMORY_CONFIG.proxyDispatchersMaxSize) {
proxyDispatchers.delete(proxyDispatchers.keys().next().value);
}
const { ProxyAgent } = await import("undici");
proxyDispatchers.set(normalized, new ProxyAgent({ uri: normalized }));
const { Agent, ProxyAgent } = await import("undici");
const connect = insecure ? { rejectUnauthorized: false } : undefined;
const dispatcher = normalized
? new ProxyAgent({ uri: normalized, ...(insecure ? { requestTls: connect } : {}) })
: new Agent({ connect });
proxyDispatchers.set(key, dispatcher);
}
return proxyDispatchers.get(normalized);
return proxyDispatchers.get(key);
}
async function fetchWithTlsFallback(url, options, proxyUrl) {
try {
const dispatcher = proxyUrl ? await getDispatcher(proxyUrl) : undefined;
return await originalFetch(url, dispatcher ? { ...options, dispatcher } : options);
} catch (err) {
const isStrictSsl = process.env.STRICT_SSL === "true" || process.env.STRICT_SSL === "1";
if (!isStrictSsl && isTlsCertError(err)) {
if (options.body && typeof options.body.getReader === "function" && options.body.locked) {
throw err;
}
// ponytail: in-memory insecure agent fallback for self-signed MITM corporate/antivirus certs
console.warn(`[ProxyFetch] TLS cert verification failed (${err.cause?.code || err.code}), retrying with insecure TLS: ${url}`);
const insecureDispatcher = await getDispatcher(proxyUrl, true);
return await originalFetch(url, { ...options, dispatcher: insecureDispatcher });
}
throw err;
}
}
/**
@@ -318,8 +356,7 @@ export async function proxyAwareFetch(url, options = {}, proxyOptions = null) {
if (proxyUrl) {
// Proxy resolves DNS externally (not affected by /etc/hosts) — use proxy directly
try {
const dispatcher = await getDispatcher(proxyUrl);
return await originalFetch(url, { ...options, dispatcher });
return await fetchWithTlsFallback(url, options, proxyUrl);
} catch (proxyError) {
if (proxyOptions?.strictProxy === true) {
throw new Error(`[ProxyFetch] Proxy required but failed (strictProxy=true): ${proxyError.message}`);
@@ -339,15 +376,14 @@ export async function proxyAwareFetch(url, options = {}, proxyOptions = null) {
if (proxyUrl) {
try {
const dispatcher = await getDispatcher(proxyUrl);
return await originalFetch(url, { ...options, dispatcher });
return await fetchWithTlsFallback(url, options, proxyUrl);
} catch (proxyError) {
// If strictProxy is enabled, fail hard instead of falling back to direct
if (proxyOptions?.strictProxy === true) {
throw new Error(`[ProxyFetch] Proxy required but failed (strictProxy=true): ${proxyError.message}`);
}
console.warn(`[ProxyFetch] Proxy failed, falling back to direct: ${proxyError.message}`);
return originalFetch(url, options);
return fetchWithTlsFallback(url, options, null);
}
}
@@ -371,7 +407,7 @@ export async function proxyAwareFetch(url, options = {}, proxyOptions = null) {
// got-scraping disabled — use native fetch directly
// (Re-enable per-host by wrapping with tryGotScrapingFetch when needed)
return originalFetch(url, options);
return fetchWithTlsFallback(url, options, null);
}
/**