santifer/career-ops · error · Error

himalayas: unexpected API response - expected

Error message

himalayas: unexpected API response - expected { jobs: [...] }, got keys: [${json ? Object.keys(json).join(', ') : 'null'}]

What it means

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.

Solutions

  1. Fetch the FEED_URL manually and inspect the actual response shape to confirm the schema changed
  2. Update the check and parseHimalayasResponse to match the new feed envelope (e.g. { data: { jobs: [...] } })
  3. Check for rate limiting or auth errors in the returned keys and address the upstream cause
  4. Pin/report the provider breakage upstream and skip the himalayas board until fixed

Example fix

// before
if (!json || !Array.isArray(json.jobs)) throw new Error(...);
// after
const jobs = json?.jobs ?? json?.data?.jobs;
if (!Array.isArray(jobs)) throw new Error(...);
Defensive patterns

Strategy: type-guard

Validate before calling

function looksLikeHimalayasFeed(json) {
  return !!json && typeof json === 'object' && !Array.isArray(json) && Array.isArray(json.jobs);
}
// guard the raw text before parse too: contentType includes 'application/json'

Type guard

const isFeedShape = (v) =>
  typeof v === 'object' && v !== null && !Array.isArray(v) && Array.isArray(v.jobs);

Try / catch

try {
  const jobs = await himalayasProvider.fetch(entry, ctx);
} catch (err) {
  if (/unexpected API response/.test(err.message)) {
    console.error('himalayas feed schema changed or returned an error body:', err.message);
    return []; // skip board, alert maintainer
  }
  throw err;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: "invalid response format", "malformed payload", "missing data field": when an API returns 200 but the response shape is wrong — this error's family across 23 libraries.

Related errors


AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16). Data as JSON: /api/errors/d5df9b34dca5ac38. Report an issue: GitHub.

Appendix: 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 aac998c7ed)