santifer/career-ops · error · Error

jobicy: unexpected API response — expected { jobs: [...] },

Error message

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

What it means

Thrown by jobicy fetch() when the parsed JSON response is falsy or its jobs field is not an array. The Jobicy public feed is expected to return { jobs: [...] }; any other shape is a contract break and is surfaced as a hard error (with the actual top-level keys in the message) so an endpoint change does not masquerade as an empty result.

Source

Thrown at providers/jobicy.mjs:29

/** @type {Provider} */
export default {
  id: 'jobicy',

  detect(entry) {
    return entry?.provider === 'jobicy' ? { url: FEED_URL } : null;
  },

  /**
   * Fetches and normalizes postings from the Jobicy public feed.
   * @param {{ name?: 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) {
    // redirect:'error' prevents SSRF via server-side redirects
    const json = await ctx.fetchJson(FEED_URL, { redirect: 'error' });
    if (!json || !Array.isArray(json.jobs)) {
      throw new Error(`jobicy: unexpected API response — expected { jobs: [...] }, got keys: [${json ? Object.keys(json).join(', ') : 'null'}]`);
    }

    return parseJobicyResponse(json, entry.name || 'Jobicy');
  },
};

/**
 * Parse a Jobicy API response. Exported for unit tests.
 *
 * @param {any} json - Raw response payload.
 * @param {string} defaultCompany - Fallback company name.
 * @returns {Array<{title: string, url: string, company: string, location: string}>}
 */
export function parseJobicyResponse(json, defaultCompany = 'Jobicy') {
  if (!json || !Array.isArray(json.jobs)) return [];

  const toEpochMs = (value) => {
    if (!value) return undefined;

View on GitHub (pinned to 9b17a8ac97)

Solutions

  1. Retry once to clear a transient error envelope.
  2. Open the Jobicy feed URL directly and inspect the top-level keys shown in the message; if jobs was renamed, update parseJobicyResponse and the guard.
  3. If the endpoint persistently returns a different shape, disable the jobicy entry until the parser is updated.
  4. Pin the known-good shape in a regression test.
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-flight 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(`jobicy: unexpected shape — keys: [${json ? Object.keys(json).join(', ') : 'null'}]`);
}

Type guard

/** Jobicy feed payload with the expected { jobs: [] } envelope. */
function isJobicyFeed(json) {
  return !!json && typeof json === 'object' && Array.isArray(json.jobs);
}

Try / catch

try {
  return await jobicyProvider.fetch(entry, ctx);
} catch (err) {
  if (/unexpected API response/.test(err.message)) {
    console.warn(`jobicy: shape drift — ${err.message}`);
    return []; // or retry/backoff per policy
  }
  throw err;
}

Prevention

When it happens

Trigger: Jobicy renamed the jobs key; the endpoint returned an error envelope; rate limiting returned a JSON error object; the endpoint moved and serves a different JSON document; a transient 200 with a null/empty body.

Common situations: Upstream API contract drift after a Jobicy release; CDN/proxy returning a cached error with 200; partial JSON from a network glitch; an A/B test serving a new shape.

Related errors


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