{"record":{"id":"cd592334128b8b9b","repo":"santifer/career-ops","slug":"jobicy-unexpected-api-response-expected-jobs","errorCode":null,"errorMessage":"jobicy: unexpected API response — expected { jobs: [...] }, got keys: [${json ? Object.keys(json).join(', ') : 'null'}]","messagePattern":"jobicy: unexpected API response — expected (.+?), got keys: \\[(.+?)\\]","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/jobicy.mjs","lineNumber":29,"sourceCode":"/** @type {Provider} */\nexport default {\n  id: 'jobicy',\n\n  detect(entry) {\n    return entry?.provider === 'jobicy' ? { url: FEED_URL } : null;\n  },\n\n  /**\n   * Fetches and normalizes postings from the Jobicy public feed.\n   * @param {{ name?: 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    // redirect:'error' prevents SSRF via server-side redirects\n    const json = await ctx.fetchJson(FEED_URL, { redirect: 'error' });\n    if (!json || !Array.isArray(json.jobs)) {\n      throw new Error(`jobicy: unexpected API response — expected { jobs: [...] }, got keys: [${json ? Object.keys(json).join(', ') : 'null'}]`);\n    }\n\n    return parseJobicyResponse(json, entry.name || 'Jobicy');\n  },\n};\n\n/**\n * Parse a Jobicy API response. Exported for unit tests.\n *\n * @param {any} json - Raw response payload.\n * @param {string} defaultCompany - Fallback company name.\n * @returns {Array<{title: string, url: string, company: string, location: string}>}\n */\nexport function parseJobicyResponse(json, defaultCompany = 'Jobicy') {\n  if (!json || !Array.isArray(json.jobs)) return [];\n\n  const toEpochMs = (value) => {\n    if (!value) return undefined;","sourceCodeStart":11,"sourceCodeEnd":47,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/jobicy.mjs#L11-L47","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Check the printed keys list in the message — it tells you what the API actually returned","Verify the Jobicy v2 API is up and the contract still returns { jobs: [...] } (curl https://jobicy.com/api/v2/remote-jobs?count=50)","If rate-limited, back off and retry later with a lower count parameter","Check for proxy/captive-portal interference returning HTML instead of JSON","If the API schema changed, update the provider's parseJobicyResponse to the new shape"],"exampleFix":"// before: assuming the shape blindly\nconst jobs = json.jobs.map(parseJob);\n// after: guarding before use\nif (!json || !Array.isArray(json.jobs)) {\n  throw new Error(`jobicy: unexpected API response, keys: ${json ? Object.keys(json) : 'null'}`);\n}\nconst jobs = json.jobs.map(parseJob);","handlingStrategy":"try-catch","validationCode":"function looksLikeJobicyPayload(json) {\n  return !!json && typeof json === 'object' && Array.isArray(json.jobs);\n}","typeGuard":"function isJobicyResponse(v) {\n  return typeof v === 'object' && v !== null && Array.isArray(v.jobs);\n}","tryCatchPattern":"try {\n  const json = await ctx.fetchJson(FEED_URL, { redirect: 'error' });\n  if (!isJobicyResponse(json)) {\n    console.warn('Jobicy returned unexpected payload; skipping board this run');\n    return [];\n  }\n} catch (e) {\n  console.warn(`Jobicy fetch failed: ${e.message}`);\n  return [];\n}","preventionTips":["Pin/monitor the Jobicy v2 API contract; subscribe to their changelog","Back off on rate-limit responses before retrying","Log the full payload (keys at minimum) when the shape check fails","Check for proxies returning HTML error pages instead of JSON"],"tags":["api","json","schema","remote-jobs","response-shape"],"backgroundTag":"unexpected-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"}