router-for-me/CLIProxyAPI · critical
kimi: refresh token rejected (status %d)
Error message
kimi: refresh token rejected (status %d)
What it means
The refresh endpoint answered 401 Unauthorized or 403 Forbidden: the stored refresh token is no longer accepted by Kimi. This is the definitive 'credential dead' signal — the token was revoked, expired permanently, or the account/client was disabled. Automated refresh cannot recover; interactive re-login is required.
Source
Thrown at internal/auth/kimi/kimi.go:401
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("kimi: refresh request failed: %w", err)
}
defer func() {
if errClose := resp.Body.Close(); errClose != nil {
log.Errorf("kimi refresh token: close body error: %v", errClose)
}
}()
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("kimi: failed to read refresh response: %w", err)
}
if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
return nil, fmt.Errorf("kimi: refresh token rejected (status %d)", resp.StatusCode)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("kimi: refresh failed with status %d: %s", resp.StatusCode, string(bodyBytes))
}
var tokenResp struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
TokenType string `json:"token_type"`
ExpiresIn float64 `json:"expires_in"`
Scope string `json:"scope"`
}
if err = json.Unmarshal(bodyBytes, &tokenResp); err != nil {
return nil, fmt.Errorf("kimi: failed to parse refresh response: %w", err)
}
View on GitHub (pinned to 78f0c4079e)
Solutions
- Re-run the Kimi login flow (device authorization) to mint a fresh token pair, replacing the entry in auths/
- Do not share or copy the same auth file across multiple instances — rotation invalidates the old refresh token
- If it recurs quickly after login, check whether another process (old instance, other machine) is using the same credential
Example fix
// before
newTok, err := c.deviceClient.RefreshToken(ctx, tok.RefreshToken)
if err != nil {
return err // surfaces as opaque failure, requests keep failing
}
// after
newTok, err := c.deviceClient.RefreshToken(ctx, tok.RefreshToken)
if err != nil {
if strings.Contains(err.Error(), "refresh token rejected") {
// credential is dead: mark for interactive re-login, stop retrying refresh
log.Errorf("kimi: refresh token rejected; re-login required")
return errReauthRequired
}
return err
} Defensive patterns
Strategy: fallback
Validate before calling
// Check expiry/validity heuristics before relying on the credential
if tok.ExpiresAt > 0 && time.Until(time.Unix(tok.ExpiresAt, 0)) < 0 && strings.TrimSpace(tok.RefreshToken) == "" {
return errReauthRequired // no refresh token and already expired: go to login UI
} Type guard
func isKimiReauthRequired(err error) bool {
return err != nil && strings.Contains(err.Error(), "refresh token rejected")
} Try / catch
if err != nil {
if strings.Contains(err.Error(), "refresh token rejected") {
// terminal: disable the credential, notify user to re-login, stop retrying
return errReauthRequired
}
return err
} Prevention
- Never copy or share auths/ credentials between instances — rotation invalidates old tokens
- Mark 401/403 refreshes as re-auth-required instead of retrying
- Revoke-and-relogin on account changes
When it happens
Trigger: User revoked the app or logged out all sessions on Kimi; refresh token past its absolute lifetime; Moonshot disabled the account; rotating refresh tokens where the old one was already consumed (singleflight normally prevents concurrent consumption, but external use of the token can).
Common situations: Long-running deployments whose stored auths/ credential was invalidated server-side, copying auth files between installs (second install consumes the rotation), account security resets.
Related errors
- kimi: refresh token is required
- kimi: failed to create refresh request: %w
- kimi: refresh request failed: %w
- kimi: failed to read refresh response: %w
- kimi: refresh failed with status %d: %s
AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15).
Data as JSON: /api/errors/e33bd7d19fa97a24.
Report an issue: GitHub.