santifer/career-ops · error · Error

manfred: unexpected API response — expected a JSON array of

Error message

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

What it means

Thrown by the manfred provider's fetch() after ctx.fetchJson() returns successfully but the parsed body is not a JSON array. The Manfred job feed API (getmanfred.com) is contractually expected to return a top-level array of offer objects; any other shape (object, string, number, null) is treated as a contract break or upstream API change. This is a response-shape validator, not a network error — the HTTP request succeeded.

Source

Thrown at providers/manfred.mjs:196

  return job;
}

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

  detect(entry) {
    return entry?.provider === 'manfred' ? { url: buildFeedUrl(entry) } : null;
  },

  async fetch(entry, ctx) {
    // Validate the URL actually fetched (not just a constant) so the host pin
    // is meaningful, then redirect:'error' blocks SSRF via server-side
    // redirects — together they keep the request on getmanfred.com.
    const url = assertManfredUrl(buildFeedUrl(entry));
    const json = /** @type {any} */ (await ctx.fetchJson(url, { redirect: 'error' }));
    if (!Array.isArray(json)) {
      throw new Error(
        `manfred: unexpected API response — expected a JSON array of offers, got ${json === null ? 'null' : typeof json}`,
      );
    }
    const fallbackCompany = entry?.name;
    const out = [];
    for (const offer of json) {
      const normalized = normalizeManfredOffer(offer, fallbackCompany);
      if (normalized) out.push(normalized);
    }
    return out;
  },
};

View on GitHub (pinned to 9b17a8ac97)

Solutions

  1. Check what the endpoint actually returns now: curl -sL <feed-url> | head -c 500 — if it's an envelope like {data:[...]}, the provider needs updating to unwrap it.
  2. Verify the entry in portals.yml points to the correct Manfred feed URL (buildFeedUrl output) and not a stale or wrong endpoint.
  3. If Manfred changed their API shape, update normalizeManfordOffer and the Array.isArray check in providers/manfred.mjs:196 to unwrap the new envelope.
  4. If the response is a JSON error object with HTTP 200, add status-code awareness to ctx.fetchJson or check for known error keys before the array assertion.

Example fix

// before
const json = await ctx.fetchJson(url, { redirect: 'error' });
if (!Array.isArray(json)) {
  throw new Error(`manfred: unexpected API response ...`);
}

// after — tolerate a known envelope shape
const json = await ctx.fetchJson(url, { redirect: 'error' });
const offers = Array.isArray(json) ? json : (Array.isArray(json?.offers) ? json.offers : null);
if (!offers) {
  throw new Error(`manfred: unexpected API response — expected array or {offers:[]}, got ${json === null ? 'null' : typeof json}`);
}
Defensive patterns

Strategy: type-guard

Validate before calling

// Before calling the provider, verify the Manfred feed returns an array
// by probing the endpoint (or trust the provider's own guard and catch).
// Pre-flight check is impractical for a remote API — the type guard is
// the runtime check itself, best handled in a catch boundary.
if (process.env.MANFRED_STRICT === '1') {
  const probe = await fetch(feedUrl).then(r => r.json());
  if (!Array.isArray(probe)) {
    console.warn('manfred feed shape changed — expected array, got', typeof probe);
  }
}

Type guard

/** @param {unknown} json @returns {json is any[]} */
function isOfferArray(json) {
  return Array.isArray(json);
}

// usage in caller:
const json = await ctx.fetchJson(url, { redirect: 'error' });
if (!isOfferArray(json)) {
  // log and skip, or unwrap a known envelope
  return [];
}

Try / catch

try {
  const jobs = await manfredProvider.fetch(entry, ctx);
} catch (err) {
  if (String(err.message).startsWith('manfred: unexpected API response')) {
    // API contract drift — log for investigation, don't crash the batch
    console.error(`manfred API shape changed for ${entry.name}:`, err.message);
    continue; // skip this provider, keep scanning others
  }
  throw err; // re-throw unrelated errors
}

Prevention

When it happens

Trigger: The URL passed assertManfredUrl() and the fetch completed with redirect:'error', but the JSON body is not an array. Specific triggers: (1) Manfred changes their API to return a wrapper object like {offers:[...]} or {data:[...]}; (2) the endpoint returns an error object like {error:'rate limited', message:'...'} with HTTP 200; (3) an HTML error page is served with Content-Type: application/json and parsed as a string; (4) the feed URL points to a page that returns a single JSON object (e.g. a 301-moved JSON envelope if redirect handling changed).

Common situations: The most common real-world hit is an unannounced Manfred API version bump that wraps results in an envelope. Other situations: rate-limiting responses returned as JSON objects with status 200, or a misconfigured entry.api pointing to a non-feed Manfred endpoint (e.g. a single-offer detail endpoint). Transient upstream issues where a CDN returns a JSON error object instead of the feed also trigger this.

Related errors


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