{"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":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/remotli.mjs","lineNumber":296,"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":278,"sourceCodeEnd":314,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/remotli.mjs#L278-L314","documentation":"During pagination, remotli fetches each page via ctx.fetchJson and asserts the response is an object with a jobs array. It throws on the first page (page 1) if the shape is wrong; on later pages, if succeededOnce is true, a shape failure breaks the loop (keeping already-collected rows) rather than throwing. So this error only surfaces when the very first request returns a non-conformant body.","triggerScenarios":"The first-page API response is null, a primitive, or an object lacking a jobs array (e.g. an error envelope { error, message }, a maintenance page, or an HTML-converted-to-JSON blob). Only fires when succeededOnce is still false.","commonSituations":"remotli.ch API is down or rate-limiting on the initial request; a proxy returned a JSON error block; the API_PATH constant changed and the first page 404'd into an error object; the response encoding changed.","solutions":["Retry the scan — transient first-page failures clear on the next run.","curl the constructed first-page URL to inspect the actual response body and shape.","Verify API_PATH, PAGE_SIZE, and ALL_WORK_MODES query parameters still match the live endpoint.","If the API permanently changed, update the shape check to match the new envelope."],"exampleFix":"// before — throws on first page if shape mismatches\nif (!data || typeof data !== 'object' || !Array.isArray(data.jobs)) throw new Error(...);\n// after — log and retry once before giving up\nif (!data || typeof data !== 'object' || !Array.isArray(data.jobs)) {\n  console.error('remotli: bad first-page shape', JSON.stringify(data).slice(0, 200));\n  throw new Error('remotli: unexpected API response');\n}","handlingStrategy":"retry","validationCode":"// Pre-flight the first page shape before committing to the full scan\nasync function probeRemotliFirstPage(ctx) {\n  const url = `${ORIGIN}${API_PATH}?page=1&limit=${PAGE_SIZE}&${ALL_WORK_MODES}`;\n  const data = await ctx.fetchJson(url, { redirect: 'error' });\n  return !!data && typeof data === 'object' && Array.isArray(data.jobs);\n}\nif (!(await probeRemotliFirstPage(ctx))) {\n  console.warn('remotli: first-page shape invalid — API down or changed');\n}","typeGuard":"/** @param {unknown} d */\nfunction isRemotliPage(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    // only the first page throws; retry once after a delay\n    await new Promise(r => setTimeout(r, 5000));\n    await provider.fetch(entry, ctx);\n  } else throw e;\n}","preventionTips":["Retry once on first-page shape errors — they are often transient outages.","Note that mid-scan shape failures do NOT throw (succeededOnce softens them); only page 1 is strict.","curl the constructed first-page URL to diagnose persistent shape changes."],"tags":["api-contract","parsing","provider","remotli","response-shape","pagination","network"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}