{"record":{"id":"7a5950182a770898","repo":"can1357/oh-my-pi","slug":"token-exchange-failed-formatopenaicodextokenend","errorCode":null,"errorMessage":"Token exchange failed: ${formatOpenAICodexTokenEndpointError(tokenResponse.status, bodyText)}","messagePattern":"Token exchange failed: (.+?)","errorType":"exception","errorClass":"AIError.OAuthError","httpStatus":null,"severity":"error","filePath":"packages/ai/src/registry/oauth/openai-codex.ts","lineNumber":193,"sourceCode":"\tredirectUri: string,\n\tfetchImpl: FetchImpl = fetch,\n): Promise<OAuthCredentials> {\n\tconst tokenResponse = await fetchImpl(TOKEN_URL, {\n\t\tmethod: \"POST\",\n\t\theaders: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n\t\tbody: new URLSearchParams({\n\t\t\tgrant_type: \"authorization_code\",\n\t\t\tclient_id: CLIENT_ID,\n\t\t\tcode,\n\t\t\tcode_verifier: verifier,\n\t\t\tredirect_uri: redirectUri,\n\t\t}),\n\t\tsignal: AbortSignal.timeout(TOKEN_REQUEST_TIMEOUT_MS),\n\t});\n\n\tif (!tokenResponse.ok) {\n\t\tconst bodyText = await tokenResponse.text();\n\t\tthrow new AIError.OAuthError(\n\t\t\t`Token exchange failed: ${formatOpenAICodexTokenEndpointError(tokenResponse.status, bodyText)}`,\n\t\t\t{ kind: \"token-exchange\", status: tokenResponse.status },\n\t\t);\n\t}\n\n\tconst tokenData = (await tokenResponse.json()) as {\n\t\taccess_token?: string;\n\t\trefresh_token?: string;\n\t\tid_token?: string;\n\t\texpires_in?: number;\n\t};\n\n\tif (!tokenData.access_token || !tokenData.refresh_token || typeof tokenData.expires_in !== \"number\") {\n\t\tthrow new AIError.OAuthError(\"Token response missing required fields\", { kind: \"validation\" });\n\t}\n\n\tconst { accountId, email, planType } = getTokenProfile(tokenData.access_token, tokenData.id_token);\n\tif (!accountId) {","sourceCodeStart":175,"sourceCodeEnd":211,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/ai/src/registry/oauth/openai-codex.ts#L175-L211","documentation":"Thrown by exchangeCodeForToken in the OpenAI Codex OAuth flow when the token endpoint responds with a non-ok HTTP status. The message embeds a formatted summary of the status and response body (formatOpenAICodexTokenEndpointError) and the error carries kind='token-exchange' plus the numeric status. It means the authorization code (or refresh token) could not be exchanged for an access token.","triggerScenarios":"Exchanging an authorization code that was already used (codes are single-use); the code expired (typically ~ minutes after login); client_id mismatch between the authorize and token requests; PKCE verifier mismatch; OpenAI returning 4xx/5xx from the token endpoint. Called from both exchangeToken (manual paste) and loginOpenAICodexDevice.","commonSituations":"User pastes an authorization code a second time after a first attempt failed downstream; user takes too long between opening the auth URL and completing exchange; clock skew invalidating PKCE; OpenAI API incident; proxy stripping POST body params.","solutions":["Redo the login/authorization flow to get a fresh code — used or expired codes cannot be retried","Check the formatted status/body in the message: 400 invalid_grant means the code is spent/expired; 401 means client credentials issue","Update the CLI so the embedded client_id/PKCE handling matches current OpenAI requirements","If status is 5xx, wait and retry; check OpenAI status page for incidents"],"exampleFix":"// before: single-shot exchange crashes on hiccup\nconst tokens = await exchangeCodeForToken(code, verifier);\n// after: detect invalid_grant and restart auth\ntry {\n  const tokens = await exchangeCodeForToken(code, verifier);\n} catch (e) {\n  if (e instanceof AIError.OAuthError && e.kind === 'token-exchange') {\n    console.error('Code rejected — restarting login:', e.message);\n    return startLoginFlow();\n  }\n  throw e;\n}","handlingStrategy":"try-catch","validationCode":"// exchange codes promptly; codes are single-use and short-lived\nif (Date.now() - codeReceivedAtMs > 5 * 60 * 1000) {\n  // code likely expired — restart the authorization flow instead of exchanging\n}","typeGuard":null,"tryCatchPattern":"try {\n  const tokens = await exchangeCodeForToken(code, verifier);\n} catch (e) {\n  if (e instanceof AIError.OAuthError && e.kind === 'token-exchange') {\n    // e.status: 400 invalid_grant → get a fresh code; 5xx → retry\n    if (e.status === 400) return restartLogin();\n  }\n  throw e;\n}","preventionTips":["Never reuse an authorization code — each is single-use","Complete authorization and exchange within the code lifetime","Keep client_id and PKCE verifier consistent between authorize and token requests","Update the CLI when OpenAI rotates OAuth clients"],"tags":["oauth","openai-codex","token-exchange","http-error"],"backgroundTag":"oauth-token-exchange-failed","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}