{"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":"Error","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/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/flowxtra.mjs#L112-L143","documentation":"flowxtra.mjs throws this in fetch() after the JSON body comes back, when json.data.data is not an Array. The Flowxtra API contract is { success, data: { data: [...jobs], next_page_url, ... }, message }, so a missing/reshaped data.data means the response is not a live job listing. The message echoes Object.keys(json) (or 'null' when the body was null) so you can see exactly what shape arrived.","triggerScenarios":"The Flowxtra endpoint returns an error envelope ({ success:false, message:'...' }) where data is absent or data.data is an object/string; the endpoint is behind a maintenance page returning HTML that ctx.fetchJson parsed into a non-standard object; the API was versioned and renamed the nested data key; a Cloudflare/WAF challenge returned an interstitial JSON; json itself was null (empty 200 body).","commonSituations":"Flowxtra ships an API change that wraps jobs under a new key; the board is temporarily down and returns { message:'maintenance' } with no data; a transient proxy/gateway returns a JSON error object; the per_page/page query params were rejected and the server replied with an error envelope.","solutions":["Re-run the fetch in isolation (curl https://app.flowxtra.com/api/central/jobs?status=Live&per_page=100&page=1) and inspect the top-level keys reported in the message to see the new/intermediate shape.","If the server is returning an error/maintenance envelope, wait and retry — this is usually transient and not a code defect.","If the shape changed permanently, update the rows extraction (json.data.data) and the loop's next_page_url/length logic in flowxtra.mjs to match the new contract.","Verify ctx.fetchJson is not silently returning a parsed HTML/interstitial object; check that the request still uses status=Live and the documented per_page."],"exampleFix":"// before\nconst rows = json?.data?.data;\nif (!Array.isArray(rows)) { throw new Error(`flowxtra: unexpected API response on page ${page} ...`); }\n\n// after (adapt to a renamed key, e.g. data.items)\nconst rows = json?.data?.data ?? json?.data?.items;\nif (!Array.isArray(rows)) { throw new Error(`flowxtra: unexpected API response on page ${page} ...`); }","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"// Narrow a Flowxtra response to the expected { data: { data: [] } } shape.\nfunction isFlowxtraPage(json) {\n  return !!json\n    && typeof json === 'object'\n    && json.data && typeof json.data === 'object'\n    && Array.isArray(json.data.data);\n}","tryCatchPattern":"// Treat a shape failure on a non-first page as 'end of feed' rather than fatal.\ntry {\n  const json = await ctx.fetchJson(url, { redirect: 'error' });\n  if (!isFlowxtraPage(json)) {\n    if (page === 1) throw new Error(`flowxtra: unexpected API response on page ${page}`);\n    break; // later-page anomaly: stop paginating, keep what we have\n  }\n} catch (err) {\n  if (page === 1) throw err;\n  console.error(`flowxtra: page ${page} failed — ${err.message}`);\n  break;\n}","preventionTips":["Pin scan runs to a known-good Flowxtra API version and watch for deprecation announcements.","Log Object.keys(json) on shape failures (the message already does) so regressions are diagnosed from one run.","Consider a retry-with-backoff for transient maintenance envelopes before surfacing the error."],"tags":["api-response-shape","runtime","flowxtra","third-party-api"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}