{"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/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/landingjobs.mjs#L107-L133","documentation":"The landingjobs provider's fetch() calls ctx.fetchJson on the pinned FEED_URL and then asserts the payload is a JSON array of jobs. If the API returns a JSON object, a string, a number, or null instead, the provider throws this error rather than crash later inside .map. It signals an unexpected API contract change or a wrong/intercepted response.","triggerScenarios":"The feed endpoint returns 200 with a JSON object (e.g. {error: ...}, an error envelope, or a paginated object instead of a bare array), or fetchJson was pointed at a non-API page that parses as non-array JSON.","commonSituations":"LandingJobs changes their API response shape (breaking change), a captive portal / proxy returns an HTML or JSON error body with a 200 status, or the FEED_URL constant is stale after an API migration.","solutions":["Inspect the actual response body (curl the FEED_URL) to see what shape the API now returns.","Update the provider (or pin the correct FEED_URL) if LandingJobs changed the response contract — e.g. unwrap the array from a new envelope field.","Check for a proxy/firewall/DNS interception returning an error page with 200.","Retry later if it's a transient upstream issue; otherwise file a bug with the response shape observed."],"exampleFix":"// before\nreturn json.map(j => normalizeLandingJob(j, fallbackCompany)).filter(Boolean);\n// after\nconst jobs = Array.isArray(json) ? json : (json.jobs ?? []);\nif (!Array.isArray(jobs)) throw new Error('landingjobs: no job array in response');\nreturn jobs.map(j => normalizeLandingJob(j, fallbackCompany)).filter(Boolean);","handlingStrategy":"type-guard","validationCode":"const json = await res.json();\nif (!Array.isArray(json)) throw new Error(`Unexpected feed shape: ${JSON.stringify(json).slice(0, 200)}`);","typeGuard":"function isJobArray(v) { return Array.isArray(v) && v.every(j => j && typeof j === 'object'); }","tryCatchPattern":"try {\n  return await provider.fetch(entry, ctx);\n} catch (e) {\n  if (e.message.startsWith('landingjobs: unexpected API response')) {\n    console.warn('LandingJobs feed shape changed or returned an error envelope; skipping');\n    return [];\n  }\n  throw e;\n}","preventionTips":["Snapshot the feed schema in tests to catch upstream contract changes","Check status code and content-type before trusting parsed JSON","Log a truncated body when the shape check fails for debugging","Degrade per-provider so a broken feed doesn't fail the whole scan"],"tags":["api","response-validation","json","upstream"],"backgroundTag":"unexpected-api-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"}