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
- Log the full response body and keys listed in the error to see what the API actually returned
- Re-check the API endpoint URL and query parameters against NoFluffJobs' current public API
- Check for rate limiting / WAF interception (429 or challenge pages) and add backoff or headers like a browser User-Agent
- If the API contract changed, update parseNoFluffJobsResponse to the new field name/shape
- 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
- Check res.ok and content-type before parsing JSON
- Log raw response bodies on parse failure to spot WAF/captcha/error pages quickly
- Pin and document the API endpoint version; re-verify the shape after vendor changes
- Add exponential backoff for 429/5xx responses before giving up
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
- API error: code=
- Apify did not return a run id
- gmail: failed to fetch message
- himalayas: unexpected API response - expected
- HTTP
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)