router-for-me/CLIProxyAPI · error
kimi: empty access token in refresh response
Error message
kimi: empty access token in refresh response
What it means
Thrown by the Kimi OAuth device-flow client after a token refresh POST to kimiTokenURL returns HTTP 200 but the decoded JSON body has an empty or absent access_token field. The code explicitly guards tokenResp.AccessToken == "" after a successful json.Unmarshal (internal/auth/kimi/kimi.go:420). It means the upstream token endpoint responded 'successfully' yet did not issue a usable access token, so the refreshed KimiTokenData cannot be constructed.
Source
Thrown at internal/auth/kimi/kimi.go:421
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)
}
if tokenResp.AccessToken == "" {
return nil, fmt.Errorf("kimi: empty access token in refresh response")
}
var expiresAt int64
if tokenResp.ExpiresIn > 0 {
expiresAt = time.Now().Unix() + int64(tokenResp.ExpiresIn)
}
return &KimiTokenData{
AccessToken: tokenResp.AccessToken,
RefreshToken: tokenResp.RefreshToken,
TokenType: tokenResp.TokenType,
ExpiresAt: expiresAt,
Scope: tokenResp.Scope,
}, nil
}
View on GitHub (pinned to 78f0c4079e)
Solutions
- Delete the stale Kimi credential file under auths/ and re-run the OAuth login flow to obtain a fresh refresh token
- Log the raw refresh response body when AccessToken is empty to see the server's actual error payload, then act on it
- Check whether the Kimi token endpoint URL or client_id (kimiClientID) changed in an upstream release and upgrade CLIProxyAPI
- If behind a corporate proxy, verify the token endpoint is reachable and returns genuine JSON (curl the token URL manually)
Example fix
// before (kimi.go:420)
if tokenResp.AccessToken == "" {
return nil, fmt.Errorf("kimi: empty access token in refresh response")
}
// after: include the server payload for diagnosis (do not log tokens)
if tokenResp.AccessToken == "" {
return nil, fmt.Errorf("kimi: empty access token in refresh response (status %d, body hint: %s)", resp.StatusCode, errorHintFromBody(bodyBytes))
} Defensive patterns
Strategy: try-catch
Try / catch
if _, err := kimiClient.RefreshToken(ctx, refreshToken); err != nil {
if strings.Contains(err.Error(), "empty access token in refresh response") {
// treat as dead credential: purge and re-authenticate, do not retry
_ = os.Remove(kimiAuthFile)
return relogin()
}
return err
} Prevention
- Keep Kimi credentials fresh by exercising the token before long expiry windows
- Wrap the refresh call and log (safe, non-secret) response metadata when it fails
- Automate re-login when refresh repeatedly fails instead of retrying the same stale refresh token
When it happens
Trigger: Calling the Kimi refresh flow with grant_type=refresh_token where the refresh token is expired/revoked but the server still replies 200 with an error payload (e.g. {"error":"invalid_grant"}); an API change at the Kimi token endpoint renaming access_token; a proxy or CAPTCHA/HTML page returning 200 with an empty body that still unmarshals; fields returned as null.
Common situations: Kimi auth files in auths/ that are months old with a stale refresh token; Moonshot/Kimi changing their OAuth endpoint contract; running the proxy behind a captive portal or corporate proxy that injects a 200 HTML response; clock/env issues causing the server to soft-fail.
Related errors
- failed to create refresh request: %w
- token refresh request failed: %w
- failed to read refresh response: %w
- token refresh failed with status %d: %s
- failed to parse refresh response: %w
AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15).
Data as JSON: /api/errors/738ffbc4af61222a.
Report an issue: GitHub.