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
- Have the user restart the OAuth flow to get a fresh authorization code (codes are single-use and short-lived).
- Verify IFLOW_CONFIG clientId/clientSecret match the iFlow app credentials and that redirectUri in exchangeToken is byte-identical to the one used in buildAuthUrl.
- 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.
- 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
- Never reuse or cache authorization codes; always redirect the user through a fresh authorize URL.
- Keep the redirectUri constant in a single shared config object used by both buildAuthUrl and exchangeToken.
- Load clientId/clientSecret from env at startup and fail fast if missing instead of mid-flow.
- Log the raw token-endpoint body on failure — the OAuth error_description pinpoints the cause.
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
- `Token exchange failed: ${error}`
- `Token exchange failed: ${error}`
- `Cline token exchange failed: ${error}`
- `ClinePass token exchange failed: ${error}`
- `Failed to fetch user info: ${errorText}`
AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30).
Data as JSON: /api/errors/f3d18e7598fc615b.
Report an issue: GitHub.