router-for-me/CLIProxyAPI · critical
token refresh failed with status %d: %s
Error message
token refresh failed with status %d: %s
What it means
The token endpoint returned non-200 for the refresh request; the message embeds the status code and body. This is the provider rejecting the refresh token itself: 400 invalid_grant (token expired, revoked, or rotated), refresh_token_reused (detected reuse after rotation — non-retryable per isNonRetryableRefreshErr), or 401 for a wrong client. Unlike network errors, retrying usually makes it worse.
Source
Thrown at internal/auth/codex/openai_auth.go:245
req.Header.Set("Accept", "application/json")
resp, errDo := o.httpClient.Do(req)
if errDo != nil {
return nil, fmt.Errorf("token refresh request failed: %w", errDo)
}
defer func() {
if errClose := resp.Body.Close(); errClose != nil {
log.Errorf("token refresh response body close error: %v", errClose)
}
}()
body, errRead := io.ReadAll(resp.Body)
if errRead != nil {
return nil, fmt.Errorf("failed to read refresh response: %w", errRead)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("token refresh failed with status %d: %s", resp.StatusCode, string(body))
}
var tokenResp struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
IDToken string `json:"id_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
}
if errUnmarshal := json.Unmarshal(body, &tokenResp); errUnmarshal != nil {
return nil, fmt.Errorf("failed to parse refresh response: %w", errUnmarshal)
}
// Extract account ID from ID token
claims, errParseJWT := ParseJWTToken(tokenResp.IDToken)
if errParseJWT != nil {
log.Warnf("Failed to parse refreshed ID token: %v", errParseJWT)View on GitHub (pinned to 78f0c4079e)
Solutions
- If the body contains refresh_token_reused or invalid_grant, stop retrying and re-authenticate: delete/rename the codex auth file under auths/ and run the login flow again.
- Ensure only ONE process/instance uses a given auth directory; if you need multiple instances, use the Postgres/git/object-store backends so refreshed tokens are shared.
- Never copy token files between environments expecting both to keep working.
- If the user recently changed their provider password or revoked sessions, re-login is the only fix.
- Confirm system clock accuracy (ntpd/chrony) so expiries are computed correctly.
Example fix
# before: two instances share ./auths via copied files -> one invalidates the other's refresh token # after: single source of truth for credentials rm auths/codex-*.json && cli-proxy-api login # re-auth once, keep one instance per auth dir
Defensive patterns
Strategy: validation
Validate before calling
// Before refreshing, sanity-check the stored refresh token exists and the file is exclusively owned
if ts.RefreshToken == "" {
return errors.New("no refresh token stored; re-login required")
} Try / catch
td, err := auth.RefreshTokens(ctx, rt)
if err != nil {
msg := strings.ToLower(err.Error())
if strings.Contains(msg, "refresh_token_reused") || strings.Contains(msg, "invalid_grant") || strings.Contains(msg, "status 401") {
// terminal: mark credential for re-login; do NOT retry
markAuthFileForRelogin(path)
}
} Prevention
- Run exactly one process per auth directory; use shared storage backends for multi-instance setups.
- Never copy auth JSON files between machines or environments.
- Re-auth promptly after provider password changes or session revocations.
- Alert on invalid_grant so dead credentials are re-issued before they break traffic.
When it happens
Trigger: Refresh token already rotated by a parallel process/instance using the same auth file and the old one replayed; user revoked app access at the provider; token aged past its absolute lifetime; multiple cli-proxy-api instances sharing auths/ without shared storage coordination; clock skew causing premature expiry.
Common situations: Copying auth JSON files between machines (both refresh, one invalidates the other); running the server twice against the same auths dir; provider-side session revocation after password change; stale auth file from months ago.
Related errors
- token exchange failed with status %d: %s
- token refresh failed after %d attempts: %w
- failed to create refresh request: %w
- token refresh request failed: %w
- failed to read refresh response: %w
AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15).
Data as JSON: /api/errors/cec16fd0706683c6.
Report an issue: GitHub.