{"record":{"id":"0e646667706019cb","repo":"decolua/9router","slug":"token-exchange-failed-error-0e6466","errorCode":null,"errorMessage":"Token exchange failed: ${error}","messagePattern":"Token exchange failed: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/lib/oauth/services/claude.js","lineNumber":67,"sourceCode":"      state: codeState || state,\n      grant_type: \"authorization_code\",\n      client_id: CLAUDE_CONFIG.clientId,\n      redirect_uri: redirectUri,\n      code_verifier: codeVerifier,\n    };\n\n    const response = await fetch(CLAUDE_CONFIG.tokenUrl, {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n        Accept: \"application/json\",\n      },\n      body: JSON.stringify(tokenPayload),\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 Claude tokens to server\n   */\n  async saveTokens(tokens) {\n    const { server, token, userId } = getServerCredentials();\n\n    // Server will auto-generate displayName based on existing account count\n    const response = await fetch(`${server}/api/cli/providers/claude`, {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n        Authorization: `Bearer ${token}`,\n        \"X-User-Id\": userId,","sourceCodeStart":49,"sourceCodeEnd":85,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/src/lib/oauth/services/claude.js#L49-L85","documentation":"Thrown by exchangeClaudeCode when Anthropic's OAuth token endpoint (CLAUDE_CONFIG.tokenUrl) returns a non-2xx status to the authorization_code grant request. The raw response body (Anthropic's OAuth error JSON, e.g. invalid_grant) is appended to the message. The request is sent as JSON (not form-urlencoded) with a PKCE code_verifier and optional `#`-delimited state parsed out of the code.","triggerScenarios":"Authorization code already used or expired (invalid_grant); code_verifier doesn't match the PKCE challenge; redirect_uri differs from the one used in the authorize step; state mismatch; malformed code string from a mangled callback.","commonSituations":"Re-running connect with a stale code from a previous attempt (codes are single-use); user taking too long between authorize and exchange; local callback port changed between authorize and token calls; Claude OAuth endpoint outage or client_id rotation.","solutions":["Read the appended response body — it names Anthropic's exact OAuth error (usually invalid_grant)","Re-run the full connect flow to get a fresh authorization code; codes are single-use and short-lived","Ensure redirect_uri passed to exchangeClaudeCode is byte-identical to the one used in buildClaudeAuthUrl","Verify the code_verifier belongs to the same session/state as the code (a restarted CLI generates a new verifier)"],"exampleFix":"// before: raw body only\nif (!response.ok) {\n  const error = await response.text();\n  throw new Error(`Token exchange failed: ${error}`);\n}\n// after: surface status code too for easier diagnosis\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":"if (!code) throw new Error('No authorization code to exchange');\nif (!codeVerifier || codeVerifier.length < 43) throw new Error('Invalid PKCE code_verifier — restart the full connect flow');\nif (redirectUri !== originalAuthorizeRedirectUri) throw new Error('redirect_uri mismatch between authorize and token steps');","typeGuard":"function isExchangable(ctx) { return ctx && typeof ctx.code === 'string' && ctx.code.length > 0 && typeof ctx.codeVerifier === 'string' && typeof ctx.redirectUri === 'string'; }","tryCatchPattern":"try {\n  const tokens = await service.exchangeClaudeCode(code, redirectUri, codeVerifier, state);\n} catch (e) {\n  if (e.message.startsWith('Token exchange failed')) {\n    if (e.message.includes('invalid_grant')) {\n      console.error('Code used/expired — restart connect for a fresh code.');\n    } else throw e;\n  } else throw e;\n}","preventionTips":["Never reuse an authorization code — they are single-use and expire in minutes","Pass the identical redirect_uri used in the authorize step","Keep code_verifier and code from the same session (don't mix restarted runs)","Check status.cloude.com / Anthropic status before concluding client misconfig"],"tags":["oauth","token-exchange","anthropic","pkce"],"backgroundTag":"oauth-token-exchange-failed","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}