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

The Manfred fetch() calls the feed API and expects the JSON body to be an array of offer objects. If the parsed response is not an array (an object, string, number, or null), fetch() throws this error describing what type actually arrived. It guards against the API contract changing or an error/proxy page being returned with 200.

Solutions

  1. Log the actual response body at the failing URL to see what shape came back.
  2. Check whether the Manfred feed API changed shape and update the provider to unwrap the array (e.g. json.offers).
  3. Re-verify the feed URL is the offers-feed endpoint and not an error/docs endpoint.
  4. If a bot-protection layer is intercepting, retry from a normal network or adjust request headers via the fetch context.

Example fix

// before
if (!Array.isArray(json)) throw new Error(...);
const out = [];
for (const offer of json) {...}
// after (if API now wraps the array)
const offers = Array.isArray(json) ? json : Array.isArray(json?.offers) ? json.offers : null;
if (!offers) throw new Error('manfred: unexpected API response shape');
Defensive patterns

Strategy: try-catch

Type guard

const isOfferArray = (v) => Array.isArray(v) && v.every((o) => o && typeof o === 'object');

Try / catch

try {
  const jobs = await provider.fetch(entry, ctx);
} catch (err) {
  if (String(err.message).startsWith('manfred: unexpected API response')) {
    const body = await fetch(feedUrl).then((r) => r.text());
    console.error('Feed returned non-array body:', body.slice(0, 300));
    return [];
  }
  throw err;
}

Prevention

When it happens

Trigger: fetchJson resolves with valid JSON that is not an array: the API returned an error envelope like {"error":...}, a WAF/HTML-to-JSON gateway response, an empty object, or a literal null body.

Common situations: Manfred changes its feed response shape (wraps offers in {offers:[...]}); a captive portal or bot-protection layer intercepts the request and returns JSON that isn't the feed; the feed URL is valid but points at a non-feed endpoint.

Related errors


AI-assisted analysis of santifer/career-ops@e7abd431fc (2026-09-22). Data as JSON: /api/errors/0687c18bf9826a18. Report an issue: GitHub.

Appendix: source

Thrown at providers/manfred.mjs:210

/** @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 fetchJsonWithRetry(ctx, url, { redirect: 'error', timeoutMs: FETCH_TIMEOUT_MS })
    );
    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 e7abd431fc)