Skip to content

Commit 37925d7

Browse files
committed
fix: improve error handling for Qwen API 400 status codes
- Add comprehensive error handling in QwenCodeHandler.callApiWithRetry - Handle 400, 403, 429, and 5xx errors with user-friendly messages - Add try-catch blocks to createMessage and completePrompt methods - Include detailed error context to help users understand issues - Add comprehensive test coverage for error scenarios Fixes #8142
1 parent 87b45de commit 37925d7

File tree

2 files changed

+309
-71
lines changed

2 files changed

+309
-71
lines changed
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
2+
import { QwenCodeHandler } from "../qwen-code"
3+
import { promises as fs } from "node:fs"
4+
import OpenAI from "openai"
5+
6+
// Mock fs module
7+
vi.mock("node:fs", () => ({
8+
promises: {
9+
readFile: vi.fn(),
10+
writeFile: vi.fn(),
11+
},
12+
}))
13+
14+
// Mock OpenAI
15+
vi.mock("openai", () => {
16+
const mockCreate = vi.fn()
17+
return {
18+
default: vi.fn().mockImplementation(() => ({
19+
chat: {
20+
completions: {
21+
create: mockCreate,
22+
},
23+
},
24+
})),
25+
}
26+
})
27+
28+
// Mock fetch for OAuth token refresh
29+
global.fetch = vi.fn()
30+
31+
describe("QwenCodeHandler", () => {
32+
let handler: QwenCodeHandler
33+
const mockCredentials = {
34+
access_token: "test-access-token",
35+
refresh_token: "test-refresh-token",
36+
token_type: "Bearer",
37+
expiry_date: Date.now() + 3600000, // 1 hour from now
38+
resource_url: "https://dashscope.aliyuncs.com/compatible-mode/v1",
39+
}
40+
41+
beforeEach(() => {
42+
vi.clearAllMocks()
43+
// Mock reading credentials file
44+
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockCredentials))
45+
vi.mocked(fs.writeFile).mockResolvedValue(undefined)
46+
47+
handler = new QwenCodeHandler({
48+
apiModelId: "qwen-max",
49+
})
50+
})
51+
52+
afterEach(() => {
53+
vi.clearAllMocks()
54+
})
55+
56+
describe("Error Handling", () => {
57+
it("should handle 400 errors with user-friendly message", async () => {
58+
const mockClient = new OpenAI({ apiKey: "test" })
59+
const mockError = {
60+
status: 400,
61+
message: "Invalid request format",
62+
}
63+
64+
vi.mocked(mockClient.chat.completions.create).mockRejectedValue(mockError)
65+
66+
// Override the ensureClient method to return our mock
67+
handler["client"] = mockClient
68+
69+
await expect(handler.completePrompt("test prompt")).rejects.toThrow(
70+
"Qwen API Error (400): Invalid request format. This may be due to invalid input format, unsupported file type, or request size limits.",
71+
)
72+
})
73+
74+
it("should handle 401 errors and attempt token refresh", async () => {
75+
const mockClient = new OpenAI({ apiKey: "test" })
76+
const mockError = {
77+
status: 401,
78+
message: "Unauthorized",
79+
}
80+
81+
// First call fails with 401, second succeeds after refresh
82+
vi.mocked(mockClient.chat.completions.create)
83+
.mockRejectedValueOnce(mockError)
84+
.mockResolvedValueOnce({
85+
choices: [{ message: { content: "Success after refresh" } }],
86+
} as any)
87+
88+
// Mock successful token refresh
89+
vi.mocked(global.fetch).mockResolvedValueOnce({
90+
ok: true,
91+
json: async () => ({
92+
access_token: "new-access-token",
93+
token_type: "Bearer",
94+
expires_in: 3600,
95+
refresh_token: "new-refresh-token",
96+
}),
97+
} as any)
98+
99+
handler["client"] = mockClient
100+
101+
const result = await handler.completePrompt("test prompt")
102+
expect(result).toBe("Success after refresh")
103+
expect(global.fetch).toHaveBeenCalledTimes(1)
104+
})
105+
106+
it("should handle 403 errors with permission message", async () => {
107+
const mockClient = new OpenAI({ apiKey: "test" })
108+
const mockError = {
109+
status: 403,
110+
message: "Access denied",
111+
}
112+
113+
vi.mocked(mockClient.chat.completions.create).mockRejectedValue(mockError)
114+
handler["client"] = mockClient
115+
116+
await expect(handler.completePrompt("test prompt")).rejects.toThrow(
117+
"Qwen API Error (403): Access denied. Please check your API permissions.",
118+
)
119+
})
120+
121+
it("should handle 429 rate limit errors", async () => {
122+
const mockClient = new OpenAI({ apiKey: "test" })
123+
const mockError = {
124+
status: 429,
125+
message: "Too many requests",
126+
}
127+
128+
vi.mocked(mockClient.chat.completions.create).mockRejectedValue(mockError)
129+
handler["client"] = mockClient
130+
131+
await expect(handler.completePrompt("test prompt")).rejects.toThrow(
132+
"Qwen API Error (429): Too many requests. Please wait before making more requests.",
133+
)
134+
})
135+
136+
it("should handle 500+ server errors", async () => {
137+
const mockClient = new OpenAI({ apiKey: "test" })
138+
const mockError = {
139+
status: 503,
140+
message: "Service unavailable",
141+
}
142+
143+
vi.mocked(mockClient.chat.completions.create).mockRejectedValue(mockError)
144+
handler["client"] = mockClient
145+
146+
await expect(handler.completePrompt("test prompt")).rejects.toThrow(
147+
"Qwen API Error (503): Service unavailable. The Qwen service may be temporarily unavailable.",
148+
)
149+
})
150+
151+
it("should handle generic errors with context", async () => {
152+
const mockClient = new OpenAI({ apiKey: "test" })
153+
const mockError = new Error("Network timeout")
154+
155+
vi.mocked(mockClient.chat.completions.create).mockRejectedValue(mockError)
156+
handler["client"] = mockClient
157+
158+
await expect(handler.completePrompt("test prompt")).rejects.toThrow(
159+
"Failed to complete prompt with Qwen model: Network timeout",
160+
)
161+
})
162+
163+
it("should preserve already formatted Qwen API errors", async () => {
164+
const mockClient = new OpenAI({ apiKey: "test" })
165+
const mockError = new Error("Qwen API Error (400): Already formatted error")
166+
167+
vi.mocked(mockClient.chat.completions.create).mockRejectedValue(mockError)
168+
handler["client"] = mockClient
169+
170+
await expect(handler.completePrompt("test prompt")).rejects.toThrow(
171+
"Qwen API Error (400): Already formatted error",
172+
)
173+
})
174+
})
175+
176+
describe("Model Configuration", () => {
177+
it("should return correct model info", () => {
178+
const model = handler.getModel()
179+
expect(model.id).toBe("qwen-max")
180+
expect(model.info).toBeDefined()
181+
expect(model.info.maxTokens).toBeGreaterThan(0)
182+
})
183+
184+
it("should use default model when not specified", () => {
185+
const defaultHandler = new QwenCodeHandler({})
186+
const model = defaultHandler.getModel()
187+
expect(model.id).toBeDefined()
188+
expect(model.info).toBeDefined()
189+
})
190+
})
191+
})

0 commit comments

Comments
 (0)