santifer/career-ops · error · Error

remotli: unexpected API response — expected

Error message

remotli: unexpected API response — expected { jobs: [...] }, got ${data === null ? 'null' : typeof data}

What it means

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.

Solutions

  1. Log the actual response body to confirm the new shape, then update the provider's parsing to match the current API contract.
  2. Check whether the endpoint URL is still the correct API route (an HTML 404 page can end up as non-conforming data).
  3. Add a fallback parse (e.g. accept {results:[...]}) or bump/reinstall the provider package if a newer version handles the new schema.

Example fix

// before
if (!data || typeof data !== 'object' || !Array.isArray(data.jobs)) throw new Error('remotli: unexpected API response');
// after
const rows = Array.isArray(data?.jobs) ? data.jobs : Array.isArray(data?.results) ? data.results : null;
if (!rows) throw new Error('remotli: unexpected API response');
Defensive patterns

Strategy: try-catch

Validate before calling

const res = await fetch(url); const data = await res.json();
const looksValid = data && typeof data === 'object' && Array.isArray(data.jobs);
if (!looksValid) console.warn('Unexpected remotli payload:', JSON.stringify(data).slice(0, 200));

Type guard

function isJobsPayload(d) { return !!d && typeof d === 'object' && Array.isArray(d.jobs); }

Try / catch

try { rows = await provider.fetch(entry, ctx); } catch (e) { if (e.message.startsWith('remotli: unexpected API response')) { logRawResponseForDiff(); return null; } throw e; }

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16). Data as JSON: /api/errors/2d0ccdc53e6d6c45. Report an issue: GitHub.

Appendix: source

Thrown at providers/remotli.mjs:297

    // or a page-1 body that isn't `{ jobs: [...] }` — means we cannot tell a
    // live board from a broken one, so it must throw and surface as a dead
    // target. Once one page has parsed, the board is provably reachable and a
    // later transient failure must not discard what we already collected.
    let succeededOnce = false;

    for (let page = 1; page <= Math.min(cap, totalPages); page++) {
      const url = `${ORIGIN}${API_PATH}?page=${page}&limit=${PAGE_SIZE}&${ALL_WORK_MODES}`;
      assertRemotliUrl(url);

      /** @type {any[]} */
      let rows;
      try {
        // redirect:'error' prevents SSRF via server-side redirects; combined with
        // assertRemotliUrl this pins every hop to remotli.ch.
        const data = await ctx.fetchJson(url, { redirect: 'error' });

        if (!data || typeof data !== 'object' || !Array.isArray(/** @type {any} */ (data).jobs)) {
          throw new Error(
            `remotli: unexpected API response — expected { jobs: [...] }, got ${data === null ? 'null' : typeof data}`,
          );
        }

        rows = /** @type {any} */ (data).jobs;

        const reported = Number(/** @type {any} */ (data).pagination?.totalPages);
        if (Number.isInteger(reported) && reported > 0) totalPages = reported;
      } catch (err) {
        if (!succeededOnce) throw err;
        break; // keep the pages already collected — a mid-scan blip isn't a dead board
      }

      // Set only after the shape check passed, so a malformed body never counts
      // as proof of life.
      succeededOnce = true;

      for (const row of rows) {

View on GitHub (pinned to aac998c7ed)