decolua/9router · error · Error

Token exchange failed: ${error}

Error message

Token exchange failed: ${error}

What it means

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.

Source

Thrown at src/lib/oauth/services/iflow.js:59

    const response = await fetch(this.config.tokenUrl, {
      method: "POST",
      headers: {
        "Content-Type": "application/x-www-form-urlencoded",
        Accept: "application/json",
        Authorization: `Basic ${basicAuth}`,
      },
      body: new URLSearchParams({
        grant_type: "authorization_code",
        code: code,
        redirect_uri: redirectUri,
        client_id: this.config.clientId,
        client_secret: this.config.clientSecret,
      }),
    });

    if (!response.ok) {
      const error = await response.text();
      throw new Error(`Token exchange failed: ${error}`);
    }

    return await response.json();
  }

  /**
   * Get user info from iFlow
   */
  async getUserInfo(accessToken) {
    const response = await fetch(
      `${this.config.userInfoUrl}?accessToken=${encodeURIComponent(accessToken)}`,
      {
        headers: {
          Accept: "application/json",
        },
      }
    );

View on GitHub (pinned to 90b52e06ff)

Solutions

  1. 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.
  2. Ensure redirect_uri passed to exchangeCode is byte-identical to the one in the authorization URL (same localhost port).
  3. Restart the whole `connect()` flow — authorization codes are single-use and short-lived.
  4. If invalid_client persists, verify the iFlow clientId/clientSecret constants are current.
  5. Check network/proxy interference with the token endpoint (curl the tokenUrl directly).

Example fix

// before
if (!response.ok) {
  const error = await response.text();
  throw new Error(`Token exchange failed: ${error}`);
}
// after (include status for faster diagnosis)
if (!response.ok) {
  const error = await response.text();
  throw new Error(`Token exchange failed (HTTP ${response.status}): ${error}`);
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Codes are single-use and short-lived; validate inputs before exchanging
if (!code || typeof code !== "string") throw new Error("Missing authorization code");
if (!redirectUri || !redirectUri.startsWith("http://localhost:")) throw new Error("redirect_uri must match the one used in the authorize URL");

Type guard

function isOAuthErrorBody(text) {
  try { const j = JSON.parse(text); return typeof j.error === "string"; } catch { return false; }
}

Try / catch

try {
  const tokens = await iflowService.exchangeCode(code, redirectUri);
} catch (err) {
  if (/Token exchange failed/.test(err.message)) {
    if (/invalid_grant/i.test(err.message)) {
      console.error("Authorization code expired or already used — restart the login flow.");
    } else if (/invalid_client/i.test(err.message)) {
      console.error("Bad clientId/clientSecret — check IFLOW_CONFIG.");
    }
  } else { throw err; }
}

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30). Data as JSON: /api/errors/6b6041b29eb1b36f. Report an issue: GitHub.