{"record":{"id":"288cd494e4fc9b1d","repo":"santifer/career-ops","slug":"remoteok-unexpected-api-response-expected-a-jso","errorCode":null,"errorMessage":"remoteok: unexpected API response — expected a JSON array, got ${data === null ? 'null' : typeof data}","messagePattern":"remoteok: unexpected API response — expected a JSON array, got (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/remoteok.mjs","lineNumber":29,"sourceCode":"// private scanning, but don't redistribute this feed publicly without it.\n\nconst FEED_URL = 'https://remoteok.com/api';\n\n/** @type {Provider} */\nexport default {\n  id: 'remoteok',\n\n  /**\n   * Fetches and normalizes postings from the RemoteOK 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(`remoteok: 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.position === 'string' && j.position.trim() !== ''\n        && typeof j.url === 'string' && /^https?:\\/\\//i.test(j.url.trim()))\n      .map(j => ({\n        title: j.position.trim(),\n        url: j.url.trim(),\n        company: typeof j.company === 'string' && j.company.trim() ? j.company.trim() : (entry.name || 'RemoteOK'),\n        location: typeof j.location === 'string' ? j.location.trim() : '',\n      }));\n  },\n};\n","sourceCodeStart":11,"sourceCodeEnd":44,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/remoteok.mjs#L11-L44","documentation":"The remoteok provider fetches a single public feed (FEED_URL) and asserts the response is a JSON array. If ctx.fetchJson resolves to anything that is not an array — null, an object, a string, a number — it throws. This guards against partial outages, CDN error pages served as JSON, or upstream API contract changes that wrap the list in an envelope.","triggerScenarios":"The feed returns an error envelope like { error: '...' } or { data: [...] } instead of a bare array; the endpoint returns null; a rate-limit or maintenance page is parsed as a JSON object; the upstream changed its response shape.","commonSituations":"RemoteOK is temporarily rate-limiting and returns an error object; a proxy/gateway injected a wrapping object; the FEED_URL constant points at a stale or version-changed endpoint after an API revision.","solutions":["Retry after a short delay — transient rate-limit or maintenance responses are often intermittent.","Inspect the raw response: curl the FEED_URL directly to see the current shape.","If RemoteOK permanently changed its format, update the parser to unwrap the new envelope before the Array.isArray check.","Verify network egress is not being intercepted by a corporate proxy returning a JSON block page."],"exampleFix":"// before\nconst data = await ctx.fetchJson(FEED_URL, { redirect: 'error' });\nif (!Array.isArray(data)) throw new Error(...);\n// after — tolerate a wrapped envelope\nconst data = await ctx.fetchJson(FEED_URL, { redirect: 'error' });\nconst list = Array.isArray(data) ? data : (Array.isArray(data?.jobs) ? data.jobs : null);\nif (!list) throw new Error('remoteok: unexpected API response');","handlingStrategy":"type-guard","validationCode":"// Probe the feed shape before relying on the provider's strict assertion\nasync function probeRemoteOk(ctx) {\n  const data = await ctx.fetchJson(FEED_URL, { redirect: 'error' });\n  return Array.isArray(data);\n}\nif (!(await probeRemoteOk(ctx))) {\n  console.warn('remoteok: feed shape changed or endpoint down — skipping');\n}","typeGuard":"/** @param {unknown} d */\nfunction isRemoteOkFeed(d) {\n  return Array.isArray(d) && d.every(j => j == null || (typeof j === 'object' && typeof j.position === 'string'));\n}","tryCatchPattern":"try {\n  await provider.fetch(entry, ctx);\n} catch (e) {\n  if (/unexpected API response/.test(e.message)) {\n    // likely transient outage or rate limit — retry once after delay\n    await new Promise(r => setTimeout(r, 5000));\n    await provider.fetch(entry, ctx);\n  } else throw e;\n}","preventionTips":["Treat shape errors as potentially transient — retry once before giving up.","Monitor the feed endpoint for contract changes.","Wrap provider calls so a single provider outage does not abort the whole scan."],"tags":["api-contract","parsing","provider","remoteok","response-shape","network"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}