santifer/career-ops · error · Error

remoteok: unexpected API response — expected a JSON array, g

Error message

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

What it means

The remoteok provider fetches a single public feed (FEED_URL) and asserts the response is a JSON array. If ctx.fetchJson resolves to anything that is not an array — null, an object, a string, a number — it throws. This guards against partial outages, CDN error pages served as JSON, or upstream API contract changes that wrap the list in an envelope.

Source

Thrown at providers/remoteok.mjs:29

// private scanning, but don't redistribute this feed publicly without it.

const FEED_URL = 'https://remoteok.com/api';

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

  /**
   * Fetches and normalizes postings from the RemoteOK 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(`remoteok: unexpected API response — expected a JSON array, got ${data === null ? 'null' : typeof data}`);
    }

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

View on GitHub (pinned to 9b17a8ac97)

Solutions

  1. Retry after a short delay — transient rate-limit or maintenance responses are often intermittent.
  2. Inspect the raw response: curl the FEED_URL directly to see the current shape.
  3. If RemoteOK permanently changed its format, update the parser to unwrap the new envelope before the Array.isArray check.
  4. Verify network egress is not being intercepted by a corporate proxy returning a JSON block page.

Example fix

// before
const data = await ctx.fetchJson(FEED_URL, { redirect: 'error' });
if (!Array.isArray(data)) throw new Error(...);
// after — tolerate a wrapped envelope
const data = await ctx.fetchJson(FEED_URL, { redirect: 'error' });
const list = Array.isArray(data) ? data : (Array.isArray(data?.jobs) ? data.jobs : null);
if (!list) throw new Error('remoteok: unexpected API response');
Defensive patterns

Strategy: type-guard

Validate before calling

// Probe the feed shape before relying on the provider's strict assertion
async function probeRemoteOk(ctx) {
  const data = await ctx.fetchJson(FEED_URL, { redirect: 'error' });
  return Array.isArray(data);
}
if (!(await probeRemoteOk(ctx))) {
  console.warn('remoteok: feed shape changed or endpoint down — skipping');
}

Type guard

/** @param {unknown} d */
function isRemoteOkFeed(d) {
  return Array.isArray(d) && d.every(j => j == null || (typeof j === 'object' && typeof j.position === 'string'));
}

Try / catch

try {
  await provider.fetch(entry, ctx);
} catch (e) {
  if (/unexpected API response/.test(e.message)) {
    // likely transient outage or rate limit — retry once after delay
    await new Promise(r => setTimeout(r, 5000));
    await provider.fetch(entry, ctx);
  } else throw e;
}

Prevention

When it happens

Trigger: The feed returns an error envelope like { error: '...' } or { data: [...] } instead of a bare array; the endpoint returns null; a rate-limit or maintenance page is parsed as a JSON object; the upstream changed its response shape.

Common situations: RemoteOK is temporarily rate-limiting and returns an error object; a proxy/gateway injected a wrapping object; the FEED_URL constant points at a stale or version-changed endpoint after an API revision.

Related errors


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