From 40f811eb7be8b82e723552be628276a249db1e51 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Wed, 17 Jun 2026 14:57:03 -0300 Subject: [PATCH] fix(sse): friendly 413 message for ChatGPT web payload-too-large (#4080) Integrated into release/v3.8.28 --- open-sse/executors/chatgpt-web.ts | 10 ++------ open-sse/executors/chatgptWebErrors.ts | 18 ++++++++++++++ tests/unit/chatgpt-web.test.ts | 34 ++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 8 deletions(-) create mode 100644 open-sse/executors/chatgptWebErrors.ts diff --git a/open-sse/executors/chatgpt-web.ts b/open-sse/executors/chatgpt-web.ts index 33fbfae99c..f269ee14d1 100644 --- a/open-sse/executors/chatgpt-web.ts +++ b/open-sse/executors/chatgpt-web.ts @@ -16,6 +16,7 @@ */ import { BaseExecutor, type ExecuteInput, type ProviderCredentials } from "./base.ts"; +import { describeChatGptWebHttpError } from "./chatgptWebErrors.ts"; import { createHash, randomUUID, randomBytes } from "node:crypto"; import { tlsFetchChatGpt, @@ -2742,16 +2743,9 @@ export class ChatGptWebExecutor extends BaseExecutor { // upstream message is much more useful than our wrapper. Goes through // the executor logger so it respects the application's log config. log?.warn?.("CGPT-WEB", `conv ${status}: ${(response.text || "").slice(0, 400)}`); - let errMsg = `ChatGPT returned HTTP ${status}`; + const errMsg = describeChatGptWebHttpError(status); if (status === 401 || status === 403) { - errMsg = - "ChatGPT auth failed — session may have expired. Re-paste your __Secure-next-auth.session-token."; tokenCache.delete(cookieKey(cookie)); - } else if (status === 404) { - errMsg = - "ChatGPT returned 404 — usually the model is no longer available on this account or the chat-requirements-token expired. Retry will start a fresh conversation."; - } else if (status === 429) { - errMsg = "ChatGPT rate limited. Wait a moment and retry."; } log?.warn?.("CGPT-WEB", errMsg); return { diff --git a/open-sse/executors/chatgptWebErrors.ts b/open-sse/executors/chatgptWebErrors.ts new file mode 100644 index 0000000000..b2dfd12036 --- /dev/null +++ b/open-sse/executors/chatgptWebErrors.ts @@ -0,0 +1,18 @@ +/** + * User-facing messages for upstream ChatGPT-web HTTP error statuses. + * + * Pure mapping with no side effects so it can be unit-tested in isolation — the + * caller owns any state mutation (e.g. clearing the token cache on 401/403). + * Unmapped statuses fall back to the generic `ChatGPT returned HTTP `. + */ +const CGPT_WEB_HTTP_ERROR_MESSAGES: Record = { + 401: "ChatGPT auth failed — session may have expired. Re-paste your __Secure-next-auth.session-token.", + 403: "ChatGPT auth failed — session may have expired. Re-paste your __Secure-next-auth.session-token.", + 404: "ChatGPT returned 404 — usually the model is no longer available on this account or the chat-requirements-token expired. Retry will start a fresh conversation.", + 413: "ChatGPT returned 413 — the request payload is too large for ChatGPT web's size limit (often hit by agentic clients like Cline/Kilo that send big system prompts and file context). Reduce the context: enable compression, trim the conversation/files, or use a smaller request.", + 429: "ChatGPT rate limited. Wait a moment and retry.", +}; + +export function describeChatGptWebHttpError(status: number): string { + return CGPT_WEB_HTTP_ERROR_MESSAGES[status] ?? `ChatGPT returned HTTP ${status}`; +} diff --git a/tests/unit/chatgpt-web.test.ts b/tests/unit/chatgpt-web.test.ts index de24971381..b1766c7069 100644 --- a/tests/unit/chatgpt-web.test.ts +++ b/tests/unit/chatgpt-web.test.ts @@ -3,6 +3,9 @@ import assert from "node:assert/strict"; const { ChatGptWebExecutor, __derivePublicBaseUrlForTesting, __resetChatGptWebCachesForTesting } = await import("../../open-sse/executors/chatgpt-web.ts"); +const { describeChatGptWebHttpError } = await import( + "../../open-sse/executors/chatgptWebErrors.ts" +); const { getExecutor, hasSpecializedExecutor } = await import("../../open-sse/executors/index.ts"); const { __setTlsFetchOverrideForTesting, looksLikeSse, TlsClientUnavailableError } = await import("../../open-sse/services/chatgptTlsClient.ts"); @@ -2772,3 +2775,34 @@ test("Image cache: deleting an entry decrements the byte counter", async () => { "bytes credited back on TTL evict" ); }); + +// ─── describeChatGptWebHttpError ───────────────────────────────────────────── + +test("describeChatGptWebHttpError maps 413 to a payload-too-large message with guidance", () => { + const msg = describeChatGptWebHttpError(413); + // Must NOT be the cryptic generic — should explain it's a size limit and how to recover. + assert.notEqual( + msg, + "ChatGPT returned HTTP 413", + "413 should get a tailored message, not the generic fallback" + ); + assert.match(msg, /413/, "message keeps the status code"); + assert.match(msg, /too large|payload|size limit/i, "message explains it's a size/payload limit"); + assert.match( + msg, + /context|compress/i, + "message points the user at reducing context / compression" + ); +}); + +test("describeChatGptWebHttpError preserves the existing 401/403/404/429 mappings", () => { + assert.match(describeChatGptWebHttpError(401), /session may have expired/i); + assert.match(describeChatGptWebHttpError(403), /session may have expired/i); + assert.match(describeChatGptWebHttpError(404), /no longer available|fresh conversation/i); + assert.match(describeChatGptWebHttpError(429), /rate limited/i); +}); + +test("describeChatGptWebHttpError falls back to the generic message for unmapped statuses", () => { + assert.equal(describeChatGptWebHttpError(500), "ChatGPT returned HTTP 500"); + assert.equal(describeChatGptWebHttpError(502), "ChatGPT returned HTTP 502"); +});