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

  1. Re-authenticate via kimi login — a 401/403 refresh almost always means the refresh token is dead
  2. Check the embedded status: 429/5xx suggests retrying with backoff instead of re-login
  3. Verify oauthHost in the stored auth matches the real token endpoint host
  4. 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

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


AI-assisted analysis of musistudio/claude-code-router@99f24806c6 (2026-08-27). Data as JSON: /api/errors/d31ddf1cdf5de621. Report an issue: GitHub.