{"record":{"id":"4b783503307405f4","repo":"router-for-me/CLIProxyAPI","slug":"token-refresh-failed-after-d-attempts-w-4b7835","errorCode":null,"errorMessage":"token refresh failed after %d attempts: %w","messagePattern":"token refresh failed after (.+?) attempts: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/auth/codex/openai_auth.go","lineNumber":328,"sourceCode":"\t\t\t\treturn nil, ctx.Err()\n\t\t\tcase <-time.After(time.Duration(attempt) * time.Second):\n\t\t\t}\n\t\t}\n\n\t\ttokenData, err := o.RefreshTokens(ctx, refreshToken)\n\t\tif err == nil {\n\t\t\treturn tokenData, nil\n\t\t}\n\t\tif isNonRetryableRefreshErr(err) {\n\t\t\tlog.Warnf(\"Token refresh attempt %d failed with non-retryable error: %v\", attempt+1, err)\n\t\t\treturn nil, err\n\t\t}\n\n\t\tlastErr = err\n\t\tlog.Warnf(\"Token refresh attempt %d failed: %v\", attempt+1, err)\n\t}\n\n\treturn nil, fmt.Errorf(\"token refresh failed after %d attempts: %w\", maxRetries, lastErr)\n}\n\nfunc isNonRetryableRefreshErr(err error) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\traw := strings.ToLower(err.Error())\n\treturn strings.Contains(raw, \"refresh_token_reused\")\n}\n\n// UpdateTokenStorage updates an existing CodexTokenStorage with new token data.\n// This is typically called after a successful token refresh to persist the new credentials.\nfunc (o *CodexAuth) UpdateTokenStorage(storage *CodexTokenStorage, tokenData *CodexTokenData) {\n\tstorage.IDToken = tokenData.IDToken\n\tstorage.AccessToken = tokenData.AccessToken\n\tstorage.RefreshToken = tokenData.RefreshToken\n\tstorage.AccountID = tokenData.AccountID\n\tstorage.LastRefresh = time.Now().Format(time.RFC3339)","sourceCodeStart":310,"sourceCodeEnd":346,"githubUrl":"https://github.com/router-for-me/CLIProxyAPI/blob/78f0c4079e3e6273d65d03b5549cffc898703264/internal/auth/codex/openai_auth.go#L310-L346","documentation":"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.","triggerScenarios":"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.","commonSituations":"Server offline when the refresh scheduler fires; revoked credential retried in a loop; upstream auth outage; maxRetries set too low for the transient condition.","solutions":["Unwrap the last error (errors.Unwrap / read the message tail) — it tells you whether this is network, status, or parse.","Network cause: restore connectivity and trigger a refresh; consider a higher maxRetries with the built-in backoff.","invalid_grant/revocation cause: re-authenticate (delete the codex auth file, run login) — retries cannot fix a dead token.","Provider 5xx: check status page, retry later.","If embedding the SDK, surface this error so credential health monitoring can mark the auth file as needing re-login."],"exampleFix":"// before\ntd, err := o.RefreshTokensWithRetry(ctx, rt, 3)\nif err != nil { log.Errorf(\"refresh failed\") }\n\n// after\ntd, err := o.RefreshTokensWithRetry(ctx, rt, 3)\nif err != nil {\n    if strings.Contains(err.Error(), \"invalid_grant\") {\n        log.Errorf(\"refresh token no longer valid; re-login required\")\n    } else {\n        log.Errorf(\"refresh failed after retries: %v\", err)\n    }\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"td, err := o.RefreshTokensWithRetry(ctx, rt, 3)\nif err != nil {\n    if strings.Contains(err.Error(), \"token refresh failed after\") {\n        cause := errors.Unwrap(err)\n        if cause != nil && strings.Contains(cause.Error(), \"invalid_grant\") {\n            // credential is dead: re-login, more retries will not help\n        }\n        // otherwise network/upstream: schedule next periodic refresh\n    }\n}","preventionTips":["Classify the wrapped lastErr before deciding retry vs re-login.","Use shared storage backends when running multiple instances.","Keep maxRetries modest (3) and let periodic scheduling provide long-run retries.","Monitor refresh outcomes so permanent failures are visible early."],"tags":["oauth","codex","retry","token-refresh","invalid-grant","network"],"backgroundTag":null,"analyzedSha":"78f0c4079e3e6273d65d03b5549cffc898703264","analyzedAt":"2026-08-15T12:26:37.444Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}