santifer/career-ops · error · Error

echojobs: unexpected API response on page ${page} — expected

Error message

echojobs: unexpected API response on page ${page} — expected { jobs: [...] }, got keys: [${json ? Object.keys(json).join(', ') : 'null'}]

What it means

The echojobs feed response did not match the expected { jobs: [...] } shape: either the JSON was null/non-object, or json.jobs was absent/not an array. This is a real runtime error raised after a successful fetch, signalling the upstream API changed shape, returned an error body, or served a non-JSON page. The message includes the actual top-level keys (or 'null') to aid diagnosis.

Source

Thrown at providers/echojobs.mjs:153

  id: 'echojobs',

  detect(entry) {
    return entry?.provider === 'echojobs' ? { url: FEED_BASE } : null;
  },

  async fetch(entry, ctx) {
    const maxPages = resolveMaxPages(entry);
    const fallbackCompany = entry?.name;
    const out = [];

    for (let page = 1; page <= maxPages; page++) {
      // Validate the URL actually fetched (not just a constant) so the host pin
      // is meaningful, then redirect:'error' blocks SSRF via server-side
      // redirects — together they keep every page request on echojobs.io.
      const url = assertEchojobsUrl(`${FEED_BASE}?per_page=${PER_PAGE}&page=${page}`);
      const json = /** @type {any} */ (await ctx.fetchJson(url, { redirect: 'error' }));
      if (!json || !Array.isArray(json.jobs)) {
        throw new Error(
          `echojobs: unexpected API response on page ${page} — expected { jobs: [...] }, got keys: [${json ? Object.keys(json).join(', ') : 'null'}]`,
        );
      }
      for (const j of json.jobs) {
        const normalized = normalizeEchojobsJob(j, fallbackCompany);
        if (normalized) out.push(normalized);
      }
      if (json.jobs.length < PER_PAGE) break; // short page → last page reached
    }
    return out;
  },
};

View on GitHub (pinned to 9b17a8ac97)

Solutions

  1. Log the actual response keys/body shown in the message to identify whether it is an error envelope, a rename, or a non-JSON page.
  2. If the API renamed the field, update the Array.isArray(json.jobs) check in providers/echojobs.mjs to the new key.
  3. If it is a transient error/rate-limit body, retry after a backoff or skip the run; treat a persistent shape change as a provider bug to fix.
  4. Confirm the endpoint is still https://echojobs.io/api/jobs and that no proxy is rewriting the response.

Example fix

// before — expects the documented envelope
if (!json || !Array.isArray(json.jobs)) { throw ... }

// after — API renamed 'jobs' to 'results'
if (!json || !Array.isArray(json.results)) { throw ... }
// (and iterate json.results below)
Defensive patterns

Strategy: try-catch

Validate before calling

// Cannot validate a remote response before fetching it. Best-effort shape probe after fetch:
async function probeEchojobsShape(ctx, url) {
  const json = await ctx.fetchJson(url, { redirect: 'error' });
  return json && Array.isArray(json.jobs) ? json : null;
}

Type guard

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

Try / catch

try { await echojobs.fetch(entry, ctx); }
catch (e) {
  if (/^echojobs: unexpected API response/.test(e.message)) {
    // upstream changed shape or returned an error body — inspect the keys in the message,
    // retry once after backoff for transient rate-limit bodies, then skip/disable if persistent
  } else throw e;
}

Prevention

When it happens

Trigger: ctx.fetchJson returned null; returned an error object like { error: '...' } or { message: '...' } (e.g. rate-limit 429 body, auth failure, maintenance); the API renamed the 'jobs' field; an HTML error page was parsed as an unexpected object.

Common situations: EchoJobs deploys a breaking API change (field rename, new envelope); a rate-limit or outage returns an error body with a different shape; an intermediate proxy (corporate firewall, CDN) returns a block page that parses as JSON-ish; a regional API variant with a different envelope.

Related errors


AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13). Data as JSON: /api/errors/88b5b0f0c1971b24. Report an issue: GitHub.