santifer/career-ops · error · Error

jobicy: unexpected API response — expected

Error message

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

What it means

The Jobicy provider fetches https://jobicy.com/api/v2/remote-jobs and expects a JSON object with a jobs array ({ jobs: [...] }). If the parsed JSON is null/undefined or lacks an array at .jobs, fetch() throws, including the actual top-level keys seen (or 'null') to expose what came back instead. This is a defensive shape check against API changes, error pages, and HTML-as-JSON responses.

Solutions

  1. Check the printed keys list in the message — it tells you what the API actually returned
  2. Verify the Jobicy v2 API is up and the contract still returns { jobs: [...] } (curl https://jobicy.com/api/v2/remote-jobs?count=50)
  3. If rate-limited, back off and retry later with a lower count parameter
  4. Check for proxy/captive-portal interference returning HTML instead of JSON
  5. If the API schema changed, update the provider's parseJobicyResponse to the new shape

Example fix

// before: assuming the shape blindly
const jobs = json.jobs.map(parseJob);
// after: guarding before use
if (!json || !Array.isArray(json.jobs)) {
  throw new Error(`jobicy: unexpected API response, keys: ${json ? Object.keys(json) : 'null'}`);
}
const jobs = json.jobs.map(parseJob);
Defensive patterns

Strategy: try-catch

Validate before calling

function looksLikeJobicyPayload(json) {
  return !!json && typeof json === 'object' && Array.isArray(json.jobs);
}

Type guard

function isJobicyResponse(v) {
  return typeof v === 'object' && v !== null && Array.isArray(v.jobs);
}

Try / catch

try {
  const json = await ctx.fetchJson(FEED_URL, { redirect: 'error' });
  if (!isJobicyResponse(json)) {
    console.warn('Jobicy returned unexpected payload; skipping board this run');
    return [];
  }
} catch (e) {
  console.warn(`Jobicy fetch failed: ${e.message}`);
  return [];
}

Prevention

When it happens

Trigger: ctx.fetchJson(FEED_URL, { redirect: 'error' }) resolves with something whose shape is wrong: Jobicy returned an error object ({error:...}), an empty body parsed to null, an HTML error page, an API version change renaming jobs, or a rate-limit JSON body.

Common situations: Jobicy API downtime or maintenance returning error payloads; a rate-limit response; the API contract changing (v2 schema update); a captive portal / proxy returning HTML; redirect:'error' surfaces upstream differently but a 200-with-error-body still reaches this check.

Related errors


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

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