{"record":{"id":"f8bebe6a6a1ce381","repo":"santifer/career-ops","slug":"workingnomads-unexpected-api-response-expected","errorCode":null,"errorMessage":"workingnomads: unexpected API response — expected a JSON array, got ${data === null ? 'null' : typeof data}","messagePattern":"workingnomads: unexpected API response — expected a JSON array, got (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/workingnomads.mjs","lineNumber":26,"sourceCode":"// Wire in via a `job_boards:` entry with `provider: workingnomads`.\n\nconst FEED_URL = 'https://www.workingnomads.com/api/exposed_jobs/';\n\n/** @type {Provider} */\nexport default {\n  id: 'workingnomads',\n\n  /**\n   * Fetches and normalizes postings from the Working Nomads 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}>>}\n   */\n  async fetch(entry, ctx) {\n    // redirect:'error' prevents SSRF via server-side redirects\n    const data = await ctx.fetchJson(FEED_URL, { redirect: 'error' });\n    if (!Array.isArray(data)) {\n      throw new Error(`workingnomads: unexpected API response — expected a JSON array, got ${data === null ? 'null' : typeof data}`);\n    }\n\n    return data\n      .filter(j => j && typeof j === 'object'\n        && typeof j.title === 'string' && j.title.trim() !== ''\n        && typeof j.url === 'string' && /^https?:\\/\\//i.test(j.url.trim()))\n      .map(j => ({\n        title: j.title.trim(),\n        url: j.url.trim(),\n        company: typeof j.company_name === 'string' && j.company_name.trim() ? j.company_name.trim() : (entry.name || 'Working Nomads'),\n        location: typeof j.location === 'string' ? j.location.trim() : '',\n      }));\n  },\n};\n","sourceCodeStart":8,"sourceCodeEnd":41,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/workingnomads.mjs#L8-L41","documentation":"The workingnomads provider fetches its fixed FEED_URL and requires the response to be a JSON array of jobs. When ctx.fetchJson returns null or any non-array value, fetch throws, treating the response as an unexpected API shape rather than silently returning zero results.","triggerScenarios":"fetch(entry, ctx) where the Working Nomads API returns null (fetch failure surfaced as null), an error object, or a wrapped payload like { jobs: [...] } instead of a top-level array.","commonSituations":"API downtime returning an HTML/JSON error page parsed to null; an API version change that wraps the array in an object; a proxy or rate-limiter returning a non-array body.","solutions":["Retry the fetch; transient API errors often resolve on the next run","Open FEED_URL directly and inspect the JSON shape for an API format change","If the payload is now wrapped (e.g. {jobs:[...]}), update the provider to unwrap the array","Check for rate limiting or auth requirements introduced by the API"],"exampleFix":"// before\nconst data = await ctx.fetchJson(FEED_URL, { redirect: 'error' });\n// after (unwrap if API changed)\nlet data = await ctx.fetchJson(FEED_URL, { redirect: 'error' });\nif (data && !Array.isArray(data) && Array.isArray(data.jobs)) data = data.jobs;","handlingStrategy":"try-catch","validationCode":"const data = await ctx.fetchJson(FEED_URL, { redirect: 'error' }).catch(() => null);\nif (!Array.isArray(data)) throw new Error('workingnomads feed not an array; skipping run');","typeGuard":"const isJobArray = (d) => Array.isArray(d) && d.every(j => j && typeof j === 'object' && typeof j.title === 'string' && typeof j.url === 'string');","tryCatchPattern":"try { jobs = await provider.fetch(entry, ctx); } catch (e) { if (e.message.includes('unexpected API response')) { console.warn('Working Nomads API shape changed or is down; retrying next run'); jobs = []; } else throw e; }","preventionTips":["Check the API is reachable and returns an array before large batch runs","Pin/monitor the upstream API for shape changes","Treat empty/garbage responses as transient and schedule a retry rather than failing the whole pipeline"],"tags":["api-response","json","unexpected-shape","workingnomads"],"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"}