santifer/career-ops · error · Error
himalayas: unexpected API response - expected { jobs: [...]
Error message
himalayas: unexpected API response - expected { jobs: [...] }, got keys: [${json ? Object.keys(json).join(', ') : 'null'}] What it means
Thrown by himalayas fetch() when the parsed JSON response is falsy or its jobs field is not an array. The Himalayas public feed is expected to return { jobs: [...] }; any other shape is treated as a contract break and surfaced as a hard error so a silent endpoint change does not look like an empty board. The error message echoes the actual top-level keys to aid diagnosis.
Source
Thrown at providers/himalayas.mjs:89
id: 'himalayas',
detect(entry) {
return entry?.provider === 'himalayas' ? { url: FEED_URL } : null;
},
/**
* Fetches and normalizes postings from the Himalayas public feed.
* @param {{ provider?: 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) {
const feedUrl = assertHimalayasUrl(FEED_URL);
// redirect:'error' prevents SSRF via server-side redirects; combined with
// assertHimalayasUrl above it keeps the request pinned to himalayas.app.
const json = await ctx.fetchJson(feedUrl, { redirect: 'error' });
if (!json || !Array.isArray(json.jobs)) {
throw new Error(`himalayas: unexpected API response - expected { jobs: [...] }, got keys: [${json ? Object.keys(json).join(', ') : 'null'}]`);
}
return parseHimalayasResponse(json);
},
};
/**
* Parse Himalayas' public jobs API response. Exported for unit tests.
*
* Shape: `{ jobs: [...] }`, where each job currently carries `title`,
* `companyName`, `locationRestrictions`, `applicationLink`, `guid`,
* `pubDate`, and `companySlug`. `applicationLink` is preferred over `guid`
* and used as the dedup key after HTTPS + host validation.
*
* @param {unknown} json - raw parsed API response
* @returns {Array<{title: string, url: string, company: string, location: string, postedAt?: number}>}
*/
export function parseHimalayasResponse(json) {
if (!json || typeof json !== 'object' || !Array.isArray(json.jobs)) return [];View on GitHub (pinned to 9b17a8ac97)
Solutions
- Retry the scan once — transient rate-limit/error envelopes usually clear.
- Open https://himalayas.app/jobs/api?limit=50 directly and inspect the top-level keys; if jobs was renamed, update parseHimalayasResponse and the guard.
- If the endpoint persistently returns an error envelope, disable the himalayas board entry until the feed recovers.
- Confirm ctx.fetchJson is not masking a non-2xx status as a parsed-but-wrong body.
Defensive patterns
Strategy: try-catch
Validate before calling
// Optional pre-flight: fetch 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(`himalayas: unexpected shape — keys: [${json ? Object.keys(json).join(', ') : 'null'}]`);
} Type guard
/** Himalayas feed payload with the expected { jobs: [] } envelope. */
function isHimalayasFeed(json) {
return !!json && typeof json === 'object' && Array.isArray(json.jobs);
} Try / catch
try {
return await himalayasProvider.fetch(entry, ctx);
} catch (err) {
if (/unexpected API response/.test(err.message)) {
// Likely contract drift or a transient error envelope — retry once, then surface.
console.warn(`himalayas: shape drift — ${err.message}`);
return []; // or rethrow after N retries, depending on policy
}
throw err;
} Prevention
- Pin the known-good feed shape in a regression test so upstream drift is caught before it reaches a scan.
- Inspect the keys echoed in the message to diagnose renames vs. error envelopes quickly.
- Keep ctx.fetchJson strict about non-2xx so error bodies do not masquerade as odd JSON shapes.
When it happens
Trigger: Himalayas renamed the jobs key (e.g. to listings or data); the endpoint returned an error envelope like { error: '...' } or a rate-limit response; the endpoint moved and now serves HTML that parsed to an unexpected object; a transient 200 with an empty/null body.
Common situations: Upstream API contract drift after a Himalayas release; rate limiting returning a JSON error instead of the feed; a proxy/CDN serving a cached error page with a 200 status; network glitch yielding a partial JSON body.
Related errors
- ibm: unexpected API response — expected hits.hits[], got key
- jobicy: unexpected API response — expected { jobs: [...] },
- 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/d5df9b34dca5ac38.
Report an issue: GitHub.