{"record":{"id":"d9e1f8e03412f524","repo":"decolua/9router","slug":"failed-to-fetch-user-info-errortext","errorCode":null,"errorMessage":"`Failed to fetch user info: ${errorText}`","messagePattern":"`Failed to fetch user info: (.+?)`","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/lib/oauth/providers/iflow.js","lineNumber":58,"sourceCode":"      throw new Error(`Token exchange failed: ${error}`);\n    }\n\n    return await response.json();\n  },\n  postExchange: async (tokens) => {\n    // Fetch user info (MUST succeed to get API key)\n    const userInfoRes = await fetch(\n      `${IFLOW_CONFIG.userInfoUrl}?accessToken=${encodeURIComponent(tokens.access_token)}`,\n      {\n        headers: {\n          Accept: \"application/json\",\n        },\n      }\n    );\n\n    if (!userInfoRes.ok) {\n      const errorText = await userInfoRes.text();\n      throw new Error(`Failed to fetch user info: ${errorText}`);\n    }\n\n    const result = await userInfoRes.json();\n    if (!result.success) {\n      throw new Error(`User info request failed: ${result.message || 'Unknown error'}`);\n    }\n\n    const userInfo = result.data || {};\n\n    // Validate API key (critical for iFlow)\n    if (!userInfo.apiKey || userInfo.apiKey.trim() === \"\") {\n      throw new Error(\"Empty API key returned from iFlow\");\n    }\n\n    // Validate email/phone\n    const email = userInfo.email?.trim() || userInfo.phone?.trim();\n    if (!email) {\n      throw new Error(\"Missing account email/phone in user info\");","sourceCodeStart":40,"sourceCodeEnd":76,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/src/lib/oauth/providers/iflow.js#L40-L76","documentation":"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.","triggerScenarios":"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.","commonSituations":"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).","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."],"exampleFix":"// before\nconst userInfoRes = await fetch(`${IFLOW_CONFIG.userInfoUrl}?accessToken=${encodeURIComponent(tokens.access_token)}`, {...});\n// after: fail fast on a missing token instead of sending 'undefined'\nif (!tokens?.access_token) {\n  throw new Error(`iFlow token response missing access_token: ${JSON.stringify(tokens)}`);\n}\nconst userInfoRes = await fetch(`${IFLOW_CONFIG.userInfoUrl}?accessToken=${encodeURIComponent(tokens.access_token)}`, {...});","handlingStrategy":"try-catch","validationCode":"// before calling postExchange, verify the token payload shape\nfunction validateTokensForUserInfo(tokens) {\n  if (!tokens || typeof tokens.access_token !== 'string' || tokens.access_token.length === 0) {\n    throw new Error('access_token missing from iFlow token exchange response — cannot fetch user info');\n  }\n}","typeGuard":"function hasAccessToken(t) {\n  return typeof t === 'object' && t !== null && typeof t.access_token === 'string' && t.access_token.length > 0;\n}","tryCatchPattern":"try {\n  const { userInfo } = await provider.postExchange(tokens);\n} catch (e) {\n  if (String(e.message).startsWith('Failed to fetch user info:')) {\n    const body = e.message.slice('Failed to fetch user info:'.length);\n    if (/401|Unauthorized/i.test(body)) {\n      throw new Error('iFlow rejected the access token; restart OAuth flow for a fresh token');\n    }\n    if (/^[45]\\d\\d/.test(body)) {\n      // transient provider/network error — safe to retry once\n      return retryPostExchange(tokens, 1);\n    }\n  }\n  throw e;\n}","preventionTips":["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."],"tags":["oauth","http-4xx","user-info","network"],"backgroundTag":"oauth-userinfo-request-failed","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}