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

  1. 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.
  2. 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.
  3. Never copy token files between environments expecting both to keep working.
  4. If the user recently changed their provider password or revoked sessions, re-login is the only fix.
  5. 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

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


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