{"record":{"id":"69555ae46c27bf19","repo":"slopus/happy","slug":"token-exchange-failed-error","errorCode":null,"errorMessage":"Token exchange failed: ${error}","messagePattern":"Token exchange failed: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/happy-cli/src/commands/connect/authenticateCodex.ts","lineNumber":111,"sourceCode":"    port: number\n): Promise<CodexAuthTokens> {\n    const response = await fetch(`${AUTH_BASE_URL}/oauth/token`, {\n        method: 'POST',\n        headers: {\n            'Content-Type': 'application/x-www-form-urlencoded',\n        },\n        body: new URLSearchParams({\n            grant_type: 'authorization_code',\n            client_id: CLIENT_ID,\n            code: code,\n            code_verifier: verifier,\n            redirect_uri: `http://localhost:${port}/auth/callback`,\n        }),\n    });\n\n    if (!response.ok) {\n        const error = await response.text();\n        throw new Error(`Token exchange failed: ${error}`);\n    }\n\n    const data = (await response.json() as any);\n\n    // Parse ID token to get account ID\n    const idTokenPayload = parseJWT(data.id_token);\n\n    // The account ID is stored at chatgpt_account_id in the payload\n    let accountId = idTokenPayload.chatgpt_account_id;\n\n    // Check nested location\n    if (!accountId) {\n        const authClaim = idTokenPayload['https://api.openai.com/auth'];\n        if (authClaim && typeof authClaim === 'object') {\n            accountId = authClaim.chatgpt_account_id || authClaim.account_id;\n        }\n    }\n","sourceCodeStart":93,"sourceCodeEnd":129,"githubUrl":"https://github.com/slopus/happy/blob/b824cd0a4681d41af631a8e422a813873e4455b0/packages/happy-cli/src/commands/connect/authenticateCodex.ts#L93-L129","documentation":"Thrown by exchangeCodeForTokens for the Codex (OpenAI) OAuth flow when the token endpoint responds non-OK. Unlike the Claude variant, it includes the response body text in the message, so the server's error description is visible.","triggerScenarios":"POST to the OpenAI token endpoint with authorization_code, code_verifier, and redirect_uri http://localhost:<port>/auth/callback returns !response.ok — expired/replayed code, PKCE mismatch, client misconfiguration, or 5xx.","commonSituations":"Callback server bound to a different port than redirect_uri, user retried an already-consumed code, corporate proxy intercepting localhost callback or outbound call, provider outage.","solutions":["Read the error body in the message and follow the provider's documented cause (e.g. invalid_grant)","Re-run the Codex authentication flow for a fresh authorization code","Ensure the callback port matches the redirect_uri exactly","Update the CLI if the provider changed token-endpoint requirements"],"exampleFix":"// before\nif (!response.ok) {\n    const error = await response.text();\n    throw new Error(`Token exchange failed: ${error}`);\n}\n// after\nif (!response.ok) {\n    const error = await response.text();\n    throw new Error(`Token exchange failed (HTTP ${response.status}): ${error}`);\n}","handlingStrategy":"try-catch","validationCode":"// Confirm the callback port matches the redirect_uri before exchanging\nif (!redirectUri.startsWith(`http://localhost:${port}/auth/callback`)) {\n  throw new Error('redirect_uri/port mismatch');\n}","typeGuard":null,"tryCatchPattern":"try {\n  await exchangeCodeForTokens(code, port);\n} catch (err) {\n  if (String(err.message).startsWith('Token exchange failed')) {\n    console.error('Codex token exchange rejected:', err.message);\n    // restart auth to get a fresh code\n    await runCodexAuth();\n  } else throw err;\n}","preventionTips":["Complete auth promptly; codes expire quickly","Never reuse an authorization code across attempts","Keep callback port consistent with redirect_uri","Check provider status page / proxy interference on repeated failures"],"tags":["oauth","network","authentication"],"backgroundTag":"oauth-token-exchange-failed","analyzedSha":"b824cd0a4681d41af631a8e422a813873e4455b0","analyzedAt":"2026-08-31T23:12:36.205Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}