Skip to content

Commit a490969

Browse files
committed
feat(auth): add retry logic and error handling for API key validation
1 parent 842bec9 commit a490969

2 files changed

Lines changed: 52 additions & 3 deletions

File tree

packages/cli/src/commands/auth/login-console.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,7 @@ export async function runConsoleLogin(
281281
opts?: { needApiKey?: boolean; onApiKey?: (key: string) => Promise<void> },
282282
): Promise<void> {
283283
const state = randomBytes(16).toString("hex");
284+
let callbackError: unknown;
284285
const server = http.createServer(async (req, res) => {
285286
try {
286287
if (req.method === "OPTIONS") {
@@ -313,9 +314,11 @@ export async function runConsoleLogin(
313314
if (apiKey && opts?.onApiKey) {
314315
await opts.onApiKey(apiKey);
315316
}
316-
} catch {
317+
} catch (err: unknown) {
318+
callbackError = err;
317319
res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
318320
res.end("Failed to save credentials\n");
321+
server.close();
319322
return;
320323
}
321324
}
@@ -388,4 +391,8 @@ export async function runConsoleLogin(
388391
}
389392
});
390393
});
394+
395+
if (callbackError) {
396+
throw callbackError;
397+
}
391398
}

packages/cli/src/commands/auth/login.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import {
2+
BailianError,
3+
ExitCode,
24
chatEndpoint,
35
defineCommand,
46
getConfigPath,
@@ -16,18 +18,58 @@ import { promptConfirm } from "../../output/prompt.ts";
1618
import { printCurrentCommandHelp } from "../../utils/command-help.ts";
1719
import { resolveConsoleOrigin, runConsoleLogin } from "./login-console.ts";
1820

21+
const RETRY_DELAY_BASE_MS = 500;
22+
23+
function canRetry(err: unknown): boolean {
24+
if (err instanceof BailianError) {
25+
if (err.exitCode === ExitCode.NETWORK || err.exitCode === ExitCode.TIMEOUT) {
26+
return true;
27+
}
28+
const status = err.api?.httpStatus;
29+
return status === 401 || (status !== undefined && status >= 500);
30+
}
31+
if (err instanceof Error) {
32+
return (
33+
err.name === "AbortError" ||
34+
err.name === "TimeoutError" ||
35+
err.message.includes("timed out") ||
36+
err.message === "fetch failed"
37+
);
38+
}
39+
return false;
40+
}
41+
1942
async function validateKeyAndPersist(config: Config, key: string): Promise<void> {
2043
process.stderr.write("Testing key... ");
2144
const testConfig = { ...config, apiKey: key };
22-
await requestJson<unknown>(testConfig, {
45+
const requestOpts = {
2346
url: chatEndpoint(testConfig.baseUrl),
2447
method: "POST",
48+
timeout: Math.min(config.timeout, 30),
2549
body: {
2650
model: "qwen3.7-max",
2751
messages: [{ role: "user", content: "hi" }],
2852
max_tokens: 1,
2953
},
30-
});
54+
};
55+
56+
for (let attempt = 1; attempt <= 3; attempt++) {
57+
try {
58+
await requestJson<unknown>(testConfig, requestOpts);
59+
break;
60+
} catch (err) {
61+
if (attempt >= 3 || !canRetry(err)) {
62+
process.stderr.write("\n");
63+
throw new BailianError("API key validation failed", ExitCode.AUTH, "Invalid API key.", {
64+
cause: err,
65+
});
66+
}
67+
// retry delay: 500ms, 1000ms, 2000ms
68+
const delayMs = RETRY_DELAY_BASE_MS * 2 ** (attempt - 1);
69+
await new Promise((resolve) => setTimeout(resolve, delayMs));
70+
}
71+
}
72+
3173
process.stderr.write("Valid\n");
3274

3375
const existing = readConfigFile() as Record<string, unknown>;

0 commit comments

Comments
 (0)