{"record":{"id":"06d9547434b3e161","repo":"santifer/career-ops","slug":"remotive-unexpected-api-response-expected-job","errorCode":null,"errorMessage":"remotive: unexpected API response — expected { jobs: [...] }, got keys: [${json ? Object.keys(json).join(', ') : 'null'}]","messagePattern":"remotive: unexpected API response — expected (.+?), got keys: \\[(.+?)\\]","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/remotive.mjs","lineNumber":28,"sourceCode":"// Wire in via a `job_boards:` entry with `provider: remotive`.\n\nconst FEED_URL = 'https://remotive.com/api/remote-jobs';\n\n/** @type {Provider} */\nexport default {\n  id: 'remotive',\n\n  /**\n   * Fetches and normalizes postings from the Remotive 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 json = await ctx.fetchJson(FEED_URL, { redirect: 'error' });\n    if (!json || !Array.isArray(json.jobs)) {\n      throw new Error(`remotive: unexpected API response — expected { jobs: [...] }, got keys: [${json ? Object.keys(json).join(', ') : 'null'}]`);\n    }\n\n    return json.jobs\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 || 'Remotive'),\n        location: typeof j.candidate_required_location === 'string' ? j.candidate_required_location.trim() : '',\n      }));\n  },\n};\n","sourceCodeStart":10,"sourceCodeEnd":43,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/remotive.mjs#L10-L43","documentation":"Remotive's public feed must return a JSON object with a `jobs` array ({ jobs: [...] }). fetch() throws this error when the response is null, not an object, or lacks a jobs array. The message includes the observed top-level keys (or 'null') to aid debugging.","triggerScenarios":"The Remotive API returns null/empty body, a bare array, a JSON error object without `jobs`, or changes its envelope; rate limiting or CDN error pages that still parse as JSON.","commonSituations":"Remotive deploys an API version change moving `jobs` under another key; a proxy returns a JSON error payload; transient outage where the feed returns an error object.","solutions":["Retry after a delay; transient error bodies are common","Fetch the Remotive feed URL manually and inspect the top-level keys shown in the error message","Check Remotive API docs/changelog for an envelope change (e.g. jobs renamed or nested under data) and update the provider parser","Verify no proxy/middleware is rewriting the response body"],"exampleFix":"null","handlingStrategy":"try-catch","validationCode":"null","typeGuard":"function hasJobsArray(json) {\n  return json !== null && typeof json === 'object' && Array.isArray(json.jobs);\n}","tryCatchPattern":"try {\n  const jobs = await remotive.fetch(entry, ctx);\n} catch (err) {\n  if (err.message.startsWith('remotive: unexpected API response')) {\n    console.warn(`Remotive envelope changed or errored (${err.message}) — check feed manually`);\n    return [];\n  }\n  throw err;\n}","preventionTips":["Check the error's keys list first — it tells you exactly what envelope came back","Watch Remotive API changelogs for jobs-key renames or nesting changes","Add a health probe that calls the feed and asserts { jobs: [...] } before scheduled scans"],"tags":["network","api","schema-validation"],"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"}