From 2496d77689ee72426556bbe253b6a926409befcb Mon Sep 17 00:00:00 2001 From: Roo Code Date: Sat, 20 Dec 2025 15:05:05 +0000 Subject: [PATCH] fix: attach HTTP status codes to Cerebras API errors for proper UI display Resolves issue where Cerebras API errors showed "Unknown API error" instead of the actual error message. The Cerebras provider was throwing Error objects without attaching the HTTP status code, which is required by ChatRow to display appropriate error messages. The frontend parses the status code from error objects to show user-friendly messages based on HTTP status (401, 429, 500, etc.). Changes: - Add throwWithStatus helper to create errors with status property attached - Update all error throwing locations to use throwWithStatus - Preserve status when re-wrapping errors in outer catch blocks - Add tests to verify status is attached to errors Fixes #10212 --- src/api/providers/__tests__/cerebras.spec.ts | 53 ++++++++++++++++++ src/api/providers/cerebras.ts | 56 +++++++++++++++----- 2 files changed, 95 insertions(+), 14 deletions(-) diff --git a/src/api/providers/__tests__/cerebras.spec.ts b/src/api/providers/__tests__/cerebras.spec.ts index 936bd4fd3f..b835f55c76 100644 --- a/src/api/providers/__tests__/cerebras.spec.ts +++ b/src/api/providers/__tests__/cerebras.spec.ts @@ -123,6 +123,59 @@ describe("CerebrasHandler", () => { await expect(generator.next()).rejects.toThrow() }) + it("should attach HTTP status code to error objects", async () => { + const mockErrorResponse = { + ok: false, + status: 401, + text: () => Promise.resolve('{"error": {"message": "Unauthorized"}}'), + } + vi.mocked(fetch).mockResolvedValueOnce(mockErrorResponse as any) + + const generator = handler.createMessage("System prompt", []) + try { + await generator.next() + // Should not reach here + expect(true).toBe(false) + } catch (error: any) { + // The outer catch wraps the error, but status should be preserved + expect(error.status).toBe(401) + } + }) + + it("should attach HTTP status code for rate limit errors", async () => { + const mockErrorResponse = { + ok: false, + status: 429, + text: () => Promise.resolve('{"error": {"message": "Rate limit exceeded"}}'), + } + vi.mocked(fetch).mockResolvedValueOnce(mockErrorResponse as any) + + const generator = handler.createMessage("System prompt", []) + try { + await generator.next() + expect(true).toBe(false) + } catch (error: any) { + expect(error.status).toBe(429) + } + }) + + it("should attach HTTP status code for server errors", async () => { + const mockErrorResponse = { + ok: false, + status: 500, + text: () => Promise.resolve('{"error": {"message": "Internal server error"}}'), + } + vi.mocked(fetch).mockResolvedValueOnce(mockErrorResponse as any) + + const generator = handler.createMessage("System prompt", []) + try { + await generator.next() + expect(true).toBe(false) + } catch (error: any) { + expect(error.status).toBe(500) + } + }) + it("should parse streaming responses correctly", async () => { // Test streaming response parsing // Mock ReadableStream with various data chunks diff --git a/src/api/providers/cerebras.ts b/src/api/providers/cerebras.ts index 99e7c4cc3d..2ff4138c83 100644 --- a/src/api/providers/cerebras.ts +++ b/src/api/providers/cerebras.ts @@ -13,6 +13,16 @@ import { BaseProvider } from "./base-provider" import { DEFAULT_HEADERS } from "./constants" import { t } from "../../i18n" +/** + * Creates an Error with an HTTP status code attached for proper UI error handling. + * The status property is used by ChatRow to display appropriate error messages. + */ +function throwWithStatus(message: string, status: number): never { + const error = new Error(message) + ;(error as any).status = status + throw error +} + const CEREBRAS_BASE_URL = "https://api.cerebras.ai/v1" const CEREBRAS_DEFAULT_TEMPERATURE = 0 @@ -150,18 +160,22 @@ export class CerebrasHandler extends BaseProvider implements SingleCompletionHan errorMessage = errorText || `HTTP ${response.status}` } - // Provide more actionable error messages + // Provide more actionable error messages with HTTP status attached if (response.status === 401) { - throw new Error(t("common:errors.cerebras.authenticationFailed")) + throwWithStatus(t("common:errors.cerebras.authenticationFailed"), response.status) } else if (response.status === 403) { - throw new Error(t("common:errors.cerebras.accessForbidden")) + throwWithStatus(t("common:errors.cerebras.accessForbidden"), response.status) } else if (response.status === 429) { - throw new Error(t("common:errors.cerebras.rateLimitExceeded")) + throwWithStatus(t("common:errors.cerebras.rateLimitExceeded"), response.status) } else if (response.status >= 500) { - throw new Error(t("common:errors.cerebras.serverError", { status: response.status })) + throwWithStatus( + t("common:errors.cerebras.serverError", { status: response.status }), + response.status, + ) } else { - throw new Error( + throwWithStatus( t("common:errors.cerebras.genericError", { status: response.status, message: errorMessage }), + response.status, ) } } @@ -273,7 +287,12 @@ export class CerebrasHandler extends BaseProvider implements SingleCompletionHan } } catch (error) { if (error instanceof Error) { - throw new Error(t("common:errors.cerebras.completionError", { error: error.message })) + // Preserve HTTP status code if present on the original error + const wrappedError = new Error(t("common:errors.cerebras.completionError", { error: error.message })) + if ((error as any).status !== undefined) { + ;(wrappedError as any).status = (error as any).status + } + throw wrappedError } throw error } @@ -304,18 +323,22 @@ export class CerebrasHandler extends BaseProvider implements SingleCompletionHan if (!response.ok) { const errorText = await response.text() - // Provide consistent error handling with createMessage + // Provide consistent error handling with createMessage (with HTTP status attached) if (response.status === 401) { - throw new Error(t("common:errors.cerebras.authenticationFailed")) + throwWithStatus(t("common:errors.cerebras.authenticationFailed"), response.status) } else if (response.status === 403) { - throw new Error(t("common:errors.cerebras.accessForbidden")) + throwWithStatus(t("common:errors.cerebras.accessForbidden"), response.status) } else if (response.status === 429) { - throw new Error(t("common:errors.cerebras.rateLimitExceeded")) + throwWithStatus(t("common:errors.cerebras.rateLimitExceeded"), response.status) } else if (response.status >= 500) { - throw new Error(t("common:errors.cerebras.serverError", { status: response.status })) + throwWithStatus( + t("common:errors.cerebras.serverError", { status: response.status }), + response.status, + ) } else { - throw new Error( + throwWithStatus( t("common:errors.cerebras.genericError", { status: response.status, message: errorText }), + response.status, ) } } @@ -324,7 +347,12 @@ export class CerebrasHandler extends BaseProvider implements SingleCompletionHan return result.choices?.[0]?.message?.content || "" } catch (error) { if (error instanceof Error) { - throw new Error(t("common:errors.cerebras.completionError", { error: error.message })) + // Preserve HTTP status code if present on the original error + const wrappedError = new Error(t("common:errors.cerebras.completionError", { error: error.message })) + if ((error as any).status !== undefined) { + ;(wrappedError as any).status = (error as any).status + } + throw wrappedError } throw error }