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
This commit is contained in:
Roo Code 2025-12-20 15:05:05 +00:00
parent 78dc34498b
commit 2496d77689
2 changed files with 95 additions and 14 deletions

View file

@ -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

View file

@ -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
}