{"record":{"id":"e95744ab1879e2d1","repo":"router-for-me/CLIProxyAPI","slug":"token-exchange-failed-with-status-d-s-e95744","errorCode":null,"errorMessage":"token exchange failed with status %d: %s","messagePattern":"token exchange failed with status (.+?): (.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/auth/codex/openai_auth.go","lineNumber":139,"sourceCode":"\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tresp, err := o.httpClient.Do(req)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"token exchange request failed: %w\", err)\n\t}\n\tdefer func() {\n\t\t_ = resp.Body.Close()\n\t}()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read token response: %w\", err)\n\t}\n\t// log.Debugf(\"Token response: %s\", string(body))\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, fmt.Errorf(\"token exchange failed with status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\t// Parse token response\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 err = json.Unmarshal(body, &tokenResp); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse token response: %w\", err)\n\t}\n\n\t// Extract account ID from ID token\n\tclaims, err := ParseJWTToken(tokenResp.IDToken)\n\tif err != nil {","sourceCodeStart":121,"sourceCodeEnd":157,"githubUrl":"https://github.com/router-for-me/CLIProxyAPI/blob/78f0c4079e3e6273d65d03b5549cffc898703264/internal/auth/codex/openai_auth.go#L121-L157","documentation":"The Codex token endpoint answered the exchange request with a non-200 status; the message includes the status code and the raw response body. This is the provider rejecting the exchange itself — the authorization code was invalid/expired/already used, the PKCE verifier did not match the challenge, the redirect_uri differed from the one in the authorization request, or the endpoint rate-limited the client.","triggerScenarios":"Replaying an authorization code (codes are single-use); code expired before exchange; code_verifier regenerated between authorize and exchange steps; redirect_uri in the token request differs from the callback the code was issued for; 429 rate limiting from repeated login attempts; OpenAI-side incident returning 5xx.","commonSituations":"Restarting the login mid-flow and pasting an old code; clocks/edits causing verifier mismatch; hammering the login command in a loop; upstream OpenAI auth outage (check status.openai.com).","solutions":["Read the status and body in the message: 400 + invalid_grant means the code is stale/used — start a fresh login, do not reuse the old URL or code.","Ensure the whole flow (authorize URL → callback → exchange) happens in one run with no restarts in between.","On 429, wait before retrying login.","On 5xx, check the provider status page and retry later.","If persistent, verify ClientID/TokenURL constants and redirect URI wiring are unmodified."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Never reuse a code; check it looks fresh before exchanging\nif code == \"\" || len(code) < 16 {\n    return errors.New(\"authorization code missing or truncated; restart login\")\n}","typeGuard":null,"tryCatchPattern":"tok, err := auth.ExchangeCode(ctx, code)\nif err != nil {\n    msg := err.Error()\n    switch {\n    case strings.Contains(msg, \"invalid_grant\"), strings.Contains(msg, \"status 400\"):\n        // code expired/used: MUST start a brand-new login, do not retry exchange\n    case strings.Contains(msg, \"status 429\"):\n        // rate limited: back off before next login attempt\n    case strings.Contains(msg, \"status 5\"):\n        // provider incident: retry later\n    }\n}","preventionTips":["Complete authorize→callback→exchange in a single uninterrupted flow.","Never copy/paste authorization codes between attempts.","Rate-limit automated login scripts to avoid 429s."],"tags":["oauth","codex","http-status","invalid-grant","pkce","auth-flow"],"backgroundTag":null,"analyzedSha":"78f0c4079e3e6273d65d03b5549cffc898703264","analyzedAt":"2026-08-15T12:26:37.444Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}