Skip to content

Commit 4204fc5

Browse files
committed
fix: improve Claude Code authentication error handling
- Add dedicated authentication error message in i18n translations - Detect authentication errors in Claude Code provider and run.ts - Provide helpful instructions for users to authenticate with 'claude login' - Add comprehensive tests for authentication error detection Fixes #8946
1 parent 4a096e1 commit 4204fc5

File tree

4 files changed

+233
-2
lines changed

4 files changed

+233
-2
lines changed

src/api/providers/__tests__/claude-code.spec.ts

Lines changed: 163 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,32 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
12
import { ClaudeCodeHandler } from "../claude-code"
23
import { ApiHandlerOptions } from "../../../shared/api"
34
import { ClaudeCodeMessage } from "../../../integrations/claude-code/types"
5+
import { runClaudeCode } from "../../../integrations/claude-code/run"
6+
import { t } from "../../../i18n"
47

58
// Mock the runClaudeCode function
69
vi.mock("../../../integrations/claude-code/run", () => ({
710
runClaudeCode: vi.fn(),
811
}))
912

13+
vi.mock("../../../i18n", () => ({
14+
t: vi.fn((key: string, options?: any) => {
15+
if (key === "common:errors.claudeCode.authenticationError") {
16+
return `Claude Code authentication failed. Original error: ${options?.originalError || "unknown"}`
17+
}
18+
if (key === "common:errors.claudeCode.apiKeyModelPlanMismatch") {
19+
return "API keys and subscription plans allow different models. Make sure the selected model is included in your plan."
20+
}
21+
return key
22+
}),
23+
}))
24+
1025
// Mock the message filter
1126
vi.mock("../../../integrations/claude-code/message-filter", () => ({
1227
filterMessagesForClaudeCode: vi.fn((messages) => messages),
1328
}))
1429

