santifer/career-ops · error · Error

landingjobs: unexpected API response — expected a JSON array

Error message

landingjobs: unexpected API response — expected a JSON array, got ${json === null ? 'null' : typeof json}

What it means

The LandingJobs provider fetches a JSON feed (FEED_URL) and requires the top-level value to be an array of job postings. This error fires after fetchJson succeeds (HTTP response parsed as JSON) but the result is not an Array — it reports the actual type received (or 'null'). It guards the downstream .map().filter() chain, which would otherwise throw a less informative TypeError.

Source

Thrown at providers/landingjobs.mjs:125

  const location = [base, j.remote === true ? 'Remote' : ''].filter(Boolean).join(', ');

  /** @type {{ title: string, url: string, company: string, location: string, postedAt?: number }} */
  const job = { title, url, company, location };
  const postedAt = toEpochMs(j.published_at) ?? toEpochMs(j.created_at);
  if (postedAt !== undefined) job.postedAt = postedAt;
  return job;
}

/** @type {Provider} */
export default {
  id: 'landingjobs',

  async fetch(entry, ctx) {
    assertLandingUrl(FEED_URL);
    // redirect:'error' prevents SSRF via server-side redirects
    const json = await ctx.fetchJson(FEED_URL, { redirect: 'error' });
    if (!Array.isArray(json)) {
      throw new Error(
        `landingjobs: unexpected API response — expected a JSON array, got ${json === null ? 'null' : typeof json}`,
      );
    }
    const fallbackCompany = entry?.name;
    return json.map(j => normalizeLandingJob(j, fallbackCompany)).filter(Boolean);
  },
};

View on GitHub (pinned to 9b17a8ac97)

Solutions

  1. Log the received value (console.log(json) before the throw) to see the actual shape LandingJobs returned.
  2. If the API now wraps jobs in an envelope, unwrap it: replace the Array.isArray check with `const arr = Array.isArray(json) ? json : json?.jobs || json?.data; if(!Array.isArray(arr)) throw ...`.
  3. Verify FEED_URL still points at the documented feed endpoint by curl-ing it directly.
  4. If null/object corresponds to a known rate-limit or maintenance response, surface it as a distinct, retriable error instead of a hard failure.

Example fix

// before
if (!Array.isArray(json)) {
  throw new Error(`landingjobs: unexpected API response — expected a JSON array, got ${json === null ? 'null' : typeof json}`);
}
return json.map(j => normalizeLandingJob(j, fallbackCompany)).filter(Boolean);

// after — tolerate a common envelope shape
const arr = Array.isArray(json) ? json : (json && (Array.isArray(json.jobs) ? json.jobs : Array.isArray(json.data) ? json.data : null));
if (!Array.isArray(arr)) {
  throw new Error(`landingjobs: unexpected API response — expected a JSON array or {jobs|data:[]}, got ${json === null ? 'null' : typeof json}`);
}
return arr.map(j => normalizeLandingJob(j, fallbackCompany)).filter(Boolean);
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-check the response shape before relying on the provider's own guard.
// Useful when you call ctx.fetchJson directly in a custom integration.
function isLandingJobsFeed(value) {
  return Array.isArray(value);
}
// const json = await ctx.fetchJson(url, { redirect: 'error' });
// if (!isLandingJobsFeed(json)) { /* log + skip, or unwrap envelope */ }

Type guard

/** @param {unknown} v */
function isJobArray(v) {
  return Array.isArray(v) && v.every(item => item && typeof item === 'object');
}

Try / catch

// At the scan/orchestrator level, isolate each provider so one bad
// response shape never aborts the whole sweep.
try {
  const jobs = await provider.fetch(entry, ctx);
  results.push(...jobs);
} catch (err) {
  console.error(`[skip] ${provider.id} (${entry.name}): ${err.message}`);
  // continue with the next provider — do not rethrow for shape mismatches
}

Prevention

When it happens

Trigger: ctx.fetchJson(FEED_URL) returns a JSON object (e.g. an envelope like {data:[...]} or {jobs:[...]}), returns null, returns a single job object, or returns a maintenance/error payload such as {error:'rate limited'}. The template literal embeds json===null?'null':typeof json so the message distinguishes null from object/string.

Common situations: LandingJobs changes its feed response shape (wraps results in an envelope); a temporary outage returns a JSON error body instead of the feed; the FEED_URL constant was edited to point at a non-feed endpoint; a proxy/CDN injects a JSON status object.

Related errors


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