santifer/career-ops · error · Error

workingnomads: unexpected API response — expected a JSON…

Error message

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

What it means

The workingnomads provider fetches its fixed FEED_URL and requires the response to be a JSON array of jobs. When ctx.fetchJson returns null or any non-array value, fetch throws, treating the response as an unexpected API shape rather than silently returning zero results.

Solutions

  1. Retry the fetch; transient API errors often resolve on the next run
  2. Open FEED_URL directly and inspect the JSON shape for an API format change
  3. If the payload is now wrapped (e.g. {jobs:[...]}), update the provider to unwrap the array
  4. Check for rate limiting or auth requirements introduced by the API

Example fix

// before
const data = await ctx.fetchJson(FEED_URL, { redirect: 'error' });
// after (unwrap if API changed)
let data = await ctx.fetchJson(FEED_URL, { redirect: 'error' });
if (data && !Array.isArray(data) && Array.isArray(data.jobs)) data = data.jobs;
Defensive patterns

Strategy: try-catch

Validate before calling

const data = await ctx.fetchJson(FEED_URL, { redirect: 'error' }).catch(() => null);
if (!Array.isArray(data)) throw new Error('workingnomads feed not an array; skipping run');

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 provider.fetch(entry, ctx); } catch (e) { if (e.message.includes('unexpected API response')) { console.warn('Working Nomads API shape changed or is down; retrying next run'); jobs = []; } else throw e; }

Prevention

When it happens

Trigger: fetch(entry, ctx) where the Working Nomads API returns null (fetch failure surfaced as null), an error object, or a wrapped payload like { jobs: [...] } instead of a top-level array.

Common situations: API downtime returning an HTML/JSON error page parsed to null; an API version change that wraps the array in an object; a proxy or rate-limiter returning a non-array body.

Related errors


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

Appendix: 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 aac998c7ed)