Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit 2496d77

Browse files
committed
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
1 parent 78dc344 commit 2496d77

2 files changed

Lines changed: 95 additions & 14 deletions

File tree

src/api/providers/__tests__/cerebras.spec.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,59 @@ describe("CerebrasHandler", () => {
123123
await expect(generator.next()).rejects.toThrow()
124124
})
125125

126+
it("should attach HTTP status code to error objects", async () => {
127+
const mockErrorResponse = {
128+
ok: false,
129+
status: 401,
130+
text: () => Promise.resolve('{"error": {"message": "Unauthorized"}}'),
131+
}
132+
vi.mocked(fetch).mockResolvedValueOnce(mockErrorResponse as any)
133+
134+
const generator = handler.createMessage("System prompt", [])
135+
try {
136+
await generator.next()
137+
// Should not reach here
138+
expect(true).toBe(false)
139+
} catch (error: any) {
140+
// The outer catch wraps the error, but status should be preserved
141+
expect(error.status).toBe(401)
142+
}
143+
})
144+
145+
it("should attach HTTP status code for rate limit errors", async () => {
146+
const mockErrorResponse = {
147+
ok: false,
148+
status: 429,
149+
text: () => Promise.resolve('{"error": {"message": "Rate limit exceeded"}}'),
150+
}
151+
vi.mocked(fetch).mockResolvedValueOnce(mockErrorResponse as any)
152+
153+
const generator = handler.createMessage("System prompt", [])
154+
try {
155+
await generator.next()
156+
expect(true).toBe(false)
157+
} catch (error: any) {
158+
expect(error.status).toBe(429)
159+
}
160+
})
161+
162+
it("should attach HTTP status code for server errors", async () => {
163+
const mockErrorResponse = {
164+
ok: false,
165+
status: 500,
166+
text: () => Promise.resolve('{"error": {"message": "Internal server error"}}'),
167+
}
168+
vi.mocked(fetch).mockResolvedValueOnce(mockErrorResponse as any)
169+
170+
const generator = handler.createMessage("System prompt", [])
171+
try {
172+
await generator.next()
173+
expect(true).toBe(false)
174+
} catch (error: any) {
175+
expect(error.status).toBe(500)
176+
}
177+
})
178+
126179
it("should parse streaming responses correctly", async () => {
127180
// Test streaming response parsing
128181
// Mock ReadableStream with various data chunks

src/api/providers/cerebras.ts

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,16 @@ import { BaseProvider } from "./base-provider"
1313
import { DEFAULT_HEADERS } from "./constants"
1414
import { t } from "../../i18n"
1515

16+
/**
17+
* Creates an Error with an HTTP status code attached for proper UI error handling.
18+
* The status property is used by ChatRow to display appropriate error messages.
19+
*/
20+
function throwWithStatus(message: string, status: number): never {
21+
const error = new Error(message)
22+
;(error as any).status = status
23+
throw error
24+
}
25+
1626
const CEREBRAS_BASE_URL = "https://api.cerebras.ai/v1"
1727
const CEREBRAS_DEFAULT_TEMPERATURE = 0
1828

@@ -150,18 +160,22 @@ export class CerebrasHandler extends BaseProvider implements SingleCompletionHan
150160
errorMessage = errorText || `HTTP ${response.status}`
151161
}
152162

153-
// Provide more actionable error messages
163+
// Provide more actionable error messages with HTTP status attached
154164
if (response.status === 401) {
155-
throw new Error(t("common:errors.cerebras.authenticationFailed"))
165+
throwWithStatus(t("common:errors.cerebras.authenticationFailed"), response.status)
156166
} else if (response.status === 403) {
157-
throw new Error(t("common:errors.cerebras.accessForbidden"))
167+
throwWithStatus(t("common:errors.cerebras.accessForbidden"), response.status)
158168
} else if (response.status === 429) {
159-
throw new Error(t("common:errors.cerebras.rateLimitExceeded"))
169+
throwWithStatus(t("common:errors.cerebras.rateLimitExceeded"), response.status)
160170
} else if (response.status >= 500) {
161-
throw new Error(t("common:errors.cerebras.serverError", { status: response.status }))
171+
throwWithStatus(
172+
t("common:errors.cerebras.serverError", { status: response.status }),
173+
response.status,
174+
)
162175
} else {
163-
throw new Error(
176+
throwWithStatus(
164177
t("common:errors.cerebras.genericError", { status: response.status, message: errorMessage }),
178+
response.status,
165179
)
166180
}
167181
}
@@ -273,7 +287,12 @@ export class CerebrasHandler extends BaseProvider implements SingleCompletionHan
273287
}
274288
} catch (error) {
275289
if (error instanceof Error) {
276-
throw new Error(t("common:errors.cerebras.completionError", { error: error.message }))
290+
// Preserve HTTP status code if present on the original error
291+
const wrappedError = new Error(t("common:errors.cerebras.completionError", { error: error.message }))
292+
if ((error as any).status !== undefined) {
293+
;(wrappedError as any).status = (error as any).status
294+
}
295+
throw wrappedError
277296
}
278297
throw error
279298
}
@@ -304,18 +323,22 @@ export class CerebrasHandler extends BaseProvider implements SingleCompletionHan
304323
if (!response.ok) {
305324
const errorText = await response.text()
306325

307-
// Provide consistent error handling with createMessage
326+
// Provide consistent error handling with createMessage (with HTTP status attached)
308327
if (response.status === 401) {
309-
throw new Error(t("common:errors.cerebras.authenticationFailed"))
328+
throwWithStatus(t("common:errors.cerebras.authenticationFailed"), response.status)
310329
} else if (response.status === 403) {
311-
throw new Error(t("common:errors.cerebras.accessForbidden"))
330+
throwWithStatus(t("common:errors.cerebras.accessForbidden"), response.status)
312331
} else if (response.status === 429) {
313-
throw new Error(t("common:errors.cerebras.rateLimitExceeded"))
332+
throwWithStatus(t("common:errors.cerebras.rateLimitExceeded"), response.status)
314333
} else if (response.status >= 500) {
315-
throw new Error(t("common:errors.cerebras.serverError", { status: response.status }))
334+
throwWithStatus(
335+
t("common:errors.cerebras.serverError", { status: response.status }),
336+
response.status,
337+
)
316338
} else {
317-
throw new Error(
339+
throwWithStatus(
318340
t("common:errors.cerebras.genericError", { status: response.status, message: errorText }),
341+
response.status,
319342
)
320343
}
321344
}
@@ -324,7 +347,12 @@ export class CerebrasHandler extends BaseProvider implements SingleCompletionHan
324347
return result.choices?.[0]?.message?.content || ""
325348
} catch (error) {
326349
if (error instanceof Error) {
327-
throw new Error(t("common:errors.cerebras.completionError", { error: error.message }))
350+
// Preserve HTTP status code if present on the original error
351+
const wrappedError = new Error(t("common:errors.cerebras.completionError", { error: error.message }))
352+
if ((error as any).status !== undefined) {
353+
;(wrappedError as any).status = (error as any).status
354+
}
355+
throw wrappedError
328356
}
329357
throw error
330358
}

0 commit comments

Comments
 (0)