{"record":{"id":"6f691a0c8f5b13b2","repo":"santifer/career-ops","slug":"flowxtra-unexpected-api-response-on-page-page","errorCode":null,"errorMessage":"flowxtra: unexpected API response on page ${page} — expected { data: { data: [...] } }, got keys: [${json ? Object.keys(json).join(', ') : 'null'}]","messagePattern":"flowxtra: unexpected API response on page (.+?) — expected (.+?) \\}, got keys: \\[(.+?)\\]","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"providers/flowxtra.mjs","lineNumber":130,"sourceCode":"}\n\n/** @type {Provider} */\nexport default {\n  id: 'flowxtra',\n\n  async fetch(entry, ctx) {\n    const maxPages = resolveMaxPages(entry);\n    const fallbackCompany = entry?.name;\n    const out = [];\n\n    for (let page = 1; page <= maxPages; page++) {\n      const url = `${JOBS_ENDPOINT}?status=Live&per_page=${PER_PAGE}&page=${page}`;\n      assertFlowxtraEndpointUrl(url);\n      // redirect:'error' prevents SSRF via server-side redirects\n      const json = /** @type {any} */ (await ctx.fetchJson(url, { redirect: 'error' }));\n      const rows = json?.data?.data;\n      if (!Array.isArray(rows)) {\n        throw new Error(\n          `flowxtra: unexpected API response on page ${page} — expected { data: { data: [...] } }, got keys: [${json ? Object.keys(json).join(', ') : 'null'}]`,\n        );\n      }\n      for (const j of rows) {\n        const normalized = normalizeFlowxtraJob(j, fallbackCompany);\n        if (normalized) out.push(normalized);\n      }\n      if (!json.data.next_page_url || rows.length < PER_PAGE) break; // last page reached\n    }\n    return out;\n  },\n};\n","sourceCodeStart":112,"sourceCodeEnd":143,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/flowxtra.mjs#L112-L143","documentation":"In fetch (providers/flowxtra.mjs), each page of the Flowxtra API is fetched via ctx.fetchJson with redirect:'error' (SSRF guard) and asserted to follow the expected envelope { data: { data: [...] } }. This throw fires when the JSON response's data.data is not an array, and the message includes the actual top-level keys (or null) for diagnosis. It means the API answered but not with the documented paginated-jobs shape.","triggerScenarios":"The endpoint returns an error payload ({message, errors}), an auth/limit envelope, an empty object, an HTML error page that somehow parsed as JSON, or a version change renaming the data field — any case where json?.data?.data is not an Array.","commonSituations":"Flowxtra API version bump changing the pagination envelope; API key expiring so the server returns an error JSON; per_page/page parameters rejected and an error object returned; network middleware (WAF) returning JSON-formatted block responses.","solutions":["Read the 'got keys' in the message to identify what was actually returned (e.g. message/errors → API error; entirely different keys → envelope change).","Verify API credentials/status with the Flowxtra admin — an auth or quota error often changes the response shape.","Check whether the Flowxtra API version changed and update the parsing path in normalizeFlowxtraJob's caller accordingly.","Confirm the request URL (status=Live&per_page&page) is still a supported query; adjust constants if the API contract moved."],"exampleFix":"// before\nconst rows = json?.data?.data;\n// after (tolerate a flattened envelope)\nconst rows = Array.isArray(json?.data?.data) ? json.data.data\n  : Array.isArray(json?.data) ? json.data : null;\nif (!rows) throw new Error(`flowxtra: unexpected API response on page ${page}: ${JSON.stringify(json).slice(0, 200)}`);","handlingStrategy":"type-guard","validationCode":"function looksLikeFlowxtraEnvelope(json) {\n  return !!json && typeof json === 'object' && Array.isArray(json?.data?.data);\n}","typeGuard":"const isJobEnvelope = (json) =>\n  typeof json === 'object' && json !== null && Array.isArray(json?.data?.data);","tryCatchPattern":"try {\n  const json = await ctx.fetchJson(url, { redirect: 'error' });\n  if (!isJobEnvelope(json)) {\n    console.error('Flowxtra envelope changed; keys:', Object.keys(json ?? {}).join(','));\n    // inspect json.message / json.errors before retrying\n  }\n} catch (e) {\n  if (e.message.includes('unexpected API response')) { /* fall back or alert */ }\n  else throw e;\n}","preventionTips":["Pin/monitor the Flowxtra API version you integrate against.","Alert on any response whose top-level keys are not [data] — it usually means auth or quota errors.","Test the provider against a recorded fixture of the real envelope in CI."],"tags":["api","response-shape","pagination","contract-change"],"backgroundTag":"unexpected-api-response-shape","analyzedSha":"aac998c7ed7248ea853b720ceeb1fdbeb322fc5d","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}