{"record":{"id":"6b6041b29eb1b36f","repo":"decolua/9router","slug":"token-exchange-failed-error-6b6041","errorCode":null,"errorMessage":"Token exchange failed: ${error}","messagePattern":"Token exchange failed: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/lib/oauth/services/iflow.js","lineNumber":59,"sourceCode":"    const response = await fetch(this.config.tokenUrl, {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/x-www-form-urlencoded\",\n        Accept: \"application/json\",\n        Authorization: `Basic ${basicAuth}`,\n      },\n      body: new URLSearchParams({\n        grant_type: \"authorization_code\",\n        code: code,\n        redirect_uri: redirectUri,\n        client_id: this.config.clientId,\n        client_secret: this.config.clientSecret,\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   * Get user info from iFlow\n   */\n  async getUserInfo(accessToken) {\n    const response = await fetch(\n      `${this.config.userInfoUrl}?accessToken=${encodeURIComponent(accessToken)}`,\n      {\n        headers: {\n          Accept: \"application/json\",\n        },\n      }\n    );\n","sourceCodeStart":41,"sourceCodeEnd":77,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/src/lib/oauth/services/iflow.js#L41-L77","documentation":"Thrown by IFlowService.exchangeCode() when the iFlow OAuth token endpoint responds with a non-2xx status to the authorization-code exchange POST. The raw response body (text) is embedded in the message, so it usually contains the upstream OAuth error such as invalid_grant, invalid_client, or redirect_uri mismatch. Basic auth (clientId:clientSecret) is sent alongside the form body, so either can be rejected.","triggerScenarios":"exchangeCode(code, redirectUri) is called with an authorization code that is expired, already redeemed, or issued for a different redirect_uri; or the client credentials in IFLOW_CONFIG (clientId/clientSecret) are wrong/revoked; or the redirect_uri passed does not exactly match the one used in buildAuthUrl (including the random local port).","commonSituations":"Re-running connect() and reusing a stale callback code; proxy/firewall rewriting the token endpoint response; iFlow rotating the shared client secret; the localhost callback port differing between authorize and token calls after a retry.","solutions":["Read the embedded upstream error in the message: invalid_grant → restart the login flow to get a fresh code; invalid_client → check IFLOW_CONFIG credentials.","Ensure redirect_uri passed to exchangeCode is byte-identical to the one in the authorization URL (same localhost port).","Restart the whole `connect()` flow — authorization codes are single-use and short-lived.","If invalid_client persists, verify the iFlow clientId/clientSecret constants are current.","Check network/proxy interference with the token endpoint (curl the tokenUrl directly)."],"exampleFix":"// before\nif (!response.ok) {\n  const error = await response.text();\n  throw new Error(`Token exchange failed: ${error}`);\n}\n// after (include status for faster 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":"// Codes are single-use and short-lived; validate inputs before exchanging\nif (!code || typeof code !== \"string\") throw new Error(\"Missing authorization code\");\nif (!redirectUri || !redirectUri.startsWith(\"http://localhost:\")) throw new Error(\"redirect_uri must match the one used in the authorize URL\");","typeGuard":"function isOAuthErrorBody(text) {\n  try { const j = JSON.parse(text); return typeof j.error === \"string\"; } catch { return false; }\n}","tryCatchPattern":"try {\n  const tokens = await iflowService.exchangeCode(code, redirectUri);\n} catch (err) {\n  if (/Token exchange failed/.test(err.message)) {\n    if (/invalid_grant/i.test(err.message)) {\n      console.error(\"Authorization code expired or already used — restart the login flow.\");\n    } else if (/invalid_client/i.test(err.message)) {\n      console.error(\"Bad clientId/clientSecret — check IFLOW_CONFIG.\");\n    }\n  } else { throw err; }\n}","preventionTips":["Never reuse an authorization code — always restart the full connect() flow on any failure.","Ensure redirect_uri is identical in authorize and token calls (same port, same path).","Keep IFLOW_CONFIG client credentials up to date.","Log response.status alongside the body to classify failures quickly."],"tags":["oauth","http","token-exchange","network"],"backgroundTag":"oauth-token-exchange-failed","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}