decolua/9router · error · Error
`Failed to fetch user info
Error message
`Failed to fetch user info: ${errorText}` What it means
The iFlow user-info request (issued inside postExchange after a successful token exchange) returned a non-2xx HTTP status. The raw response body is surfaced as the error text. This endpoint is mandatory for iFlow because it returns the apiKey needed for subsequent API calls.
Solutions
- Confirm tokens.access_token is a non-empty string before calling postExchange; if undefined, the token endpoint's response schema changed — inspect the raw exchange response.
- Verify IFLOW_CONFIG.userInfoUrl matches the environment of the token endpoint (staging vs production hosts are usually not interchangeable).
- Re-run the OAuth flow to obtain a fresh access token — a 401 here shortly after exchange means the token was rejected server-side.
- Check network/proxy configuration if the status is 5xx, and retry the whole flow.
Example fix
// before
const userInfoRes = await fetch(`${IFLOW_CONFIG.userInfoUrl}?accessToken=${encodeURIComponent(tokens.access_token)}`, {...});
// after: fail fast on a missing token instead of sending 'undefined'
if (!tokens?.access_token) {
throw new Error(`iFlow token response missing access_token: ${JSON.stringify(tokens)}`);
}
const userInfoRes = await fetch(`${IFLOW_CONFIG.userInfoUrl}?accessToken=${encodeURIComponent(tokens.access_token)}`, {...}); Defensive patterns
Strategy: try-catch
Validate before calling
// before calling postExchange, verify the token payload shape
function validateTokensForUserInfo(tokens) {
if (!tokens || typeof tokens.access_token !== 'string' || tokens.access_token.length === 0) {
throw new Error('access_token missing from iFlow token exchange response — cannot fetch user info');
}
} Type guard
function hasAccessToken(t) {
return typeof t === 'object' && t !== null && typeof t.access_token === 'string' && t.access_token.length > 0;
} Try / catch
try {
const { userInfo } = await provider.postExchange(tokens);
} catch (e) {
if (String(e.message).startsWith('Failed to fetch user info:')) {
const body = e.message.slice('Failed to fetch user info:'.length);
if (/401|Unauthorized/i.test(body)) {
throw new Error('iFlow rejected the access token; restart OAuth flow for a fresh token');
}
if (/^[45]\d\d/.test(body)) {
// transient provider/network error — safe to retry once
return retryPostExchange(tokens, 1);
}
}
throw e;
} Prevention
- Always check hasAccessToken(tokens) before the user-info call so you never send the string 'undefined'.
- Pin userInfoUrl and tokenUrl to the same environment and verify them whenever iFlow API versions change.
- Use the access token promptly — do not persist and reuse it long past its expires_in.
- Monitor provider status pages; 5xx bursts here are usually iFlow-side, not your bug.
When it happens
Trigger: GET `${userInfoUrl}?accessToken=<access_token>` returns userInfoRes.ok === false — most commonly HTTP 401 because the freshly minted access_token is invalid/expired/for the wrong environment, or the accessToken query param is empty because tokens.access_token was missing from the token response.
Common situations: Token endpoint returned a 200 with an unexpected body shape (no access_token) so the query param is 'undefined'; access token already revoked; iFlow user-info base URL changed or points at a different environment than the token endpoint; corporate proxy blocking the request (5xx).
Related errors
- `Device authorization failed
- `Device code request failed
- Failed to get user info
- `Token exchange failed
- authData.error
AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30).
Data as JSON: /api/errors/d9e1f8e03412f524.
Report an issue: GitHub.
Appendix: source
Thrown at src/lib/oauth/providers/iflow.js:58
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}`);
}
const result = await userInfoRes.json();
if (!result.success) {
throw new Error(`User info request failed: ${result.message || 'Unknown error'}`);
}
const userInfo = result.data || {};
// Validate API key (critical for iFlow)
if (!userInfo.apiKey || userInfo.apiKey.trim() === "") {
throw new Error("Empty API key returned from iFlow");
}
// Validate email/phone
const email = userInfo.email?.trim() || userInfo.phone?.trim();
if (!email) {
throw new Error("Missing account email/phone in user info");View on GitHub (pinned to 90b52e06ff)