{"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":"validation","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/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/remotive.mjs#L10-L43","documentation":"The remotive provider fetches FEED_URL and asserts the response is an object with a jobs array ({ jobs: [...] }). It throws when json is falsy or json.jobs is not an array, listing the actual top-level keys in the message for diagnostics. This catches upstream contract changes, error envelopes, and outages.","triggerScenarios":"The feed returns null; an error object like { error, message } without a jobs key; a maintenance page parsed as { status: 'down' }; a version change that renamed jobs to results or offers.","commonSituations":"Remotive API is under maintenance; a rate-limit response replaced the normal payload; the FEED_URL was updated to a new API version with a different envelope; a proxy returned a JSON error block.","solutions":["Retry after a delay — outages and rate limits are often transient.","curl the FEED_URL to see the current top-level keys; the error message already lists them.","If remotive renamed the field, update the parser to read the new key (e.g. results) into json.jobs.","Confirm the FEED_URL constant matches the current documented endpoint."],"exampleFix":"// before\nif (!json || !Array.isArray(json.jobs)) throw new Error(...);\n// after — tolerate renamed field\nconst jobs = json?.jobs ?? json?.results;\nif (!Array.isArray(jobs)) throw new Error('remotive: unexpected API response');","handlingStrategy":"type-guard","validationCode":"async function probeRemotive(ctx) {\n  const json = await ctx.fetchJson(FEED_URL, { redirect: 'error' });\n  return !!json && Array.isArray(json.jobs);\n}\nif (!(await probeRemotive(ctx))) {\n  console.warn('remotive: feed missing jobs array — skipping');\n}","typeGuard":"/** @param {unknown} d */\nfunction isRemotiveFeed(d) {\n  return !!d && typeof d === 'object' && Array.isArray(/** @type{any}*/(d).jobs);\n}","tryCatchPattern":"try {\n  await provider.fetch(entry, ctx);\n} catch (e) {\n  if (/unexpected API response/.test(e.message)) {\n    await new Promise(r => setTimeout(r, 5000));\n    await provider.fetch(entry, ctx);\n  } else throw e;\n}","preventionTips":["Retry once on shape errors — outages and rate limits are often transient.","The error message lists the actual keys, which pinpoints the upstream change.","Keep the FEED_URL constant synced with the documented endpoint."],"tags":["api-contract","parsing","provider","remotive","response-shape","network"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}