{"record":{"id":"d5df9b34dca5ac38","repo":"santifer/career-ops","slug":"himalayas-unexpected-api-response-expected-jo","errorCode":null,"errorMessage":"himalayas: unexpected API response - expected { jobs: [...] }, got keys: [${json ? Object.keys(json).join(', ') : 'null'}]","messagePattern":"himalayas: unexpected API response - expected (.+?), got keys: \\[(.+?)\\]","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/himalayas.mjs","lineNumber":89,"sourceCode":"  id: 'himalayas',\n\n  detect(entry) {\n    return entry?.provider === 'himalayas' ? { url: FEED_URL } : null;\n  },\n\n  /**\n   * Fetches and normalizes postings from the Himalayas public feed.\n   * @param {{ provider?: string }} entry - The job_boards entry being processed.\n   * @param {{ fetchJson: (url: string, opts?: { redirect?: 'error'|'follow'|'manual' }) => Promise<any> }} ctx - HTTP context.\n   * @returns {Promise<Array<{title: string, url: string, company: string, location: string, postedAt?: number}>>}\n   */\n  async fetch(entry, ctx) {\n    const feedUrl = assertHimalayasUrl(FEED_URL);\n    // redirect:'error' prevents SSRF via server-side redirects; combined with\n    // assertHimalayasUrl above it keeps the request pinned to himalayas.app.\n    const json = await ctx.fetchJson(feedUrl, { redirect: 'error' });\n    if (!json || !Array.isArray(json.jobs)) {\n      throw new Error(`himalayas: unexpected API response - expected { jobs: [...] }, got keys: [${json ? Object.keys(json).join(', ') : 'null'}]`);\n    }\n    return parseHimalayasResponse(json);\n  },\n};\n\n/**\n * Parse Himalayas' public jobs API response. Exported for unit tests.\n *\n * Shape: `{ jobs: [...] }`, where each job currently carries `title`,\n * `companyName`, `locationRestrictions`, `applicationLink`, `guid`,\n * `pubDate`, and `companySlug`. `applicationLink` is preferred over `guid`\n * and used as the dedup key after HTTPS + host validation.\n *\n * @param {unknown} json - raw parsed API response\n * @returns {Array<{title: string, url: string, company: string, location: string, postedAt?: number}>}\n */\nexport function parseHimalayasResponse(json) {\n  if (!json || typeof json !== 'object' || !Array.isArray(json.jobs)) return [];","sourceCodeStart":71,"sourceCodeEnd":107,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/himalayas.mjs#L71-L107","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// Optional pre-flight: fetch the feed and shape-check before relying on it.\nconst json = await ctx.fetchJson(FEED_URL, { redirect: 'error' });\nif (!json || !Array.isArray(json.jobs)) {\n  console.warn(`himalayas: unexpected shape — keys: [${json ? Object.keys(json).join(', ') : 'null'}]`);\n}","typeGuard":"/** Himalayas feed payload with the expected { jobs: [] } envelope. */\nfunction isHimalayasFeed(json) {\n  return !!json && typeof json === 'object' && Array.isArray(json.jobs);\n}","tryCatchPattern":"try {\n  return await himalayasProvider.fetch(entry, ctx);\n} catch (err) {\n  if (/unexpected API response/.test(err.message)) {\n    // Likely contract drift or a transient error envelope — retry once, then surface.\n    console.warn(`himalayas: shape drift — ${err.message}`);\n    return []; // or rethrow after N retries, depending on policy\n  }\n  throw err;\n}","preventionTips":["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."],"tags":["external-api","data-integrity","himalayas","contract-drift"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}