santifer/career-ops · error · Error

flowxtra: unexpected API response on page ${page} — expected

Error message

flowxtra: unexpected API response on page ${page} — expected { data: { data: [...] } }, got keys: [${json ? Object.keys(json).join(', ') : 'null'}]

What it means

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.

Source

Thrown at providers/flowxtra.mjs:130

}

/** @type {Provider} */
export default {
  id: 'flowxtra',

  async fetch(entry, ctx) {
    const maxPages = resolveMaxPages(entry);
    const fallbackCompany = entry?.name;
    const out = [];

    for (let page = 1; page <= maxPages; page++) {
      const url = `${JOBS_ENDPOINT}?status=Live&per_page=${PER_PAGE}&page=${page}`;
      assertFlowxtraEndpointUrl(url);
      // redirect:'error' prevents SSRF via server-side redirects
      const json = /** @type {any} */ (await ctx.fetchJson(url, { redirect: 'error' }));
      const rows = json?.data?.data;
      if (!Array.isArray(rows)) {
        throw new Error(
          `flowxtra: unexpected API response on page ${page} — expected { data: { data: [...] } }, got keys: [${json ? Object.keys(json).join(', ') : 'null'}]`,
        );
      }
      for (const j of rows) {
        const normalized = normalizeFlowxtraJob(j, fallbackCompany);
        if (normalized) out.push(normalized);
      }
      if (!json.data.next_page_url || rows.length < PER_PAGE) break; // last page reached
    }
    return out;
  },
};

View on GitHub (pinned to 9b17a8ac97)

Solutions

  1. 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.
  2. If the server is returning an error/maintenance envelope, wait and retry — this is usually transient and not a code defect.
  3. 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.
  4. 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.

Example fix

// before
const rows = json?.data?.data;
if (!Array.isArray(rows)) { throw new Error(`flowxtra: unexpected API response on page ${page} ...`); }

// after (adapt to a renamed key, e.g. data.items)
const rows = json?.data?.data ?? json?.data?.items;
if (!Array.isArray(rows)) { throw new Error(`flowxtra: unexpected API response on page ${page} ...`); }
Defensive patterns

Strategy: try-catch

Type guard

// Narrow a Flowxtra response to the expected { data: { data: [] } } shape.
function isFlowxtraPage(json) {
  return !!json
    && typeof json === 'object'
    && json.data && typeof json.data === 'object'
    && Array.isArray(json.data.data);
}

Try / catch

// Treat a shape failure on a non-first page as 'end of feed' rather than fatal.
try {
  const json = await ctx.fetchJson(url, { redirect: 'error' });
  if (!isFlowxtraPage(json)) {
    if (page === 1) throw new Error(`flowxtra: unexpected API response on page ${page}`);
    break; // later-page anomaly: stop paginating, keep what we have
  }
} catch (err) {
  if (page === 1) throw err;
  console.error(`flowxtra: page ${page} failed — ${err.message}`);
  break;
}

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13). Data as JSON: /api/errors/6f691a0c8f5b13b2. Report an issue: GitHub.