{"record":{"id":"cec16fd0706683c6","repo":"router-for-me/CLIProxyAPI","slug":"token-refresh-failed-with-status-d-s","errorCode":null,"errorMessage":"token refresh failed with status %d: %s","messagePattern":"token refresh failed with status (.+?): (.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"internal/auth/codex/openai_auth.go","lineNumber":245,"sourceCode":"\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tresp, errDo := o.httpClient.Do(req)\n\tif errDo != nil {\n\t\treturn nil, fmt.Errorf(\"token refresh request failed: %w\", errDo)\n\t}\n\tdefer func() {\n\t\tif errClose := resp.Body.Close(); errClose != nil {\n\t\t\tlog.Errorf(\"token refresh response body close error: %v\", errClose)\n\t\t}\n\t}()\n\n\tbody, errRead := io.ReadAll(resp.Body)\n\tif errRead != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read refresh response: %w\", errRead)\n\t}\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, fmt.Errorf(\"token refresh failed with status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\tvar tokenResp struct {\n\t\tAccessToken  string `json:\"access_token\"`\n\t\tRefreshToken string `json:\"refresh_token\"`\n\t\tIDToken      string `json:\"id_token\"`\n\t\tTokenType    string `json:\"token_type\"`\n\t\tExpiresIn    int    `json:\"expires_in\"`\n\t}\n\n\tif errUnmarshal := json.Unmarshal(body, &tokenResp); errUnmarshal != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse refresh response: %w\", errUnmarshal)\n\t}\n\n\t// Extract account ID from ID token\n\tclaims, errParseJWT := ParseJWTToken(tokenResp.IDToken)\n\tif errParseJWT != nil {\n\t\tlog.Warnf(\"Failed to parse refreshed ID token: %v\", errParseJWT)","sourceCodeStart":227,"sourceCodeEnd":263,"githubUrl":"https://github.com/router-for-me/CLIProxyAPI/blob/78f0c4079e3e6273d65d03b5549cffc898703264/internal/auth/codex/openai_auth.go#L227-L263","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"# before: two instances share ./auths via copied files -> one invalidates the other's refresh token\n# after: single source of truth for credentials\nrm auths/codex-*.json && cli-proxy-api login   # re-auth once, keep one instance per auth dir","handlingStrategy":"validation","validationCode":"// Before refreshing, sanity-check the stored refresh token exists and the file is exclusively owned\nif ts.RefreshToken == \"\" {\n    return errors.New(\"no refresh token stored; re-login required\")\n}","typeGuard":null,"tryCatchPattern":"td, err := auth.RefreshTokens(ctx, rt)\nif err != nil {\n    msg := strings.ToLower(err.Error())\n    if strings.Contains(msg, \"refresh_token_reused\") || strings.Contains(msg, \"invalid_grant\") || strings.Contains(msg, \"status 401\") {\n        // terminal: mark credential for re-login; do NOT retry\n        markAuthFileForRelogin(path)\n    }\n}","preventionTips":["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."],"tags":["oauth","codex","invalid-grant","token-rotation","token-refresh","http-status"],"backgroundTag":null,"analyzedSha":"78f0c4079e3e6273d65d03b5549cffc898703264","analyzedAt":"2026-08-15T12:26:37.444Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}