{"record":{"id":"2d0ccdc53e6d6c45","repo":"santifer/career-ops","slug":"remotli-unexpected-api-response-expected-jobs","errorCode":null,"errorMessage":"remotli: unexpected API response — expected { jobs: [...] }, got ${data === null ? 'null' : typeof data}","messagePattern":"remotli: unexpected API response — expected (.+?), got (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/remotli.mjs","lineNumber":297,"sourceCode":"    // or a page-1 body that isn't `{ jobs: [...] }` — means we cannot tell a\n    // live board from a broken one, so it must throw and surface as a dead\n    // target. Once one page has parsed, the board is provably reachable and a\n    // later transient failure must not discard what we already collected.\n    let succeededOnce = false;\n\n    for (let page = 1; page <= Math.min(cap, totalPages); page++) {\n      const url = `${ORIGIN}${API_PATH}?page=${page}&limit=${PAGE_SIZE}&${ALL_WORK_MODES}`;\n      assertRemotliUrl(url);\n\n      /** @type {any[]} */\n      let rows;\n      try {\n        // redirect:'error' prevents SSRF via server-side redirects; combined with\n        // assertRemotliUrl this pins every hop to remotli.ch.\n        const data = await ctx.fetchJson(url, { redirect: 'error' });\n\n        if (!data || typeof data !== 'object' || !Array.isArray(/** @type {any} */ (data).jobs)) {\n          throw new Error(\n            `remotli: unexpected API response — expected { jobs: [...] }, got ${data === null ? 'null' : typeof data}`,\n          );\n        }\n\n        rows = /** @type {any} */ (data).jobs;\n\n        const reported = Number(/** @type {any} */ (data).pagination?.totalPages);\n        if (Number.isInteger(reported) && reported > 0) totalPages = reported;\n      } catch (err) {\n        if (!succeededOnce) throw err;\n        break; // keep the pages already collected — a mid-scan blip isn't a dead board\n      }\n\n      // Set only after the shape check passed, so a malformed body never counts\n      // as proof of life.\n      succeededOnce = true;\n\n      for (const row of rows) {","sourceCodeStart":279,"sourceCodeEnd":315,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/remotli.mjs#L279-L315","documentation":"After fetching, the remotli provider verifies the response is an object containing a jobs array. If the parsed JSON is null, a primitive, or an object without a jobs array, it throws this error. It guards against the API's contract changing or an HTML/error page being returned in place of JSON.","triggerScenarios":"ctx.fetchJson returns data that is null, a non-object (string/number), or an object whose .jobs is not an Array — e.g. the API returned {error:...}, an empty HTML page serialized oddly, or the endpoint version changed shape.","commonSituations":"remotli.ch API redesigned its response schema; hitting an HTML error/captcha page that got mis-parsed; querying a deprecated endpoint; or a proxy returning a JSON error body.","solutions":["Log the actual response body to confirm the new shape, then update the provider's parsing to match the current API contract.","Check whether the endpoint URL is still the correct API route (an HTML 404 page can end up as non-conforming data).","Add a fallback parse (e.g. accept {results:[...]}) or bump/reinstall the provider package if a newer version handles the new schema."],"exampleFix":"// before\nif (!data || typeof data !== 'object' || !Array.isArray(data.jobs)) throw new Error('remotli: unexpected API response');\n// after\nconst rows = Array.isArray(data?.jobs) ? data.jobs : Array.isArray(data?.results) ? data.results : null;\nif (!rows) throw new Error('remotli: unexpected API response');","handlingStrategy":"try-catch","validationCode":"const res = await fetch(url); const data = await res.json();\nconst looksValid = data && typeof data === 'object' && Array.isArray(data.jobs);\nif (!looksValid) console.warn('Unexpected remotli payload:', JSON.stringify(data).slice(0, 200));","typeGuard":"function isJobsPayload(d) { return !!d && typeof d === 'object' && Array.isArray(d.jobs); }","tryCatchPattern":"try { rows = await provider.fetch(entry, ctx); } catch (e) { if (e.message.startsWith('remotli: unexpected API response')) { logRawResponseForDiff(); return null; } throw e; }","preventionTips":["Pin/monitor the upstream API version and watch for schema changes.","Log raw response bodies on failure for fast diagnosis.","Wrap fetchJson with a schema check before consuming fields."],"tags":["api-response","schema","json","network"],"backgroundTag":"unexpected-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"}