{"record":{"id":"7e2c4ca19a7ac1b6","repo":"decolua/9router","slug":"user-info-request-failed-result-message-un","errorCode":null,"errorMessage":"`User info request failed: ${result.message || 'Unknown error'}`","messagePattern":"`User info request failed: (.+?)`","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/lib/oauth/providers/iflow.js","lineNumber":63,"sourceCode":"  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\");\n    }\n\n    return { userInfo };\n  },\n  mapTokens: (tokens, extra) => ({","sourceCodeStart":45,"sourceCodeEnd":81,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/src/lib/oauth/providers/iflow.js#L45-L81","documentation":"The iFlow user-info endpoint returned HTTP 200 but the JSON body has success: false, indicating an application-level rejection rather than an HTTP error. The provider's `message` field (or 'Unknown error') is included in the thrown error. This is iFlow's envelope convention: HTTP 200 does not imply success.","triggerScenarios":"userInfoRes.ok is true but result.success is falsy — e.g. the access token is expired/revoked but the API still answers 200, account is banned or not provisioned for iFlow coding, or the userInfoUrl path changed and returns a 200 error envelope.","commonSituations":"Expired-but-200-wrapped token responses; account restrictions (region, plan) that surface as success:false with a message; iFlow API version change that renamed success/message fields, making this check misfire; hitting the endpoint with a token from the wrong environment.","solutions":["Read result.message in the error text — it names the provider-side reason (e.g. token invalid, account restricted) and fix accordingly.","Re-run the OAuth flow for a fresh access token if the message indicates token invalidity/expiry.","Check the iFlow account's standing/plan in the iFlow console if the message indicates restrictions.","If the message is always 'Unknown error', log the full result JSON — the response schema likely changed and the success/message fields moved."],"exampleFix":"// before\nif (!result.success) {\n  throw new Error(`User info request failed: ${result.message || 'Unknown error'}`);\n}\n// after: include full payload for diagnosing schema drift\nif (!result.success) {\n  throw new Error(`User info request failed: ${result.message || 'Unknown error'} :: ${JSON.stringify(result).slice(0, 500)}`);\n}","handlingStrategy":"try-catch","validationCode":"// HTTP 200 with success:false cannot be pre-validated, but you can pre-parse defensively\nfunction parseUserInfoEnvelope(jsonText) {\n  try {\n    const r = JSON.parse(jsonText);\n    return r && typeof r === 'object' ? r : null;\n  } catch { return null; }\n}","typeGuard":"function isSuccessEnvelope(r) {\n  return r !== null && typeof r === 'object' && r.success === true && (r.data === undefined || typeof r.data === 'object');\n}","tryCatchPattern":"try {\n  const { userInfo } = await provider.postExchange(tokens);\n} catch (e) {\n  const m = String(e.message).match(/^User info request failed: (.+?)(?: ::|$)/);\n  if (m) {\n    console.error('iFlow user-info rejected:', m[1]);\n    if (/token|expired|invalid/i.test(m[1])) return restartAuthFlow(); // provider-level token rejection\n    throw new Error(`iFlow account issue: ${m[1]}`); // restriction/provisioning problem\n  }\n  throw e;\n}","preventionTips":["Never assume HTTP 200 means success with iFlow — always branch on the success field.","Log the entire response envelope when success is falsy so schema drift or new message variants are visible.","Treat messages mentioning token/expiry as restart-the-flow signals, not retry signals.","Add a regression test that feeds a success:false fixture through postExchange."],"tags":["oauth","api-response","user-info","api-envelope"],"backgroundTag":"api-error-envelope","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}