{"record":{"id":"986c998ff0290920","repo":"santifer/career-ops","slug":"landingjobs-unexpected-api-response-expected-a","errorCode":null,"errorMessage":"landingjobs: unexpected API response — expected a JSON array, got ${json === null ? 'null' : typeof json}","messagePattern":"landingjobs: unexpected API response — expected a JSON array, got (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/landingjobs.mjs","lineNumber":125,"sourceCode":"  const location = [base, j.remote === true ? 'Remote' : ''].filter(Boolean).join(', ');\n\n  /** @type {{ title: string, url: string, company: string, location: string, postedAt?: number }} */\n  const job = { title, url, company, location };\n  const postedAt = toEpochMs(j.published_at) ?? toEpochMs(j.created_at);\n  if (postedAt !== undefined) job.postedAt = postedAt;\n  return job;\n}\n\n/** @type {Provider} */\nexport default {\n  id: 'landingjobs',\n\n  async fetch(entry, ctx) {\n    assertLandingUrl(FEED_URL);\n    // redirect:'error' prevents SSRF via server-side redirects\n    const json = await ctx.fetchJson(FEED_URL, { redirect: 'error' });\n    if (!Array.isArray(json)) {\n      throw new Error(\n        `landingjobs: unexpected API response — expected a JSON array, got ${json === null ? 'null' : typeof json}`,\n      );\n    }\n    const fallbackCompany = entry?.name;\n    return json.map(j => normalizeLandingJob(j, fallbackCompany)).filter(Boolean);\n  },\n};\n","sourceCodeStart":107,"sourceCodeEnd":133,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/landingjobs.mjs#L107-L133","documentation":"The LandingJobs provider fetches a JSON feed (FEED_URL) and requires the top-level value to be an array of job postings. This error fires after fetchJson succeeds (HTTP response parsed as JSON) but the result is not an Array — it reports the actual type received (or 'null'). It guards the downstream .map().filter() chain, which would otherwise throw a less informative TypeError.","triggerScenarios":"ctx.fetchJson(FEED_URL) returns a JSON object (e.g. an envelope like {data:[...]} or {jobs:[...]}), returns null, returns a single job object, or returns a maintenance/error payload such as {error:'rate limited'}. The template literal embeds json===null?'null':typeof json so the message distinguishes null from object/string.","commonSituations":"LandingJobs changes its feed response shape (wraps results in an envelope); a temporary outage returns a JSON error body instead of the feed; the FEED_URL constant was edited to point at a non-feed endpoint; a proxy/CDN injects a JSON status object.","solutions":["Log the received value (console.log(json) before the throw) to see the actual shape LandingJobs returned.","If the API now wraps jobs in an envelope, unwrap it: replace the Array.isArray check with `const arr = Array.isArray(json) ? json : json?.jobs || json?.data; if(!Array.isArray(arr)) throw ...`.","Verify FEED_URL still points at the documented feed endpoint by curl-ing it directly.","If null/object corresponds to a known rate-limit or maintenance response, surface it as a distinct, retriable error instead of a hard failure."],"exampleFix":"// before\nif (!Array.isArray(json)) {\n  throw new Error(`landingjobs: unexpected API response — expected a JSON array, got ${json === null ? 'null' : typeof json}`);\n}\nreturn json.map(j => normalizeLandingJob(j, fallbackCompany)).filter(Boolean);\n\n// after — tolerate a common envelope shape\nconst arr = Array.isArray(json) ? json : (json && (Array.isArray(json.jobs) ? json.jobs : Array.isArray(json.data) ? json.data : null));\nif (!Array.isArray(arr)) {\n  throw new Error(`landingjobs: unexpected API response — expected a JSON array or {jobs|data:[]}, got ${json === null ? 'null' : typeof json}`);\n}\nreturn arr.map(j => normalizeLandingJob(j, fallbackCompany)).filter(Boolean);","handlingStrategy":"try-catch","validationCode":"// Pre-check the response shape before relying on the provider's own guard.\n// Useful when you call ctx.fetchJson directly in a custom integration.\nfunction isLandingJobsFeed(value) {\n  return Array.isArray(value);\n}\n// const json = await ctx.fetchJson(url, { redirect: 'error' });\n// if (!isLandingJobsFeed(json)) { /* log + skip, or unwrap envelope */ }","typeGuard":"/** @param {unknown} v */\nfunction isJobArray(v) {\n  return Array.isArray(v) && v.every(item => item && typeof item === 'object');\n}","tryCatchPattern":"// At the scan/orchestrator level, isolate each provider so one bad\n// response shape never aborts the whole sweep.\ntry {\n  const jobs = await provider.fetch(entry, ctx);\n  results.push(...jobs);\n} catch (err) {\n  console.error(`[skip] ${provider.id} (${entry.name}): ${err.message}`);\n  // continue with the next provider — do not rethrow for shape mismatches\n}","preventionTips":["Pin FEED_URL to the documented LandingJobs feed endpoint and treat changes to it as a code review item.","Log the raw json (type + keys) before the throw so a response-shape change is diagnosable in one run.","Wrap every provider.fetch() at the orchestrator in try/catch and continue past failures.","If LandingJobs is known to envelope results, normalize once in normalizeLandingJob's caller rather than relying on a bare array."],"tags":["api-contract","json","landingjobs","response-shape"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}