santifer/career-ops · error · Error
remotive: unexpected API response — expected { jobs: [...] }
Error message
remotive: unexpected API response — expected { jobs: [...] }, got keys: [${json ? Object.keys(json).join(', ') : 'null'}] What it means
The remotive provider fetches FEED_URL and asserts the response is an object with a jobs array ({ jobs: [...] }). It throws when json is falsy or json.jobs is not an array, listing the actual top-level keys in the message for diagnostics. This catches upstream contract changes, error envelopes, and outages.
Source
Thrown at providers/remotive.mjs:28
// Wire in via a `job_boards:` entry with `provider: remotive`.
const FEED_URL = 'https://remotive.com/api/remote-jobs';
/** @type {Provider} */
export default {
id: 'remotive',
/**
* Fetches and normalizes postings from the Remotive public feed.
* @param {{ name?: string }} entry - The job_boards entry being processed.
* @param {{ fetchJson: (url: string, opts?: { redirect?: 'error'|'follow'|'manual' }) => Promise<any> }} ctx - HTTP context.
* @returns {Promise<Array<{title: string, url: string, company: string, location: string}>>}
*/
async fetch(entry, ctx) {
// redirect:'error' prevents SSRF via server-side redirects
const json = await ctx.fetchJson(FEED_URL, { redirect: 'error' });
if (!json || !Array.isArray(json.jobs)) {
throw new Error(`remotive: unexpected API response — expected { jobs: [...] }, got keys: [${json ? Object.keys(json).join(', ') : 'null'}]`);
}
return json.jobs
.filter(j => j && typeof j === 'object'
&& typeof j.title === 'string' && j.title.trim() !== ''
&& typeof j.url === 'string' && /^https?:\/\//i.test(j.url.trim()))
.map(j => ({
title: j.title.trim(),
url: j.url.trim(),
company: typeof j.company_name === 'string' && j.company_name.trim() ? j.company_name.trim() : (entry.name || 'Remotive'),
location: typeof j.candidate_required_location === 'string' ? j.candidate_required_location.trim() : '',
}));
},
};
View on GitHub (pinned to 9b17a8ac97)
Solutions
- Retry after a delay — outages and rate limits are often transient.
- curl the FEED_URL to see the current top-level keys; the error message already lists them.
- If remotive renamed the field, update the parser to read the new key (e.g. results) into json.jobs.
- Confirm the FEED_URL constant matches the current documented endpoint.
Example fix
// before
if (!json || !Array.isArray(json.jobs)) throw new Error(...);
// after — tolerate renamed field
const jobs = json?.jobs ?? json?.results;
if (!Array.isArray(jobs)) throw new Error('remotive: unexpected API response'); Defensive patterns
Strategy: type-guard
Validate before calling
async function probeRemotive(ctx) {
const json = await ctx.fetchJson(FEED_URL, { redirect: 'error' });
return !!json && Array.isArray(json.jobs);
}
if (!(await probeRemotive(ctx))) {
console.warn('remotive: feed missing jobs array — skipping');
} Type guard
/** @param {unknown} d */
function isRemotiveFeed(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)) {
await new Promise(r => setTimeout(r, 5000));
await provider.fetch(entry, ctx);
} else throw e;
} Prevention
- Retry once on shape errors — outages and rate limits are often transient.
- The error message lists the actual keys, which pinpoints the upstream change.
- Keep the FEED_URL constant synced with the documented endpoint.
When it happens
Trigger: The feed returns null; an error object like { error, message } without a jobs key; a maintenance page parsed as { status: 'down' }; a version change that renamed jobs to results or offers.
Common situations: Remotive API is under maintenance; a rate-limit response replaced the normal payload; the FEED_URL was updated to a new API version with a different envelope; a proxy returned a JSON error block.
Related errors
- remoteok: unexpected API response — expected a JSON array, g
- remotli: 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/06d9547434b3e161.
Report an issue: GitHub.