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
- 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).
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
- 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.
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
- loadCodeAssist failed: HTTP ${response.status} ${errorText.s
- onboardUser HTTP ${response.status}: ${errorText.slice(0, 20
- `ClinePass token exchange failed: ${error}`
- `Token exchange failed: ${error}`
- `Trae ExchangeToken failed: ${lastErr}`
AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30).
Data as JSON: /api/errors/6b6041b29eb1b36f.
Report an issue: GitHub.