router-for-me/CLIProxyAPI · error

token refresh failed after %d attempts: %w

Error message

token refresh failed after %d attempts: %w

What it means

RefreshTokensWithRetry exhausted maxRetries attempts; the message wraps the last underlying error (network, status, or parse failure). The loop bails out early only for non-retryable errors containing refresh_token_reused, so reaching this message means every attempt failed with a retryable-looking error — usually sustained network failure or repeated non-200s like invalid_grant that do not match the reuse signature.

Source

Thrown at internal/auth/codex/openai_auth.go:328

				return nil, ctx.Err()
			case <-time.After(time.Duration(attempt) * time.Second):
			}
		}

		tokenData, err := o.RefreshTokens(ctx, refreshToken)
		if err == nil {
			return tokenData, nil
		}
		if isNonRetryableRefreshErr(err) {
			log.Warnf("Token refresh attempt %d failed with non-retryable error: %v", attempt+1, err)
			return nil, err
		}

		lastErr = err
		log.Warnf("Token refresh attempt %d failed: %v", attempt+1, err)
	}

	return nil, fmt.Errorf("token refresh failed after %d attempts: %w", maxRetries, lastErr)
}

func isNonRetryableRefreshErr(err error) bool {
	if err == nil {
		return false
	}
	raw := strings.ToLower(err.Error())
	return strings.Contains(raw, "refresh_token_reused")
}

// UpdateTokenStorage updates an existing CodexTokenStorage with new token data.
// This is typically called after a successful token refresh to persist the new credentials.
func (o *CodexAuth) UpdateTokenStorage(storage *CodexTokenStorage, tokenData *CodexTokenData) {
	storage.IDToken = tokenData.IDToken
	storage.AccessToken = tokenData.AccessToken
	storage.RefreshToken = tokenData.RefreshToken
	storage.AccountID = tokenData.AccountID
	storage.LastRefresh = time.Now().Format(time.RFC3339)

View on GitHub (pinned to 78f0c4079e)

Solutions

  1. Unwrap the last error (errors.Unwrap / read the message tail) — it tells you whether this is network, status, or parse.
  2. Network cause: restore connectivity and trigger a refresh; consider a higher maxRetries with the built-in backoff.
  3. invalid_grant/revocation cause: re-authenticate (delete the codex auth file, run login) — retries cannot fix a dead token.
  4. Provider 5xx: check status page, retry later.
  5. If embedding the SDK, surface this error so credential health monitoring can mark the auth file as needing re-login.

Example fix

// before
td, err := o.RefreshTokensWithRetry(ctx, rt, 3)
if err != nil { log.Errorf("refresh failed") }

// after
td, err := o.RefreshTokensWithRetry(ctx, rt, 3)
if err != nil {
    if strings.Contains(err.Error(), "invalid_grant") {
        log.Errorf("refresh token no longer valid; re-login required")
    } else {
        log.Errorf("refresh failed after retries: %v", err)
    }
}
Defensive patterns

Strategy: retry

Try / catch

td, err := o.RefreshTokensWithRetry(ctx, rt, 3)
if err != nil {
    if strings.Contains(err.Error(), "token refresh failed after") {
        cause := errors.Unwrap(err)
        if cause != nil && strings.Contains(cause.Error(), "invalid_grant") {
            // credential is dead: re-login, more retries will not help
        }
        // otherwise network/upstream: schedule next periodic refresh
    }
}

Prevention

When it happens

Trigger: Sustained network outage across all attempts (each failing at dial); refresh token expired/revoked returning invalid_grant repeatedly (does not match the refresh_token_reused string, so it retries and fails every time); provider 5xx lasting longer than the retry window; context deadline hit on each attempt.

Common situations: Server offline when the refresh scheduler fires; revoked credential retried in a loop; upstream auth outage; maxRetries set too low for the transient condition.

Related errors


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