{"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/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/himalayas.mjs#L71-L107","documentation":"fetch() pulls the Himalayas jobs feed via ctx.fetchJson and requires the JSON body to be an object with a `jobs` array. When the parsed response is null or lacks a jobs array, it throws this error listing the top-level keys it actually received, so the developer can see what shape came back.","triggerScenarios":"ctx.fetchJson returns null, an array, or an object without `jobs` (e.g. { error: '...' }, { message: 'rate limited' }), or the feed endpoint changed its envelope from { jobs: [...] } to something else.","commonSituations":"Himalayas changes or deprecates its public feed schema; a captive portal / HTML error page was parsed leniently; a proxy returns an auth-error JSON body; the feed URL was updated to a v2 API with a different envelope.","solutions":["Fetch the FEED_URL manually and inspect the actual response shape to confirm the schema changed","Update the check and parseHimalayasResponse to match the new feed envelope (e.g. { data: { jobs: [...] } })","Check for rate limiting or auth errors in the returned keys and address the upstream cause","Pin/report the provider breakage upstream and skip the himalayas board until fixed"],"exampleFix":"// before\nif (!json || !Array.isArray(json.jobs)) throw new Error(...);\n// after\nconst jobs = json?.jobs ?? json?.data?.jobs;\nif (!Array.isArray(jobs)) throw new Error(...);","handlingStrategy":"type-guard","validationCode":"function looksLikeHimalayasFeed(json) {\n  return !!json && typeof json === 'object' && !Array.isArray(json) && Array.isArray(json.jobs);\n}\n// guard the raw text before parse too: contentType includes 'application/json'","typeGuard":"const isFeedShape = (v) =>\n  typeof v === 'object' && v !== null && !Array.isArray(v) && Array.isArray(v.jobs);","tryCatchPattern":"try {\n  const jobs = await himalayasProvider.fetch(entry, ctx);\n} catch (err) {\n  if (/unexpected API response/.test(err.message)) {\n    console.error('himalayas feed schema changed or returned an error body:', err.message);\n    return []; // skip board, alert maintainer\n  }\n  throw err;\n}","preventionTips":["Check fetchJson content-type/status before trusting the parsed body","Pin a schema test against a saved feed snapshot in CI","Log the received top-level keys on mismatch for quick diagnosis","Watch for provider API changelog/deprecation notices"],"tags":["api","schema","response-validation","network"],"backgroundTag":"unexpected-api-response-shape","analyzedSha":"aac998c7ed7248ea853b720ceeb1fdbeb322fc5d","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}