santifer/career-ops · error · Error

nofluffjobs: unexpected API response — expected

Error message

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

What it means

parseNoFluffJobsResponse validates the shape of the JSON returned by the NoFluffJobs API. It requires an object with a 'postings' array; anything else (null body, an error object, an HTML error page parsed accidentally, a changed API contract) throws this error listing the top-level keys actually received.

Solutions

  1. Log the full response body and keys listed in the error to see what the API actually returned
  2. Re-check the API endpoint URL and query parameters against NoFluffJobs' current public API
  3. Check for rate limiting / WAF interception (429 or challenge pages) and add backoff or headers like a browser User-Agent
  4. If the API contract changed, update parseNoFluffJobsResponse to the new field name/shape
  5. Retry later if the API was temporarily degraded

Example fix

// before
const json = await res.json();
return parseNoFluffJobsResponse(json);
// after
const json = await res.json();
if (!res.ok) throw new Error(`nofluffjobs: HTTP ${res.status}: ${JSON.stringify(json).slice(0, 200)}`);
return parseNoFluffJobsResponse(json);
Defensive patterns

Strategy: try-catch

Validate before calling

const res = await fetch(apiUrl);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const ct = res.headers.get('content-type') || '';
if (!ct.includes('application/json')) throw new Error(`non-JSON response: ${ct}`);

Type guard

function isPostingsPayload(v) {
  return v !== null && typeof v === 'object' && !Array.isArray(v) && Array.isArray(v.postings);
}

Try / catch

try {
  return parseNoFluffJobsResponse(json);
} catch (e) {
  if (String(e.message).includes('unexpected API response')) {
    log.error(`API shape changed or blocked; body keys: ${e.message}`);
    return []; // degrade to empty scan instead of crashing the run
  }
  throw e;
}

Prevention

When it happens

Trigger: The API returned 200 with a JSON body lacking a postings array — e.g. { error: ... }, an auth-challenge payload, a rate-limit notice, a paginated envelope under a different key, or a null/empty body.

Common situations: NoFluffJobs changed or versioned their public API response shape; a proxy/WAF returned a JSON error page; the request succeeded but hit a maintenance/captcha endpoint; the caller fetched the wrong endpoint URL.

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/3df3f9dcb12d8a07. Report an issue: GitHub.

Appendix: source

Thrown at providers/nofluffjobs.mjs:109

          applicationStatus: [],
          province: [],
          company: [],
          id: [],
          category: [],
          keyword: [],
          jobLanguage: [],
          seniority: [],
        },
        pageSize: Number(entry.page_size || PAGE_SIZE),
        withSalaryMatch: true,
      };

  return { url: apiUrl.href, body };
}

export function parseNoFluffJobsResponse(json) {
  if (!json || !Array.isArray(json.postings)) {
    throw new Error(`nofluffjobs: unexpected API response — expected { postings: [...] }, got keys: [${json ? Object.keys(json).join(', ') : 'null'}]`);
  }
  return json.postings
    .filter(posting => posting && typeof posting === 'object')
    .map(posting => {
      const title = String(posting.title || '').trim();
      const company = String(posting.name || '').trim();
      const slug = String(posting.url || posting.id || '').trim();
      if (!title || !slug) return null;
      return {
        title,
        url: `${JOB_BASE}${slug}`,
        company,
        location: normalizeLocation(posting),
        postedAt: postedAtMillis(posting.posted),
      };
    })
    .filter(Boolean);
}

View on GitHub (pinned to aac998c7ed)