santifer/career-ops · error · Error

workingnomads: unexpected API response — expected a JSON arr

Error message

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

What it means

Working Nomads' feed is expected to be a top-level JSON array. This throws when ctx.fetchJson returned something that is not an array — an object (error envelope), null, or a primitive — surfacing the actual type in the message.

Source

Thrown at providers/workingnomads.mjs:26

// Wire in via a `job_boards:` entry with `provider: workingnomads`.

const FEED_URL = 'https://www.workingnomads.com/api/exposed_jobs/';

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

  /**
   * Fetches and normalizes postings from the Working Nomads public feed.
   * @param {{ name?: string }} entry - The job_boards entry being processed.
   * @param {{ fetchJson: (url: string, opts?: { redirect?: 'error'|'follow'|'manual' }) => Promise<any> }} ctx - HTTP context.
   * @returns {Promise<Array<{title: string, url: string, company: string, location: string}>>}
   */
  async fetch(entry, ctx) {
    // redirect:'error' prevents SSRF via server-side redirects
    const data = await ctx.fetchJson(FEED_URL, { redirect: 'error' });
    if (!Array.isArray(data)) {
      throw new Error(`workingnomads: unexpected API response — expected a JSON array, got ${data === null ? 'null' : typeof data}`);
    }

    return data
      .filter(j => j && typeof j === 'object'
        && typeof j.title === 'string' && j.title.trim() !== ''
        && typeof j.url === 'string' && /^https?:\/\//i.test(j.url.trim()))
      .map(j => ({
        title: j.title.trim(),
        url: j.url.trim(),
        company: typeof j.company_name === 'string' && j.company_name.trim() ? j.company_name.trim() : (entry.name || 'Working Nomads'),
        location: typeof j.location === 'string' ? j.location.trim() : '',
      }));
  },
};

View on GitHub (pinned to 9b17a8ac97)

Solutions

  1. Fetch the URL directly to see the current shape: curl -s https://www.workingnomads.com/api/exposed_jobs/ | head -c 500.
  2. If the shape changed permanently, update workingnomads.mjs parsing to match the new contract (e.g. unwrap a {jobs: [...]} envelope).
  3. If it is an error envelope, retry later — likely transient.
  4. If the feed moved, update FEED_URL.

Example fix

// before
const data = await ctx.fetchJson(FEED_URL, { redirect: "error" });
if (!Array.isArray(data)) throw new Error(`workingnomads: unexpected API response ...`);
// after — tolerate a wrapped envelope if Working Nomads changes shape
const data = await ctx.fetchJson(FEED_URL, { redirect: "error" });
const rows = Array.isArray(data) ? data : Array.isArray(data?.jobs) ? data.jobs : null;
if (!rows) throw new Error(`workingnomads: unexpected API response — got ${data === null ? "null" : typeof data}`);
Defensive patterns

Strategy: try-catch

Validate before calling

// probe the feed shape out-of-band before relying on it
const probe = await fetch('https://www.workingnomads.com/api/exposed_jobs/').then(r => r.json());
if (!Array.isArray(probe)) console.warn("workingnomads feed shape changed:", typeof probe);

Type guard

const isJobArray = (d) => Array.isArray(d) &&
  d.every(j => j && typeof j === "object" && typeof j.title === "string" && typeof j.url === "string");

Try / catch

try {
  jobs = await workingnomads.fetch(entry, ctx);
} catch (err) {
  if (/workingnomads: unexpected API response/.test(err.message)) {
    logApiContractChange("workingnomads", err.message);
    continue; // skip this provider, keep scanning others
  }
  throw err;
}

Prevention

When it happens

Trigger: The endpoint https://www.workingnomads.com/api/exposed_jobs/ returned a JSON object instead of an array (contract change or error body), returned null, or an upstream proxy returned a JSON error envelope. A zero-length array [ ] does NOT trigger this — only a non-array top-level value does.

Common situations: Working Nomads changed their API shape; the endpoint is temporarily returning an error object {error: ...}; a CDN returned a JSON-formatted error; the feed URL moved.

Related errors


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