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

  1. Retry the scan once — transient rate-limit/error envelopes usually clear.
  2. Open https://himalayas.app/jobs/api?limit=50 directly and inspect the top-level keys; if jobs was renamed, update parseHimalayasResponse and the guard.
  3. If the endpoint persistently returns an error envelope, disable the himalayas board entry until the feed recovers.
  4. 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

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


AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13). Data as JSON: /api/errors/d5df9b34dca5ac38. Report an issue: GitHub.