{"record":{"id":"fb1a4e52dac5aa4a","repo":"decolua/9router","slug":"token-exchange-failed-error-fb1a4e","errorCode":null,"errorMessage":"Token exchange failed: ${error}","messagePattern":"Token exchange failed: (.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/lib/oauth/services/oauth.js","lineNumber":116,"sourceCode":"            grant_type: \"authorization_code\",\n            client_id: this.config.clientId,\n            code: code,\n            redirect_uri: redirectUri,\n            code_verifier: codeVerifier,\n          });\n\n    const response = await fetch(this.config.tokenUrl, {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": contentType,\n        Accept: \"application/json\",\n      },\n      body: body,\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   * Complete OAuth flow\n   */\n  async authenticate(providerName, buildAuthUrlFn) {\n    // Generate PKCE\n    const { codeVerifier, codeChallenge, state } = generatePKCE();\n\n    // Start local server and get redirect URI\n    const { redirectUri, waitForCallback } = await this.startAuthFlow(null, providerName);\n\n    // Build authorization URL\n    const authUrl = buildAuthUrlFn(redirectUri, state, codeChallenge);\n","sourceCodeStart":98,"sourceCodeEnd":134,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/src/lib/oauth/services/oauth.js#L98-L134","documentation":"Thrown by OAuthService.exchangeCode() when the POST to the provider's token endpoint returns a non-2xx response. The response body text (which usually contains the OAuth error JSON such as `invalid_grant`, `invalid_client`, or `redirect_uri_mismatch`) is embedded verbatim in the message. It means the authorization code could not be swapped for tokens.","triggerScenarios":"exchangeCode(code, redirectUri, codeVerifier, contentType) posts grant_type=authorization_code to config.tokenUrl and the upstream replies !response.ok — e.g. the code was already redeemed or expired, code_verifier doesn't match the challenge, redirect_uri differs from the authorize request, clientId is wrong, or the Content-Type the provider expects (form vs JSON) was mismatched.","commonSituations":"Re-running the flow and reusing the old one-time code; PKCE verifier/challenge mismatch across restarted CLI runs; redirect_uri changed because the local callback server got a different port; provider requires HTTP Basic client auth instead of body credentials; proxy/firewall returning an HTML error page.","solutions":["Read the embedded body text — it names the exact OAuth error (invalid_grant, invalid_client, redirect_uri_mismatch) and fix that specific cause.","Restart the whole auth flow to get a fresh, unused authorization code; codes are single-use and short-lived.","Ensure the redirect_uri passed to exchangeCode is byte-identical to the one used in the authorize URL (same port).","Verify clientId and PKCE code_verifier match the challenge sent in buildAuthUrl; if regenerating, regenerate both together.","Try the alternate contentType (application/json vs application/x-www-form-urlencoded) if the provider's token endpoint rejects the default."],"exampleFix":"// before (opaque text blob)\nconst error = await response.text();\nthrow new Error(`Token exchange failed: ${error}`);\n// after (surface status + parsed OAuth error)\nconst text = await response.text();\nlet detail = text;\ntry { detail = JSON.parse(text).error_description || JSON.parse(text).error || text; } catch {}\nthrow new Error(`Token exchange failed (HTTP ${response.status}): ${detail}`);","handlingStrategy":"try-catch","validationCode":"// Pre-flight sanity checks before calling exchangeCode:\nif (!code || !codeVerifier || !redirectUri) throw new Error(\"Missing code/verifier/redirectUri before exchange\");\nif (!tokenUrl.startsWith(\"https://\")) throw new Error(\"Token URL must be https\");","typeGuard":"function isTokenResponse(json) {\n  return json != null && typeof json === \"object\" && typeof json.access_token === \"string\";\n}","tryCatchPattern":"try {\n  const tokens = await service.exchangeCode(code, redirectUri, codeVerifier);\n} catch (err) {\n  if (err.message.startsWith(\"Token exchange failed:\")) {\n    if (err.message.includes(\"invalid_grant\")) { /* code expired/used — rerun auth */ }\n    else if (err.message.includes(\"redirect_uri\")) { /* align redirect_uri with authorize call */ }\n    else throw err;\n  } else throw err;\n}","preventionTips":["Exchange the code immediately after receiving it — codes are single-use and expire in minutes.","Never restart the CLI between authorize and exchange if it rebinds a different localhost port.","Keep PKCE verifier/challenge generation and use inside a single flow instance.","Match the Content-Type the provider's token endpoint requires (form-urlencoded vs JSON).","Check provider status pages / network egress before attributing failures to your code."],"tags":["oauth","token-exchange","http","pkce"],"backgroundTag":"oauth-token-exchange-failed","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}