{"record":{"id":"7147d89735472069","repo":"router-for-me/CLIProxyAPI","slug":"failed-to-parse-token-response-w-7147d8","errorCode":null,"errorMessage":"failed to parse token response: %w","messagePattern":"failed to parse token response: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/auth/codex/openai_auth.go","lineNumber":152,"sourceCode":"\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 {\n\t\tlog.Warnf(\"Failed to parse ID token: %v\", err)\n\t}\n\n\taccountID := \"\"\n\temail := \"\"\n\tif claims != nil {\n\t\taccountID = claims.GetAccountID()\n\t\temail = claims.GetUserEmail()\n\t}\n\n\t// Create token data\n\ttokenData := CodexTokenData{\n\t\tIDToken:      tokenResp.IDToken,","sourceCodeStart":134,"sourceCodeEnd":170,"githubUrl":"https://github.com/router-for-me/CLIProxyAPI/blob/78f0c4079e3e6273d65d03b5549cffc898703264/internal/auth/codex/openai_auth.go#L134-L170","documentation":"The token exchange returned HTTP 200 but the body was not valid JSON for the expected token shape — json.Unmarshal failed. A 200 with malformed JSON usually means an intermediary (captive portal, proxy, antivirus) rewrote the response, or the endpoint returned an unexpected content type like HTML. The struct expects access_token/refresh_token/id_token fields; missing fields alone do NOT trigger this error, only syntactically invalid JSON does.","triggerScenarios":"Captive portal or proxy injecting an HTML page with status 200; response body truncated to invalid JSON; endpoint returning a JSON error envelope that is syntactically fine but semantically empty (that would parse, so true triggers are syntax-level); encoding mismatch after TLS interception.","commonSituations":"Hotel/airport Wi-Fi captive portals; MITM proxies that rewrite auth-domain responses; rare upstream incidents serving error pages with 200.","solutions":["Log or inspect the raw body (temporarily re-enable the commented log.Debugf line) to see what was actually returned.","Identify and bypass the intermediary — captive portal login first, or exclude the auth host from the MITM proxy.","Retry from a clean network to confirm the response is provider JSON.","Report upstream if the raw body is genuinely malformed provider output."],"exampleFix":"// before\n// log.Debugf(\"Token response: %s\", string(body))\n\n// after (temporary debugging)\nlog.Debugf(\"Token response: %s\", string(body))","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"tok, err := auth.ExchangeCode(ctx, code)\nif err != nil && strings.Contains(err.Error(), \"failed to parse token response\") {\n    // 200 + non-JSON: almost always a portal/proxy rewriting the response\n    log.Errorf(\"auth endpoint returned non-JSON; check captive portal/proxy: %v\", err)\n}","preventionTips":["Complete captive-portal authentication before starting OAuth login.","Bypass MITM proxies for the provider's auth domain.","Enable debug body logging when diagnosing parse failures."],"tags":["oauth","codex","json","proxy","captive-portal","auth-flow"],"backgroundTag":null,"analyzedSha":"78f0c4079e3e6273d65d03b5549cffc898703264","analyzedAt":"2026-08-15T12:26:37.444Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}