decolua/9router · error · Error

`Token exchange failed: ${error}`

Error message

`Token exchange failed: ${error}`

What it means

iFlow OAuth authorization-code token exchange failed. The provider's token endpoint returned a non-2xx HTTP status, so the body (usually a JSON OAuth error like {error, error_description} or an HTML error page) is read as text and rethrown. This happens after the user completes the authorize redirect, when the one-time code is redeemed for access/refresh tokens.

Source

Thrown at src/lib/oauth/providers/iflow.js:40

    const response = await fetch(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: config.clientId,
        client_secret: config.clientSecret,
      }),
    });

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

    return await response.json();
  },
  postExchange: async (tokens) => {
    // Fetch user info (MUST succeed to get API key)
    const userInfoRes = await fetch(
      `${IFLOW_CONFIG.userInfoUrl}?accessToken=${encodeURIComponent(tokens.access_token)}`,
      {
        headers: {
          Accept: "application/json",
        },
      }
    );

    if (!userInfoRes.ok) {
      const errorText = await userInfoRes.text();
      throw new Error(`Failed to fetch user info: ${errorText}`);

View on GitHub (pinned to 90b52e06ff)

Solutions

  1. Have the user restart the OAuth flow to get a fresh authorization code (codes are single-use and short-lived).
  2. Verify IFLOW_CONFIG clientId/clientSecret match the iFlow app credentials and that redirectUri in exchangeToken is byte-identical to the one used in buildAuthUrl.
  3. Log the raw `error` text (it contains the OAuth error_description) and match it against the OAuth spec: invalid_grant => expired/replayed code; invalid_client => bad credentials.
  4. Check iFlow service status / network reachability if the status is 5xx, then retry the flow.

Example fix

// before: opaque text-only error
const error = await response.text();
throw new Error(`Token exchange failed: ${error}`);
// after: structured OAuth error
const body = await response.text();
let code = response.status, desc = body;
try { const j = JSON.parse(body); code = j.error; desc = j.error_description ?? body; } catch {}
throw new Error(`Token exchange failed (${response.status} ${code}): ${desc}`);
Defensive patterns

Strategy: try-catch

Validate before calling

// before starting the flow: ensure credentials and redirect are configured
function assertIflowConfig(config, redirectUri) {
  if (!config.clientId || !config.clientSecret) throw new Error('iFlow clientId/clientSecret not configured');
  if (!config.tokenUrl) throw new Error('iFlow tokenUrl missing');
  if (!redirectUri || !redirectUri.startsWith('http')) throw new Error('redirectUri invalid');
}

Type guard

function isOAuthTokenResponse(t) {
  return t !== null && typeof t === 'object' && typeof t.access_token === 'string' && t.access_token.length > 0;
}

Try / catch

try {
  const tokens = await provider.exchangeToken(config, code, redirectUri);
} catch (e) {
  if (String(e.message).includes('invalid_grant')) {
    // stale/replayed code: restart the authorize flow
    return restartAuthFlow();
  }
  if (String(e.message).includes('invalid_client')) {
    throw new Error('Check iFlow clientId/clientSecret configuration');
  }
  throw e; // 5xx etc: surface after logging raw body
}

Prevention

When it happens

Trigger: POST to config.tokenUrl with grant_type=authorization_code returns response.ok === false — e.g. expired/already-redeemed authorization code, redirect_uri mismatch vs buildAuthUrl, wrong client_id/client_secret in the Basic Auth header, or provider-side 4xx/5xx.

Common situations: User waited too long before completing login (code expired); user retried the callback URL with the same code; misconfigured IFLOW_CONFIG clientSecret (placeholder from .env not set); redirectUri differs from the one used in buildAuthUrl; iFlow outage returning 5xx.

Related errors


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