musistudio/claude-code-router · error · KimiRefreshAuthError
Kimi CLI OAuth token refresh returned HTTP ${response.status
Error message
Kimi CLI OAuth token refresh returned HTTP ${response.status}${tokenRefreshErrorMessage(payload, text)} What it means
The Kimi OAuth token refresh POST to ${oauthHost}/api/oauth/token returned a non-2xx status; the message embeds the status and any body-extracted error detail. 401/403 responses are wrapped in KimiRefreshAuthError so callers can force re-authentication rather than retry.
Source
Thrown at packages/core/src/agents/local-providers/kimi.ts:424
body: new URLSearchParams({
client_id: kimiOauthClientId,
grant_type: "refresh_token",
refresh_token: auth.refreshToken
}).toString(),
headers: {
...withoutHeader(kimiIdentityHeaders(), "user-agent"),
accept: "application/json",
"content-type": "application/x-www-form-urlencoded"
},
method: "POST",
signal: controller.signal
});
const text = await response.text();
const payload = parseJsonRecord(text);
if (!response.ok) {
const message = `Kimi CLI OAuth token refresh returned HTTP ${response.status}${tokenRefreshErrorMessage(payload, text)}`;
if (response.status === 401 || response.status === 403) {
throw new KimiRefreshAuthError(response.status, message);
}
throw new Error(message);
}
const accessToken = readString(payload?.access_token) || readString(payload?.accessToken);
const refreshToken = readString(payload?.refresh_token) || readString(payload?.refreshToken);
const expiresIn = numberValue(payload?.expires_in) ?? numberValue(payload?.expiresIn);
if (!accessToken || !refreshToken || !expiresIn) {
throw new Error("Kimi CLI OAuth token refresh returned an incomplete token response.");
}
const refreshed: KimiTokenSet = {
...auth,
accessToken,
expiresAt: Math.floor(Date.now() / 1000) + expiresIn,
expiresIn,
refreshToken,
scope: readString(payload?.scope) || auth.scope || "",
tokenType: readString(payload?.token_type) || readString(payload?.tokenType) || "Bearer"
};View on GitHub (pinned to 99f24806c6)
Solutions
- Re-authenticate via kimi login — a 401/403 refresh almost always means the refresh token is dead
- Check the embedded status: 429/5xx suggests retrying with backoff instead of re-login
- Verify oauthHost in the stored auth matches the real token endpoint host
- Confirm system clock accuracy
Defensive patterns
Strategy: try-catch
Type guard
function isKimiRefreshAuthError(e: unknown): e is KimiRefreshAuthError {
return e instanceof KimiRefreshAuthError;
} Try / catch
try {
await resolveKimiAuth(ref);
} catch (e) {
if (e instanceof KimiRefreshAuthError) {
await kimiLogin(); // 401/403 → re-auth
} else if (e instanceof Error && /returned HTTP 5\d\d|429/.test(e.message)) {
await retryWithBackoff();
} else throw e;
} Prevention
- Single-session logins: avoid concurrent logins that rotate/kill refresh tokens
- Store new refresh tokens immediately after rotation
- Handle 429 with backoff rather than hammering the token endpoint
When it happens
Trigger: resolveKimiAuth calls refreshKimiAuth with an expired access token; the token endpoint rejects the refresh grant — revoked/rotated refresh token (401/403), invalid client_id, expired grant, rate limiting (429), or server error (5xx).
Common situations: Logging in from another machine invalidated the refresh token; oauthHost in the stored config points to the wrong environment (staging vs prod); clock skew; upstream Kimi auth service incident.
Related errors
- Kimi CLI refresh token was not found.
- Grok CLI OAuth token refresh did not return an access token.
- Grok CLI OIDC discovery returned HTTP ${response.status}${to
- Kimi CLI credential was not found or is expired.
- Kimi CLI OAuth token refresh returned an incomplete token re
AI-assisted analysis of musistudio/claude-code-router@99f24806c6 (2026-08-27).
Data as JSON: /api/errors/d31ddf1cdf5de621.
Report an issue: GitHub.