{"record":{"id":"100bb39310f9fd09","repo":"mastra-ai/mastra","slug":"token-exchange-failed-error-100bb3","errorCode":null,"errorMessage":"Token exchange failed: ${error}","messagePattern":"Token exchange failed: (.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"mastracode/sdk/src/auth/providers/anthropic.ts","lineNumber":91,"sourceCode":"    // caller (and, in the shipyard server, the containing project lock)\n    // indefinitely. See 2025-07-23 shipyard latency incident.\n    signal: AbortSignal.timeout(15_000),\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    body: JSON.stringify({\n      grant_type: 'authorization_code',\n      client_id: CLIENT_ID,\n      code,\n      state,\n      redirect_uri: REDIRECT_URI,\n      code_verifier: verifier,\n    }),\n  });\n\n  if (!tokenResponse.ok) {\n    const error = await tokenResponse.text();\n    throw new Error(`Token exchange failed: ${error}`);\n  }\n\n  const tokenData = (await tokenResponse.json()) as {\n    access_token: string;\n    refresh_token: string;\n    expires_in: number;\n  };\n\n  // Calculate expiry time (current time + expires_in seconds - 5 min buffer)\n  const expiresAt = Date.now() + tokenData.expires_in * 1000 - 5 * 60 * 1000;\n\n  return {\n    refresh: tokenData.refresh_token,\n    access: tokenData.access_token,\n    expires: expiresAt,\n  };\n}\n","sourceCodeStart":73,"sourceCodeEnd":109,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/mastracode/sdk/src/auth/providers/anthropic.ts#L73-L109","documentation":"After validating the code and state, `completeAnthropicLogin` POSTs to Anthropic's token endpoint (console.anthropic.com/v1/oauth/token) with grant_type=authorization_code. If the HTTP response is not ok, it reads the response body as text and throws 'Token exchange failed: <body>'. The embedded text is Anthropic's own error payload (e.g. invalid_grant, invalid_client) and is the key to diagnosing the failure.","triggerScenarios":"The authorization code was already redeemed (invalid_grant) — e.g. a retry or two callers exchanging the same code; the code expired (Anthropic codes are short-lived); the PKCE code_verifier does not match the challenge sent at authorize time; a client_id/endpoint mismatch; or any 4xx/5xx from the token endpoint including timeouts raised by the built-in 15s AbortSignal.timeout.","commonSituations":"Double-exchange after a timeout retry; user waited too long between authorizing and pasting the code; copying the code from a stale browser tab started with an older verifier/challenge; Anthropic endpoint changes or temporary 5xx; proxy/firewall mangling the POST.","solutions":["Read the embedded error text: invalid_grant usually means the code was consumed or expired — restart with startAnthropicLogin() and re-authorize; do not reuse the same code.","Ensure each code is exchanged exactly once; make retries generate a fresh login instead of resubmitting the same code.","Verify the verifier passed to completeAnthropicLogin is the one from the startAnthropicLogin() call that generated the authorization URL (PKCE binding).","For 5xx/network errors, retry with fresh backoff only after confirming the code has not been redeemed; a redeemed code will never succeed again.","Check outbound network/proxy access to console.anthropic.com; the request is bounded by a 15s timeout."],"exampleFix":"// before\n// retrying the exchange after a timeout reuses the consumed code\ntry {\n  await completeAnthropicLogin(input, verifier);\n} catch {\n  await completeAnthropicLogin(input, verifier); // 'Token exchange failed: invalid_grant'\n}\n// after\ntry {\n  await completeAnthropicLogin(input, verifier);\n} catch (e) {\n  // code is single-use: always restart the flow for a fresh code\n  const { url, verifier: v2 } = await startAnthropicLogin();\n  showAuthUrl(url);\n  const freshInput = await promptForCode();\n  await completeAnthropicLogin(freshInput, v2);\n}","handlingStrategy":"try-catch","validationCode":"// pre-checks: code present and exchange not already attempted for this code\nif (!code) throw new Error('No authorization code to exchange');\nif (await exchangedCodes.has(code)) throw new Error('Code already redeemed — restart the login');","typeGuard":"function isTokenExchangeError(e: unknown): e is Error & { message: string } {\n  return e instanceof Error && e.message.startsWith('Token exchange failed:');\n}","tryCatchPattern":"try {\n  return await completeAnthropicLogin(input, verifier);\n} catch (e) {\n  if (isTokenExchangeError(e)) {\n    if (/invalid_grant/i.test(e.message)) {\n      // code consumed or expired: single-use — restart the flow, never retry same code\n      return startFreshLogin();\n    }\n    if (/\\b5\\d\\d\\b|timeout|aborted/i.test(e.message)) {\n      await sleep(1000); // transient: safe retry only if code not yet redeemed\n    }\n  }\n  throw e;\n}","preventionTips":["Treat authorization codes as strictly single-use; never retry an exchange with the same code after a failure.","Complete the exchange promptly — codes expire within minutes of issuance.","Keep the PKCE verifier from the exact startAnthropicLogin() call that produced the URL.","Wrap the fetch with your own timeout/retry policy for 5xx, matching the built-in 15s AbortSignal.timeout.","Log the embedded upstream error text to distinguish auth failures from transient outages."],"tags":["oauth","anthropic","token-exchange","http-error","pkce"],"backgroundTag":"oauth-token-exchange-failed","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}