15-
const { runClaudeCode } = await import("../../../integrations/claude-code/run")
1630
const { filterMessagesForClaudeCode } = await import("../../../integrations/claude-code/message-filter")
1731
const mockRunClaudeCode = vi.mocked(runClaudeCode)
1832
const mockFilterMessages = vi.mocked(filterMessagesForClaudeCode)
@@ -563,3 +577,151 @@ describe("ClaudeCodeHandler", () => {
563577
consoleSpy.mockRestore()
564578
})
565579
})
580+
581+
describe("ClaudeCodeHandler Authentication Error Handling", () => {
582+
let handler: ClaudeCodeHandler
583+
584+
beforeEach(() => {
585+
vi.clearAllMocks()
586+
handler = new ClaudeCodeHandler({
587+
claudeCodePath: "claude",
588+
apiModelId: "claude-3-5-sonnet-20241022",
589+
} as ApiHandlerOptions)
590+
})
591+
592+
afterEach(() => {
593+
vi.clearAllMocks()
594+
})
595+
596+
it("should detect and handle authentication errors from API response", async () => {
597+
const mockGenerator = async function* (): AsyncGenerator<ClaudeCodeMessage | string> {
598+
yield {
599+
type: "assistant" as const,
600+
message: {
601+
id: "msg_123",
602+
type: "message",
603+
role: "assistant",
604+
model: "claude-3-5-sonnet-20241022",
605+
content: [
606+
{
607+
type: "text",
608+
text: 'API Error: 401 {"error":{"message":"Authentication failed. Please login with claude login"}}',
609+
},
610+
],
611+
stop_reason: "stop",
612+
stop_sequence: null,
613+
usage: {
614+
input_tokens: 100,
615+
output_tokens: 50,
616+
cache_read_input_tokens: 0,
617+
cache_creation_input_tokens: 0,
618+
},
619+
} as any,
620+
session_id: "session_123",
621+
}
622+
}
623+
624+
mockRunClaudeCode.mockReturnValue(mockGenerator())
625+
626+
const messages: any[] = [{ role: "user", content: "test" }]
627+
const generator = handler.createMessage("system", messages)
628+
629+
await expect(async () => {
630+
for await (const _ of generator) {
631+
// consume generator
632+
}
633+
}).rejects.toThrow("Claude Code authentication failed")
634+
})
635+
636+
it("should detect various authentication error patterns", async () => {
637+
const authErrorMessages = [
638+
"API Error: 403 Unauthorized",
639+
"API Error: Invalid API key",
640+
"API Error: Credential expired",
641+
"API Error: Login required",
642+
"API Error: Not authenticated",
643+
]
644+
645+
for (const errorMessage of authErrorMessages) {
646+
const mockGenerator = async function* (): AsyncGenerator<ClaudeCodeMessage | string> {
647+
yield {
648+
type: "assistant" as const,
649+
message: {
650+
id: "msg_123",
651+
type: "message",
652+
role: "assistant",
653+
model: "claude-3-5-sonnet-20241022",
654+
content: [
655+
{
656+
type: "text",
657+
text: errorMessage,
658+
},
659+
],
660+
stop_reason: "stop",
661+
stop_sequence: null,
662+
usage: {
663+
input_tokens: 100,
664+
output_tokens: 50,
665+
cache_read_input_tokens: 0,
666+
cache_creation_input_tokens: 0,
667+
},
668+
} as any,
669+
session_id: "session_123",
670+
}
671+
}
672+
673+
mockRunClaudeCode.mockReturnValue(mockGenerator())
674+
675+
const messages: any[] = [{ role: "user", content: "test" }]
676+
const generator = handler.createMessage("system", messages)
677+
678+
await expect(async () => {
679+
for await (const _ of generator) {
680+
// consume generator
681+
}
682+
}).rejects.toThrow("Claude Code authentication failed")
683+
684+
vi.clearAllMocks()
685+
}
686+
})
687+
688+
it("should not treat non-authentication errors as authentication errors", async () => {
689+
const mockGenerator = async function* (): AsyncGenerator<ClaudeCodeMessage | string> {
690+
yield {
691+
type: "assistant" as const,
692+
message: {
693+
id: "msg_123",
694+
type: "message",
695+
role: "assistant",
696+
model: "claude-3-5-sonnet-20241022",
697+
content: [
698+
{
699+
type: "text",
700+
text: "API Error: 500 Internal Server Error",
701+
},
702+
],
703+
stop_reason: "stop",
704+
stop_sequence: null,
705+
usage: {
706+
input_tokens: 100,
707+
output_tokens: 50,
708+
cache_read_input_tokens: 0,
709+
cache_creation_input_tokens: 0,
710+
},
711+
} as any,
712+
session_id: "session_123",
713+
}
714+
}
715+
716+
mockRunClaudeCode.mockReturnValue(mockGenerator())
717+
718+
const messages: any[] = [{ role: "user", content: "test" }]
719+
const generator = handler.createMessage("system", messages)
720+
721+
await expect(async () => {
722+
for await (const _ of generator) {
723+
// consume generator
724+
}
725+
}).rejects.toThrow("API Error: 500 Internal Server Error")
726+
})
727+
})

