This commit is contained in:
roomote-v0[bot] 2026-04-26 00:10:07 +05:30 • committed by GitHub
commit ff343812d1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 127 additions and 7 deletions

View file

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

View file

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