santifer/career-ops · error

flowxtra: unexpected API response on page

Error message

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

What it means

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.

Solutions

  1. Read the 'got keys' in the message to identify what was actually returned (e.g. message/errors → API error; entirely different keys → envelope change).
  2. Verify API credentials/status with the Flowxtra admin — an auth or quota error often changes the response shape.
  3. Check whether the Flowxtra API version changed and update the parsing path in normalizeFlowxtraJob's caller accordingly.
  4. Confirm the request URL (status=Live&per_page&page) is still a supported query; adjust constants if the API contract moved.

Example fix

// before
const rows = json?.data?.data;
// after (tolerate a flattened envelope)
const rows = Array.isArray(json?.data?.data) ? json.data.data
  : Array.isArray(json?.data) ? json.data : null;
if (!rows) throw new Error(`flowxtra: unexpected API response on page ${page}: ${JSON.stringify(json).slice(0, 200)}`);
Defensive patterns

Strategy: type-guard

Validate before calling

function looksLikeFlowxtraEnvelope(json) {
  return !!json && typeof json === 'object' && Array.isArray(json?.data?.data);
}

Type guard

const isJobEnvelope = (json) =>
  typeof json === 'object' && json !== null && Array.isArray(json?.data?.data);

Try / catch

try {
  const json = await ctx.fetchJson(url, { redirect: 'error' });
  if (!isJobEnvelope(json)) {
    console.error('Flowxtra envelope changed; keys:', Object.keys(json ?? {}).join(','));
    // inspect json.message / json.errors before retrying
  }
} catch (e) {
  if (e.message.includes('unexpected API response')) { /* fall back or alert */ }
  else throw e;
}

Prevention

When it happens

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

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

Understand the failure class

Background: "invalid response format", "malformed payload", "missing data field": when an API returns 200 but the response shape is wrong — this error's family across 23 libraries.

Related errors


AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16). Data as JSON: /api/errors/6f691a0c8f5b13b2. Report an issue: GitHub.

Appendix: 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 aac998c7ed)