src/api/providers/claude-code.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,26 @@ export class ClaudeCodeHandler extends BaseProvider implements ApiHandler {
8282

8383
const error = this.attemptParse(errorMessage)
8484
if (!error) {
85+
// Check for authentication errors in raw message
86+
if (this.isAuthenticationError(content.text)) {
87+
throw new Error(
88+
t("common:errors.claudeCode.authenticationError", {
89+
originalError: content.text,
90+
}),
91+
)
92+
}
8593
throw new Error(content.text)
8694
}
8795

96+
// Check for authentication-related errors
97+
if (this.isAuthenticationError(error.error?.message || errorMessage)) {
98+
throw new Error(
99+
t("common:errors.claudeCode.authenticationError", {
100+
originalError: error.error?.message || errorMessage,
101+
}),
102+
)
103+
}
104+
88105
if (error.error.message.includes("Invalid model name")) {
89106
throw new Error(
90107
content.text + `\n\n${t("common:errors.claudeCode.apiKeyModelPlanMismatch")}`,
@@ -172,4 +189,22 @@ export class ClaudeCodeHandler extends BaseProvider implements ApiHandler {
172189
return null
173190
}
174191
}
192+
193+
private isAuthenticationError(message: string): boolean {
194+
const authErrorPatterns = [
195+
"authentication failed",
196+
"unauthorized",
197+
"not authenticated",
198+
"login required",
199+
"invalid api key",
200+
"api key expired",
201+
"credential",
202+
"auth error",
203+
"403",
204+
"401",
205+
]
206+
207+
const lowerMessage = message.toLowerCase()
208+
return authErrorPatterns.some((pattern) => lowerMessage.includes(pattern))
209+
}
175210
}

src/i18n/locales/en/common.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,8 @@
9292
"processExitedWithError": "Claude Code process exited with code {{exitCode}}. Error output: {{output}}",
9393
"stoppedWithReason": "Claude Code stopped with reason: {{reason}}",
9494
"apiKeyModelPlanMismatch": "API keys and subscription plans allow different models. Make sure the selected model is included in your plan.",
95-
"notFound": "Claude Code executable '{{claudePath}}' not found.\n\nPlease install Claude Code CLI:\n1. Visit {{installationUrl}} to download Claude Code\n2. Follow the installation instructions for your operating system\n3. Ensure the 'claude' command is available in your PATH\n4. Alternatively, configure a custom path in Roo settings under 'Claude Code Path'\n\nOriginal error: {{originalError}}"
95+
"notFound": "Claude Code executable '{{claudePath}}' not found.\n\nPlease install Claude Code CLI:\n1. Visit {{installationUrl}} to download Claude Code\n2. Follow the installation instructions for your operating system\n3. Ensure the 'claude' command is available in your PATH\n4. Alternatively, configure a custom path in Roo settings under 'Claude Code Path'\n\nOriginal error: {{originalError}}",
96+
"authenticationError": "Claude Code authentication failed.\n\nTo authenticate with Claude Code CLI:\n1. Open a terminal/command prompt\n2. Run: claude login\n3. Follow the authentication prompts in your browser\n4. Once authenticated, try again in VS Code\n\nIf you're still having issues:\n• Ensure you're using Claude Code CLI v2.0.30 or later\n• Try running: claude logout && claude login\n• Check if your API key has expired at https://console.anthropic.com\n\nOriginal error: {{originalError}}"
9697
},
9798
"message": {
9899
"no_active_task_to_delete": "No active task to delete messages from",

src/integrations/claude-code/run.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,16 @@ export async function* runClaudeCode(
108108
}
109109

110110
const errorOutput = (processState.error as any)?.message || processState.stderrLogs?.trim()
111+
112+
// Check for authentication errors in stderr or error output
113+
if (errorOutput && isAuthenticationError(errorOutput)) {
114+
throw new Error(
115+
t("common:errors.claudeCode.authenticationError", {
116+
originalError: errorOutput,
117+
}),
118+
)
119+
}
120+
111121
throw new Error(
112122
`Claude Code process exited with code ${exitCode}.${errorOutput ? ` Error output: ${errorOutput}` : ""}`,
113123
)
@@ -273,3 +283,26 @@ function createClaudeCodeNotFoundError(claudePath: string, originalError: Error)
273283
error.name = "ClaudeCodeNotFoundError"
274284
return error
275285
}
286+
287+
/**
288+
* Checks if an error message indicates an authentication issue
289+
*/
290+
function isAuthenticationError(message: string): boolean {
291+
const authErrorPatterns = [
292+
"authentication failed",
293+
"unauthorized",
294+
"not authenticated",
295+
"login required",
296+
"invalid api key",
297+
"api key expired",
298+
"credential",
299+
"auth error",
300+
"403",
301+
"401",
302+
"please authenticate",
303+
"claude login",
304+
]
305+
306+
const lowerMessage = message.toLowerCase()
307+
return authErrorPatterns.some((pattern) => lowerMessage.includes(pattern))
308+
}

0 commit comments

Comments
 (0)