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

  1. Re-run the Kimi login flow (device authorization) to mint a fresh token pair, replacing the entry in auths/
  2. Do not share or copy the same auth file across multiple instances — rotation invalidates the old refresh token
  3. 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

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


AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15). Data as JSON: /api/errors/e33bd7d19fa97a24. Report an issue: GitHub.