From e91bb8593dbff38394d988d99127deb50fca7e12 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Wed, 7 Jan 2026 21:00:20 +0000 Subject: [PATCH] feat: improve HTTP 402 error handling for all providers Fixes issue where providers like DeepSeek show "Unknown API error" instead of user-friendly messages for HTTP status codes like 402 (insufficient balance/payment required). Changes: - Modify handleProviderError to prefix error messages with HTTP status code (e.g., "402 - DeepSeek completion error: ...") when status >= 400 - This allows the UI (ChatRow.tsx) to parse the status code and display appropriate user-friendly messages from the localization files - Preserves existing behavior for custom messageTransformers and non-HTTP errors The UI already has localized messages for common HTTP status codes (400, 401, 402, 403, 429, 500) but they were not being shown because the error message format did not allow the UI to extract the status code. Closes #10016 --- .../utils/__tests__/error-handler.spec.ts | 112 +++++++++++++++++- src/api/providers/utils/error-handler.ts | 22 +++- 2 files changed, 127 insertions(+), 7 deletions(-) diff --git a/src/api/providers/utils/__tests__/error-handler.spec.ts b/src/api/providers/utils/__tests__/error-handler.spec.ts index 54971134df..c587d87c34 100644 --- a/src/api/providers/utils/__tests__/error-handler.spec.ts +++ b/src/api/providers/utils/__tests__/error-handler.spec.ts @@ -11,6 +11,8 @@ describe("handleProviderError", () => { const result = handleProviderError(error, providerName) expect(result).toBeInstanceOf(Error) + // Message should be prefixed with status code for UI parsing + expect(result.message).toMatch(/^401/) expect(result.message).toContain("TestProvider completion error") expect((result as any).status).toBe(401) }) @@ -257,6 +259,111 @@ describe("handleProviderError", () => { expect(result.message).toContain("Invalid API key") }) }) + + describe("HTTP status code prefixing for UI parsing", () => { + it("should prefix error message with status code 402 (Payment Required)", () => { + const error = new Error("Insufficient balance") as any + error.status = 402 + + const result = handleProviderError(error, "DeepSeek") + + // Message should start with status code for UI to parse + expect(result.message).toMatch(/^402/) + expect(result.message).toContain("DeepSeek completion error") + expect(result.message).toContain("Insufficient balance") + expect((result as any).status).toBe(402) + }) + + it("should prefix error message with status code 401 (Unauthorized)", () => { + const error = new Error("Invalid API key") as any + error.status = 401 + + const result = handleProviderError(error, "OpenAI") + + expect(result.message).toMatch(/^401/) + expect(result.message).toContain("OpenAI completion error") + expect((result as any).status).toBe(401) + }) + + it("should prefix error message with status code 403 (Forbidden)", () => { + const error = new Error("Access denied") as any + error.status = 403 + + const result = handleProviderError(error, "Anthropic") + + expect(result.message).toMatch(/^403/) + expect(result.message).toContain("Anthropic completion error") + expect((result as any).status).toBe(403) + }) + + it("should prefix error message with status code 429 (Rate Limit)", () => { + const error = new Error("Too many requests") as any + error.status = 429 + + const result = handleProviderError(error, "Mistral") + + expect(result.message).toMatch(/^429/) + expect(result.message).toContain("Mistral completion error") + expect((result as any).status).toBe(429) + }) + + it("should prefix error message with status code 500 (Server Error)", () => { + const error = new Error("Internal server error") as any + error.status = 500 + + const result = handleProviderError(error, "Groq") + + expect(result.message).toMatch(/^500/) + expect(result.message).toContain("Groq completion error") + expect((result as any).status).toBe(500) + }) + + it("should not prefix with status code when using custom messageTransformer", () => { + const error = new Error("Payment required") as any + error.status = 402 + + const result = handleProviderError(error, "TestProvider", { + messageTransformer: (msg) => `Custom: ${msg}`, + }) + + // Custom transformer should be used as-is + expect(result.message).toBe("Custom: Payment required") + // Status should still be preserved + expect((result as any).status).toBe(402) + }) + + it("should not prefix with status code for non-HTTP errors (no status)", () => { + const error = new Error("Network timeout") + + const result = handleProviderError(error, "TestProvider") + + // Should not have status prefix + expect(result.message).not.toMatch(/^\d{3}/) + expect(result.message).toBe("TestProvider completion error: Network timeout") + }) + + it("should not prefix with status code for status < 400", () => { + const error = new Error("Redirect") as any + error.status = 301 + + const result = handleProviderError(error, "TestProvider") + + // Should not have status prefix for non-error status codes + expect(result.message).not.toMatch(/^301/) + expect(result.message).toBe("TestProvider completion error: Redirect") + }) + + it("should work with custom messagePrefix option", () => { + const error = new Error("No funds") as any + error.status = 402 + + const result = handleProviderError(error, "DeepSeek", { messagePrefix: "streaming" }) + + expect(result.message).toMatch(/^402/) + expect(result.message).toContain("DeepSeek streaming error") + expect(result.message).toContain("No funds") + }) + }) }) describe("handleOpenAIError (backward compatibility)", () => { @@ -267,6 +374,8 @@ describe("handleOpenAIError (backward compatibility)", () => { const result = handleOpenAIError(error, "OpenAI") expect(result).toBeInstanceOf(Error) + // Should be prefixed with status code for UI parsing + expect(result.message).toMatch(/^500/) expect(result.message).toContain("OpenAI completion error") expect((result as any).status).toBe(500) }) @@ -277,7 +386,8 @@ describe("handleOpenAIError (backward compatibility)", () => { const result = handleOpenAIError(error, "Roo Code Cloud") - expect(result.message).toBe("Roo Code Cloud completion error: Authentication failed") + // Message should now include status prefix for UI parsing + expect(result.message).toBe("401 - Roo Code Cloud completion error: Authentication failed") expect((result as any).status).toBe(401) }) }) diff --git a/src/api/providers/utils/error-handler.ts b/src/api/providers/utils/error-handler.ts index 2c55b96f9c..7378a35057 100644 --- a/src/api/providers/utils/error-handler.ts +++ b/src/api/providers/utils/error-handler.ts @@ -49,13 +49,14 @@ export function handleProviderError( if (error instanceof Error) { const anyErr = error as any const msg = anyErr?.error?.metadata?.raw || error.message || "" + const status = anyErr.status // Log the original error details for debugging console.error(`[${providerName}] API error:`, { message: msg, name: error.name, stack: error.stack, - status: anyErr.status, + status, }) let wrapped: Error @@ -66,17 +67,26 @@ export function handleProviderError( wrapped = new Error(i18n.t("common:errors.api.invalidKeyInvalidChars")) } else { // Apply custom transformer if provided, otherwise use default format - const finalMessage = options?.messageTransformer - ? options.messageTransformer(msg) - : `${providerName} ${messagePrefix} error: ${msg}` + // IMPORTANT: When HTTP status is available, prefix the message with the status code + // This allows the UI (ChatRow.tsx) to parse the status and show user-friendly error messages + // for known status codes (e.g., 402 = insufficient balance, 429 = rate limit) + let finalMessage: string + if (options?.messageTransformer) { + finalMessage = options.messageTransformer(msg) + } else if (typeof status === "number" && status >= 400) { + // Prefix with status code for UI parsing (e.g., "402 - DeepSeek completion error: ...") + finalMessage = `${status} - ${providerName} ${messagePrefix} error: ${msg}` + } else { + finalMessage = `${providerName} ${messagePrefix} error: ${msg}` + } wrapped = new Error(finalMessage) } // Preserve HTTP status and structured details for retry/backoff + UI // These fields are used by Task.backoffAndAnnounce() and ChatRow/ErrorRow // to provide status-aware error messages and handling - if (anyErr.status !== undefined) { - ;(wrapped as any).status = anyErr.status + if (status !== undefined) { + ;(wrapped as any).status = status } if (anyErr.errorDetails !== undefined) { ;(wrapped as any).errorDetails = anyErr.errorDetails