From 790a17ac3395d4cdfd07e2a4adcb0cb05e163171 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Fri, 7 Aug 2026 10:47:57 -0300 Subject: [PATCH] fix(errorClassifier): classify ChatGPT Web SENTINEL_BLOCKED 403 as FORBIDDEN, enabling combo fallback (#8813) --- changelog.d/fixes/8813-chatgpt-sentinel.md | 1 + open-sse/services/errorClassifier.ts | 14 ++++++++++ .../errorClassifier-noauth-403-6315.test.ts | 27 +++++++++++++++++++ 3 files changed, 42 insertions(+) create mode 100644 changelog.d/fixes/8813-chatgpt-sentinel.md diff --git a/changelog.d/fixes/8813-chatgpt-sentinel.md b/changelog.d/fixes/8813-chatgpt-sentinel.md new file mode 100644 index 0000000000..0cd1091c5e --- /dev/null +++ b/changelog.d/fixes/8813-chatgpt-sentinel.md @@ -0,0 +1 @@ +- fix(errorClassifier): classify ChatGPT Web SENTINEL_BLOCKED 403 as terminal FORBIDDEN, enabling proper combo fallback (#8813) diff --git a/open-sse/services/errorClassifier.ts b/open-sse/services/errorClassifier.ts index 7f1ff1dece..71bc81f5eb 100644 --- a/open-sse/services/errorClassifier.ts +++ b/open-sse/services/errorClassifier.ts @@ -245,6 +245,20 @@ export function classifyProviderError( if (recoverableProject403) { return PROVIDER_ERROR_TYPES.PROJECT_ROUTE_ERROR; } + // #8813 — ChatGPT Web's Cloudflare Sentinel/Turnstile 403 is a TERMINAL + // block: the user's IP/session needs a browser Turnstile challenge, and + // retrying the same connection will keep 403ing. Classify as FORBIDDEN so + // the connection gets banned and combo routing falls back to other providers. + // Must be checked BEFORE the generic apikey-403→null return below, which + // is designed for normal API-key auth 403s that ARE recoverable. + if ( + bodyStr.includes("SENTINEL_BLOCKED") || + /\bSentinel\b[^\n]{0,80}\bblocked\b/i.test(bodyStr) || + /\bTurnstile required\b/i.test(bodyStr) + ) { + return PROVIDER_ERROR_TYPES.FORBIDDEN; + } + if (provider && getProviderCategory(provider) === "apikey") { return null; } diff --git a/tests/unit/errorClassifier-noauth-403-6315.test.ts b/tests/unit/errorClassifier-noauth-403-6315.test.ts index 1c9ede34f7..6d02d8c1a7 100644 --- a/tests/unit/errorClassifier-noauth-403-6315.test.ts +++ b/tests/unit/errorClassifier-noauth-403-6315.test.ts @@ -26,6 +26,33 @@ test("control: apikey-provider bare 403 still recoverable (null) — no regressi assert.equal(classifyProviderError(403, "forbidden", "openai"), null); }); +test("#8813: chatgpt-web SENTINEL_BLOCKED 403 with 'Sentinel/Turnstile required' → FORBIDDEN (terminal)", () => { + // The executor returns error code "SENTINEL_BLOCKED" in the JSON body. + // This is a TERMINAL state — retrying the same blocked session will keep 403ing. + // Must be classified as FORBIDDEN so the circuit breaker marks the connection + // as banned and combo routing falls back to other providers. + const body = JSON.stringify({ + error: { + message: + "ChatGPT blocked the request (Sentinel/Turnstile required). Try again later or open chatgpt.com in a browser to refresh state.", + type: "upstream_error", + code: "SENTINEL_BLOCKED", + }, + }); + assert.equal( + classifyProviderError(403, body, "chatgpt-web"), + PROVIDER_ERROR_TYPES.FORBIDDEN + ); +}); + +test("#8813: chatgpt-web SENTINEL_BLOCKED 403 with raw 'Sentinel blocked' text → FORBIDDEN (terminal)", () => { + // Edge case: when the raw text includes "Sentinel" and 403, classify as terminal. + assert.equal( + classifyProviderError(403, "Sentinel blocked the request", "chatgpt-web"), + PROVIDER_ERROR_TYPES.FORBIDDEN + ); +}); + test("control: recognized ban phrase on a no-credential provider still terminal (ACCOUNT_DEACTIVATED)", () => { const body = "This service has been disabled in this account for violation of policy."; assert.equal(classifyProviderError(403, body, "mimocode"), PROVIDER_ERROR_TYPES.ACCOUNT_DEACTIVATED);