{"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/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/workingnomads.mjs#L8-L41","documentation":"Working Nomads' feed is expected to be a top-level JSON array. This throws when ctx.fetchJson returned something that is not an array — an object (error envelope), null, or a primitive — surfacing the actual type in the message.","triggerScenarios":"The endpoint https://www.workingnomads.com/api/exposed_jobs/ returned a JSON object instead of an array (contract change or error body), returned null, or an upstream proxy returned a JSON error envelope. A zero-length array [ ] does NOT trigger this — only a non-array top-level value does.","commonSituations":"Working Nomads changed their API shape; the endpoint is temporarily returning an error object {error: ...}; a CDN returned a JSON-formatted error; the feed URL moved.","solutions":["Fetch the URL directly to see the current shape: curl -s https://www.workingnomads.com/api/exposed_jobs/ | head -c 500.","If the shape changed permanently, update workingnomads.mjs parsing to match the new contract (e.g. unwrap a {jobs: [...]} envelope).","If it is an error envelope, retry later — likely transient.","If the feed moved, update FEED_URL."],"exampleFix":"// before\nconst data = await ctx.fetchJson(FEED_URL, { redirect: \"error\" });\nif (!Array.isArray(data)) throw new Error(`workingnomads: unexpected API response ...`);\n// after — tolerate a wrapped envelope if Working Nomads changes shape\nconst data = await ctx.fetchJson(FEED_URL, { redirect: \"error\" });\nconst rows = Array.isArray(data) ? data : Array.isArray(data?.jobs) ? data.jobs : null;\nif (!rows) throw new Error(`workingnomads: unexpected API response — got ${data === null ? \"null\" : typeof data}`);","handlingStrategy":"try-catch","validationCode":"// probe the feed shape out-of-band before relying on it\nconst probe = await fetch('https://www.workingnomads.com/api/exposed_jobs/').then(r => r.json());\nif (!Array.isArray(probe)) console.warn(\"workingnomads feed shape changed:\", typeof probe);","typeGuard":"const isJobArray = (d) => Array.isArray(d) &&\n  d.every(j => j && typeof j === \"object\" && typeof j.title === \"string\" && typeof j.url === \"string\");","tryCatchPattern":"try {\n  jobs = await workingnomads.fetch(entry, ctx);\n} catch (err) {\n  if (/workingnomads: unexpected API response/.test(err.message)) {\n    logApiContractChange(\"workingnomads\", err.message);\n    continue; // skip this provider, keep scanning others\n  }\n  throw err;\n}","preventionTips":["Treat this error as an upstream-contract-change signal and alert on it.","Add a smoke test that asserts the feed is an array.","Pin the provider version; review when Working Nomads announces API changes."],"tags":["api-contract","workingnomads","validation","parsing"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}