{"record":{"id":"1f0f9e4d1abe439f","repo":"decolua/9router","slug":"token-exchange-failed-error-1f0f9e","errorCode":null,"errorMessage":"Token exchange failed: ${error}","messagePattern":"Token exchange failed: (.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/lib/oauth/services/openai.js","lineNumber":54,"sourceCode":"  async exchangeOpenAICode(code, redirectUri, codeVerifier) {\n    const response = await fetch(OPENAI_CONFIG.tokenUrl, {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/x-www-form-urlencoded\",\n        Accept: \"application/json\",\n      },\n      body: new URLSearchParams({\n        grant_type: \"authorization_code\",\n        client_id: OPENAI_CONFIG.clientId,\n        code: code,\n        redirect_uri: redirectUri,\n        code_verifier: codeVerifier,\n      }),\n    });\n\n    if (!response.ok) {\n      const error = await response.text();\n      throw new Error(`Token exchange failed: ${error}`);\n    }\n\n    return await response.json();\n  }\n\n  /**\n   * Save OpenAI tokens to server\n   */\n  async saveTokens(tokens) {\n    const { server, token, userId } = getServerCredentials();\n\n    const response = await fetch(`${server}/api/cli/providers/openai`, {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n        Authorization: `Bearer ${token}`,\n        \"X-User-Id\": userId,\n      },","sourceCodeStart":36,"sourceCodeEnd":72,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/src/lib/oauth/services/openai.js#L36-L72","documentation":"OpenAI-specific override of the generic token exchange: exchangeOpenAICode() POSTs the authorization code, client_id, redirect_uri, and code_verifier to OPENAI_CONFIG.tokenUrl, and throws when the response is not ok. The raw response body (typically `invalid_grant` or `invalid_client` JSON) is included in the message. Raised on the path exchangeOpenAICode <- tokens/connect flow.","triggerScenarios":"connect() -> authenticate() succeeds, then exchangeOpenAICode(code, redirectUri, codeVerifier) gets !response.ok from OpenAI's token endpoint — the one-time code expired or was redeemed, code_verifier doesn't match the challenge, redirect_uri (the ephemeral localhost:port/callback) differs from the authorize request, or the registered OpenAI client_id is rejected.","commonSituations":"Waiting too long between browser approval and exchange (code TTL); re-running after a partial failure reuses the old code; a different port bound on restart changes redirect_uri; OpenAI rotating/invalidating the client_id packaged with the CLI; network egress blocked to auth.openai.com.","solutions":["Run the full connect() flow again to get a fresh code and exchange it immediately; the code is single-use and short-lived.","Verify the embedded body text for the exact OAuth error and address it (invalid_grant -> fresh code; invalid_client -> client_id).","Ensure redirect_uri is the exact same localhost URL used in the authorize step (same port) — don't restart the CLI between authorize and exchange.","Check outbound HTTPS access to OpenAI's token endpoint (proxy/VPN can return an HTML block page).","Update the CLI — a stale bundled OPENAI_CONFIG.clientId can be rejected by OpenAI."],"exampleFix":"// before\nconst tokens = await this.exchangeOpenAICode(code, redirectUri, codeVerifier);\n// after (fresh flow per attempt, immediate exchange)\nconst { code, codeVerifier, redirectUri } = await this.authenticate(\"OpenAI\", this.buildOpenAIAuthUrl.bind(this));\nconst tokens = await this.exchangeOpenAICode(code, redirectUri, codeVerifier); // exchange right away, no restart in between","handlingStrategy":"try-catch","validationCode":"// Pre-exchange checks:\nif (!code) throw new Error(\"No authorization code to exchange\");\nif (!redirectUri.startsWith(\"http://localhost:\")) throw new Error(\"redirect_uri must match the authorize-time localhost URL\");","typeGuard":"function isOpenAITokenResponse(json) {\n  return json != null && typeof json === \"object\" && typeof json.access_token === \"string\";\n}","tryCatchPattern":"try {\n  const tokens = await service.exchangeOpenAICode(code, redirectUri, codeVerifier);\n} catch (err) {\n  if (err.message.startsWith(\"Token exchange failed:\")) {\n    if (err.message.includes(\"invalid_grant\")) { /* restart full flow for a fresh code */ }\n    else if (err.message.includes(\"invalid_client\")) { /* update CLI / check OPENAI_CONFIG.clientId */ }\n    else throw err;\n  } else throw err;\n}","preventionTips":["Do the authorize->exchange without restarts so redirect_uri port stays identical.","Exchange the fresh code immediately; don't cache it across runs.","Keep the CLI updated so the bundled OpenAI client_id/config stays valid.","Verify outbound access to OpenAI's token endpoint through proxies/VPNs."],"tags":["oauth","openai","token-exchange","pkce"],"backgroundTag":"oauth-token-exchange-failed","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}