santifer/career-ops · error · Error
remotli: unexpected API response — expected { jobs: [...] },
Error message
remotli: unexpected API response — expected { jobs: [...] }, got ${data === null ? 'null' : typeof data} What it means
During pagination, remotli fetches each page via ctx.fetchJson and asserts the response is an object with a jobs array. It throws on the first page (page 1) if the shape is wrong; on later pages, if succeededOnce is true, a shape failure breaks the loop (keeping already-collected rows) rather than throwing. So this error only surfaces when the very first request returns a non-conformant body.
Source
Thrown at providers/remotli.mjs:296
// or a page-1 body that isn't `{ jobs: [...] }` — means we cannot tell a
// live board from a broken one, so it must throw and surface as a dead
// target. Once one page has parsed, the board is provably reachable and a
// later transient failure must not discard what we already collected.
let succeededOnce = false;
for (let page = 1; page <= Math.min(cap, totalPages); page++) {
const url = `${ORIGIN}${API_PATH}?page=${page}&limit=${PAGE_SIZE}&${ALL_WORK_MODES}`;
assertRemotliUrl(url);
/** @type {any[]} */
let rows;
try {
// redirect:'error' prevents SSRF via server-side redirects; combined with
// assertRemotliUrl this pins every hop to remotli.ch.
const data = await ctx.fetchJson(url, { redirect: 'error' });
if (!data || typeof data !== 'object' || !Array.isArray(/** @type {any} */ (data).jobs)) {
throw new Error(
`remotli: unexpected API response — expected { jobs: [...] }, got ${data === null ? 'null' : typeof data}`,
);
}
rows = /** @type {any} */ (data).jobs;
const reported = Number(/** @type {any} */ (data).pagination?.totalPages);
if (Number.isInteger(reported) && reported > 0) totalPages = reported;
} catch (err) {
if (!succeededOnce) throw err;
break; // keep the pages already collected — a mid-scan blip isn't a dead board
}
// Set only after the shape check passed, so a malformed body never counts
// as proof of life.
succeededOnce = true;
for (const row of rows) {View on GitHub (pinned to 9b17a8ac97)
Solutions
- Retry the scan — transient first-page failures clear on the next run.
- curl the constructed first-page URL to inspect the actual response body and shape.
- Verify API_PATH, PAGE_SIZE, and ALL_WORK_MODES query parameters still match the live endpoint.
- If the API permanently changed, update the shape check to match the new envelope.
Example fix
// before — throws on first page if shape mismatches
if (!data || typeof data !== 'object' || !Array.isArray(data.jobs)) throw new Error(...);
// after — log and retry once before giving up
if (!data || typeof data !== 'object' || !Array.isArray(data.jobs)) {
console.error('remotli: bad first-page shape', JSON.stringify(data).slice(0, 200));
throw new Error('remotli: unexpected API response');
} Defensive patterns
Strategy: retry
Validate before calling
// Pre-flight the first page shape before committing to the full scan
async function probeRemotliFirstPage(ctx) {
const url = `${ORIGIN}${API_PATH}?page=1&limit=${PAGE_SIZE}&${ALL_WORK_MODES}`;
const data = await ctx.fetchJson(url, { redirect: 'error' });
return !!data && typeof data === 'object' && Array.isArray(data.jobs);
}
if (!(await probeRemotliFirstPage(ctx))) {
console.warn('remotli: first-page shape invalid — API down or changed');
} Type guard
/** @param {unknown} d */
function isRemotliPage(d) {
return !!d && typeof d === 'object' && Array.isArray(/** @type{any}*/(d).jobs);
} Try / catch
try {
await provider.fetch(entry, ctx);
} catch (e) {
if (/unexpected API response/.test(e.message)) {
// only the first page throws; retry once after a delay
await new Promise(r => setTimeout(r, 5000));
await provider.fetch(entry, ctx);
} else throw e;
} Prevention
- Retry once on first-page shape errors — they are often transient outages.
- Note that mid-scan shape failures do NOT throw (succeededOnce softens them); only page 1 is strict.
- curl the constructed first-page URL to diagnose persistent shape changes.
When it happens
Trigger: The first-page API response is null, a primitive, or an object lacking a jobs array (e.g. an error envelope { error, message }, a maintenance page, or an HTML-converted-to-JSON blob). Only fires when succeededOnce is still false.
Common situations: remotli.ch API is down or rate-limiting on the initial request; a proxy returned a JSON error block; the API_PATH constant changed and the first page 404'd into an error object; the response encoding changed.
Related errors
- remoteok: unexpected API response — expected a JSON array, g
- remotive: unexpected API response — expected { jobs: [...] }
- echojobs: unexpected API response on page ${page} — expected
- Apify did not return a run id: ${JSON.stringify(body).slice(
- Gmail token refresh returned no access_token
AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13).
Data as JSON: /api/errors/2d0ccdc53e6d6c45.
Report an issue: GitHub.