Files
OmniRoute/open-sse/services/accountFallback/nonRetryableUpstream.ts
AmirHossein Rezaei 4d66dd113f fix(resilience): honor Cloudflare 1010 retryable:false — skip COOLDOWN_RETRY (#8775) (#8800)
Api-key 403 bodies with Cloudflare error 1010 / browser_signature_banned /
retryable:false were treated as short AUTH_ERROR cooldowns, so the chat loop
waited ~21–33s before falling through. Return cooldownMs:0 so the tier fails
fast without permanently banning the account.
2026-07-27 22:37:51 -03:00

101 lines
3.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* #8775 — Detect Cloudflare (and similar edge) errors that explicitly forbid
* retries. When these bodies are misclassified as short AUTH_ERROR cooldowns,
* OmniRoute waits 3× for a permanent client-signature ban and burns 2133s
* before falling through to the next combo tier.
*
* Kept as a pure helper so checkFallbackError stays within its file-size freeze
* and every branch is unit-testable without DB/auth wiring.
*/
function tryParseJsonObject(text: string): Record<string, unknown> | null {
const trimmed = text.trim();
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return null;
try {
const parsed: unknown = JSON.parse(trimmed);
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
return parsed as Record<string, unknown>;
}
} catch {
// Body may be truncated HTML/JSON hybrids — fall through to string probes.
}
return null;
}
function hasCloudflareBanSignal(value: unknown): boolean {
if (typeof value === "number") return value === 1010;
if (typeof value !== "string") return false;
const lower = value.toLowerCase();
return (
lower.includes("browser_signature_banned") ||
lower.includes("error 1010") ||
lower === "1010" ||
lower.includes("cloudflare")
);
}
/**
* Returns true when upstream error text says the failure must not be retried —
* Cloudflare error 1010 / browser_signature_banned, or an explicit
* `retryable: false` paired with Cloudflare / owner_action_required signals.
*/
export function isNonRetryableCloudflareError(errorText: string | null | undefined): boolean {
if (typeof errorText !== "string" || errorText.length === 0) return false;
const obj = tryParseJsonObject(errorText);
if (obj) {
const retryable = obj.retryable;
const errorCode = obj.error_code ?? obj.errorCode;
const errorName = typeof obj.error_name === "string" ? obj.error_name : "";
const cloudflareError = obj.cloudflare_error === true || obj.cloudflareError === true;
const ownerActionRequired =
obj.owner_action_required === true || obj.ownerActionRequired === true;
if (errorCode === 1010 || errorCode === "1010") return true;
if (errorName.toLowerCase() === "browser_signature_banned") return true;
if (retryable === false && (cloudflareError || ownerActionRequired)) return true;
if (retryable === false && hasCloudflareBanSignal(obj.title ?? obj.type ?? obj.detail)) {
return true;
}
}
// Non-JSON / nested-string fallbacks (logs often stringify the body).
if (/browser_signature_banned/i.test(errorText)) return true;
if (/"error_code"\s*:\s*1010\b/.test(errorText)) return true;
if (
/"retryable"\s*:\s*false/.test(errorText) &&
(/"cloudflare_error"\s*:\s*true/.test(errorText) ||
/"owner_action_required"\s*:\s*true/.test(errorText) ||
/cloudflare/i.test(errorText))
) {
return true;
}
return false;
}
type ApiKeyForbiddenFallback = {
shouldFallback: boolean;
cooldownMs: number;
reason: string;
baseCooldownMs?: number;
newBackoffLevel?: number;
usedUpstreamRetryHint?: boolean;
};
/**
* Api-key 403 branch helper (#8775): CF 1010 / retryable:false gets cooldown 0
* so COOLDOWN_RETRY does not wait; other 403s keep the short retryable cooldown.
*/
export function resolveApiKeyForbiddenFallback(
errorStr: string,
buildRetryableFallback: (reason: string) => ApiKeyForbiddenFallback,
authErrorReason: string
): ApiKeyForbiddenFallback {
if (isNonRetryableCloudflareError(errorStr)) {
return { shouldFallback: true, cooldownMs: 0, reason: authErrorReason };
}
return buildRetryableFallback(authErrorReason);
}