{"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/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/jobicy.mjs#L11-L47","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Retry once to clear a transient error envelope.","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.","If the endpoint persistently returns a different shape, disable the jobicy entry until the parser is updated.","Pin the known-good shape in a regression test."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// Pre-flight the feed and shape-check before relying on it.\nconst json = await ctx.fetchJson(FEED_URL, { redirect: 'error' });\nif (!json || !Array.isArray(json.jobs)) {\n  console.warn(`jobicy: unexpected shape — keys: [${json ? Object.keys(json).join(', ') : 'null'}]`);\n}","typeGuard":"/** Jobicy feed payload with the expected { jobs: [] } envelope. */\nfunction isJobicyFeed(json) {\n  return !!json && typeof json === 'object' && Array.isArray(json.jobs);\n}","tryCatchPattern":"try {\n  return await jobicyProvider.fetch(entry, ctx);\n} catch (err) {\n  if (/unexpected API response/.test(err.message)) {\n    console.warn(`jobicy: shape drift — ${err.message}`);\n    return []; // or retry/backoff per policy\n  }\n  throw err;\n}","preventionTips":["Pin the known-good Jobicy envelope in a regression test.","Use the echoed keys in the message to distinguish a rename from an error envelope.","Keep fetchJson strict about non-2xx so error bodies are not mistaken for odd shapes."],"tags":["external-api","data-integrity","jobicy","contract-drift"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}