santifer/career-ops · error · Error
echojobs: unexpected API response on page ${page} — expected
Error message
echojobs: unexpected API response on page ${page} — expected { jobs: [...] }, got keys: [${json ? Object.keys(json).join(', ') : 'null'}] What it means
The echojobs feed response did not match the expected { jobs: [...] } shape: either the JSON was null/non-object, or json.jobs was absent/not an array. This is a real runtime error raised after a successful fetch, signalling the upstream API changed shape, returned an error body, or served a non-JSON page. The message includes the actual top-level keys (or 'null') to aid diagnosis.
Source
Thrown at providers/echojobs.mjs:153
id: 'echojobs',
detect(entry) {
return entry?.provider === 'echojobs' ? { url: FEED_BASE } : null;
},
async fetch(entry, ctx) {
const maxPages = resolveMaxPages(entry);
const fallbackCompany = entry?.name;
const out = [];
for (let page = 1; page <= maxPages; page++) {
// Validate the URL actually fetched (not just a constant) so the host pin
// is meaningful, then redirect:'error' blocks SSRF via server-side
// redirects — together they keep every page request on echojobs.io.
const url = assertEchojobsUrl(`${FEED_BASE}?per_page=${PER_PAGE}&page=${page}`);
const json = /** @type {any} */ (await ctx.fetchJson(url, { redirect: 'error' }));
if (!json || !Array.isArray(json.jobs)) {
throw new Error(
`echojobs: unexpected API response on page ${page} — expected { jobs: [...] }, got keys: [${json ? Object.keys(json).join(', ') : 'null'}]`,
);
}
for (const j of json.jobs) {
const normalized = normalizeEchojobsJob(j, fallbackCompany);
if (normalized) out.push(normalized);
}
if (json.jobs.length < PER_PAGE) break; // short page → last page reached
}
return out;
},
};
View on GitHub (pinned to 9b17a8ac97)
Solutions
- Log the actual response keys/body shown in the message to identify whether it is an error envelope, a rename, or a non-JSON page.
- If the API renamed the field, update the Array.isArray(json.jobs) check in providers/echojobs.mjs to the new key.
- If it is a transient error/rate-limit body, retry after a backoff or skip the run; treat a persistent shape change as a provider bug to fix.
- Confirm the endpoint is still https://echojobs.io/api/jobs and that no proxy is rewriting the response.
Example fix
// before — expects the documented envelope
if (!json || !Array.isArray(json.jobs)) { throw ... }
// after — API renamed 'jobs' to 'results'
if (!json || !Array.isArray(json.results)) { throw ... }
// (and iterate json.results below) Defensive patterns
Strategy: try-catch
Validate before calling
// Cannot validate a remote response before fetching it. Best-effort shape probe after fetch:
async function probeEchojobsShape(ctx, url) {
const json = await ctx.fetchJson(url, { redirect: 'error' });
return json && Array.isArray(json.jobs) ? json : null;
} Type guard
function isEchojobsEnvelope(json) {
return !!json && typeof json === 'object' && Array.isArray(json.jobs);
} Try / catch
try { await echojobs.fetch(entry, ctx); }
catch (e) {
if (/^echojobs: unexpected API response/.test(e.message)) {
// upstream changed shape or returned an error body — inspect the keys in the message,
// retry once after backoff for transient rate-limit bodies, then skip/disable if persistent
} else throw e;
} Prevention
- Pin the provider version so a silent upstream API change surfaces immediately.
- Log the actual top-level keys on failure to distinguish rename vs error envelope vs block page.
- For transient error bodies, retry with backoff; for persistent shape changes, update the parser.
When it happens
Trigger: ctx.fetchJson returned null; returned an error object like { error: '...' } or { message: '...' } (e.g. rate-limit 429 body, auth failure, maintenance); the API renamed the 'jobs' field; an HTML error page was parsed as an unexpected object.
Common situations: EchoJobs deploys a breaking API change (field rename, new envelope); a rate-limit or outage returns an error body with a different shape; an intermediate proxy (corporate firewall, CDN) returns a block page that parses as JSON-ish; a regional API variant with a different envelope.
Related errors
- remoteok: unexpected API response — expected a JSON array, g
- remotive: unexpected API response — expected { jobs: [...] }
- remotli: unexpected API response — expected { jobs: [...] },
- solidjobs: unexpected API response — expected { jobs: [...]
- thehub: unexpected API response on page ${page} — expected {
AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13).
Data as JSON: /api/errors/88b5b0f0c1971b24.
Report an issue: GitHub.