santifer/career-ops · error · Error

remotive: unexpected API response — expected

Error message

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

What it means

Remotive's public feed must return a JSON object with a `jobs` array ({ jobs: [...] }). fetch() throws this error when the response is null, not an object, or lacks a jobs array. The message includes the observed top-level keys (or 'null') to aid debugging.

Solutions

  1. Retry after a delay; transient error bodies are common
  2. Fetch the Remotive feed URL manually and inspect the top-level keys shown in the error message
  3. Check Remotive API docs/changelog for an envelope change (e.g. jobs renamed or nested under data) and update the provider parser
  4. Verify no proxy/middleware is rewriting the response body

Example fix

null
Defensive patterns

Strategy: try-catch

Validate before calling

null

Type guard

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

Try / catch

try {
  const jobs = await remotive.fetch(entry, ctx);
} catch (err) {
  if (err.message.startsWith('remotive: unexpected API response')) {
    console.warn(`Remotive envelope changed or errored (${err.message}) — check feed manually`);
    return [];
  }
  throw err;
}

Prevention

When it happens

Trigger: The Remotive API returns null/empty body, a bare array, a JSON error object without `jobs`, or changes its envelope; rate limiting or CDN error pages that still parse as JSON.

Common situations: Remotive deploys an API version change moving `jobs` under another key; a proxy returns a JSON error payload; transient outage where the feed returns an error object.

Understand the failure class

Background: "invalid response format", "malformed payload", "missing data field": when an API returns 200 but the response shape is wrong — this error's family across 23 libraries.

Related errors


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

Appendix: source

Thrown at providers/remotive.mjs:28

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

const FEED_URL = 'https://remotive.com/api/remote-jobs';

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

  /**
   * Fetches and normalizes postings from the Remotive 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 json = await ctx.fetchJson(FEED_URL, { redirect: 'error' });
    if (!json || !Array.isArray(json.jobs)) {
      throw new Error(`remotive: unexpected API response — expected { jobs: [...] }, got keys: [${json ? Object.keys(json).join(', ') : 'null'}]`);
    }

    return json.jobs
      .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 || 'Remotive'),
        location: typeof j.candidate_required_location === 'string' ? j.candidate_required_location.trim() : '',
      }));
  },
};

View on GitHub (pinned to aac998c7ed)