{"record":{"id":"989e0f0fdd246162","repo":"decolua/9router","slug":"empty-api-key-returned-from-iflow","errorCode":null,"errorMessage":"\"Empty API key returned from iFlow\"","messagePattern":"\"Empty API key returned from iFlow\"","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/lib/oauth/providers/iflow.js","lineNumber":70,"sourceCode":"        },\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) => ({\n    accessToken: tokens.access_token,\n    refreshToken: tokens.refresh_token,\n    expiresIn: tokens.expires_in,\n    apiKey: extra?.userInfo?.apiKey,\n    email: extra?.userInfo?.email || extra?.userInfo?.phone,\n    displayName: extra?.userInfo?.nickname || extra?.userInfo?.name,\n  }),","sourceCodeStart":52,"sourceCodeEnd":88,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/src/lib/oauth/providers/iflow.js#L52-L88","documentation":"The iFlow user-info call succeeded but result.data contains no usable apiKey (missing, null, or whitespace-only). iFlow accounts authenticate downstream API calls with this key, so the OAuth flow aborts rather than saving a credential that cannot work.","triggerScenarios":"postExchange validates userInfo = result.data || {} and throws when !userInfo.apiKey || userInfo.apiKey.trim() === '' — i.e. data is absent entirely (schema drift), or the account has no API key provisioned yet, or iFlow returned apiKey: '' for a newly registered account.","commonSituations":"Newly created iFlow accounts where the API key has not been generated yet; response schema change (apiKey renamed/moved out of data) so the check reads undefined; regional restrictions where iFlow declines to issue keys; result.data accidentally null so userInfo defaults to {}.","solutions":["Log in to the iFlow console and confirm/generate an API key for the account, then retry the OAuth flow.","If data was empty entirely, dump the raw user-info JSON — a schema change (renamed field) is likely; update the lookup accordingly.","Verify the account is eligible for API access in its region/plan; upgrade or switch accounts if iFlow withholds keys.","Retry the flow — occasionally keys are provisioned asynchronously right after account activation."],"exampleFix":"// before\nconst userInfo = result.data || {};\nif (!userInfo.apiKey || userInfo.apiKey.trim() === \"\") {\n  throw new Error(\"Empty API key returned from iFlow\");\n}\n// after: distinguish missing-data vs missing-key\nconst userInfo = result.data;\nif (!userInfo) {\n  throw new Error(`iFlow user info missing data field: ${JSON.stringify(result).slice(0, 300)}`);\n}\nif (!userInfo.apiKey || userInfo.apiKey.trim() === \"\") {\n  throw new Error(\"iFlow account has no API key provisioned — generate one in the iFlow console\");\n}","handlingStrategy":"validation","validationCode":"// validate the user-info payload before/after postExchange\nfunction hasUsableApiKey(data) {\n  return typeof data === 'object' && data !== null\n    && typeof data.apiKey === 'string'\n    && data.apiKey.trim().length > 0;\n}\n// usage: if (!hasUsableApiKey(userInfo)) throw new Error('iFlow API key not provisioned');","typeGuard":"function isUserInfoWithApiKey(u) {\n  return typeof u === 'object' && u !== null && 'apiKey' in u\n    && typeof u.apiKey === 'string' && u.apiKey.trim() !== '';\n}","tryCatchPattern":"try {\n  const { userInfo } = await provider.postExchange(tokens);\n} catch (e) {\n  if (e.message === 'Empty API key returned from iFlow') {\n    // actionable, user-facing: not retryable, direct to iFlow console\n    return { ok: false, reason: 'no-api-key', hint: 'Generate an API key in the iFlow console, then retry login' };\n  }\n  throw e;\n}","preventionTips":["Confirm API-key provisioning as part of account onboarding before wiring up OAuth login.","If postExchange ever starts throwing this for all accounts, suspect schema drift in result.data — log the raw payload.","Do not store the mapped account when apiKey is empty; a credential without a key will fail on first API call anyway.","Add a fixture-based unit test asserting a valid userInfo shape includes apiKey."],"tags":["oauth","validation","api-key","user-info"],"backgroundTag":"missing-api-key","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}