santifer/career-ops · error · Error
jobicy: unexpected API response — expected { jobs: [...] },
Error message
jobicy: unexpected API response — expected { jobs: [...] }, got keys: [${json ? Object.keys(json).join(', ') : 'null'}] What it means
Thrown by jobicy fetch() when the parsed JSON response is falsy or its jobs field is not an array. The Jobicy public feed is expected to return { jobs: [...] }; any other shape is a contract break and is surfaced as a hard error (with the actual top-level keys in the message) so an endpoint change does not masquerade as an empty result.
Source
Thrown at providers/jobicy.mjs:29
/** @type {Provider} */
export default {
id: 'jobicy',
detect(entry) {
return entry?.provider === 'jobicy' ? { url: FEED_URL } : null;
},
/**
* Fetches and normalizes postings from the Jobicy 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, postedAt?: number}>>}
*/
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(`jobicy: unexpected API response — expected { jobs: [...] }, got keys: [${json ? Object.keys(json).join(', ') : 'null'}]`);
}
return parseJobicyResponse(json, entry.name || 'Jobicy');
},
};
/**
* Parse a Jobicy API response. Exported for unit tests.
*
* @param {any} json - Raw response payload.
* @param {string} defaultCompany - Fallback company name.
* @returns {Array<{title: string, url: string, company: string, location: string}>}
*/
export function parseJobicyResponse(json, defaultCompany = 'Jobicy') {
if (!json || !Array.isArray(json.jobs)) return [];
const toEpochMs = (value) => {
if (!value) return undefined;View on GitHub (pinned to 9b17a8ac97)
Solutions
- Retry once to clear a transient error envelope.
- Open the Jobicy feed URL directly and inspect the top-level keys shown in the message; if jobs was renamed, update parseJobicyResponse and the guard.
- If the endpoint persistently returns a different shape, disable the jobicy entry until the parser is updated.
- Pin the known-good shape in a regression test.
Defensive patterns
Strategy: try-catch
Validate before calling
// Pre-flight the feed and shape-check before relying on it.
const json = await ctx.fetchJson(FEED_URL, { redirect: 'error' });
if (!json || !Array.isArray(json.jobs)) {
console.warn(`jobicy: unexpected shape — keys: [${json ? Object.keys(json).join(', ') : 'null'}]`);
} Type guard
/** Jobicy feed payload with the expected { jobs: [] } envelope. */
function isJobicyFeed(json) {
return !!json && typeof json === 'object' && Array.isArray(json.jobs);
} Try / catch
try {
return await jobicyProvider.fetch(entry, ctx);
} catch (err) {
if (/unexpected API response/.test(err.message)) {
console.warn(`jobicy: shape drift — ${err.message}`);
return []; // or retry/backoff per policy
}
throw err;
} Prevention
- Pin the known-good Jobicy envelope in a regression test.
- Use the echoed keys in the message to distinguish a rename from an error envelope.
- Keep fetchJson strict about non-2xx so error bodies are not mistaken for odd shapes.
When it happens
Trigger: Jobicy renamed the jobs key; the endpoint returned an error envelope; rate limiting returned a JSON error object; the endpoint moved and serves a different JSON document; a transient 200 with a null/empty body.
Common situations: Upstream API contract drift after a Jobicy release; CDN/proxy returning a cached error with 200; partial JSON from a network glitch; an A/B test serving a new shape.
Related errors
- himalayas: unexpected API response - expected { jobs: [...]
- ibm: unexpected API response — expected hits.hits[], got key
- hackernews: unexpected item response for thread ${threadId}
- Could not parse existing candidates file at ${candidatesPath
- Existing candidates file at ${candidatesPath} is not a JSON
AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13).
Data as JSON: /api/errors/cd592334128b8b9b.
Report an issue: GitHub.