santifer/career-ops · error · Error
justjoin: unexpected API response — expected
Error message
justjoin: unexpected API response — expected { data: [...] }, got keys: [${json ? Object.keys(json).join(', ') : 'null'}] What it means
Thrown by parseJustJoinResponse when the JSON body from the justjoin.it API does not contain a `data` array — i.e. the response shape is not { data: [...] }. The error message includes the actual top-level keys (or 'null' for a non-object body) to aid debugging. This is a contract check protecting the mapper that iterates json.data, guarding against API changes, error payloads, or HTML/error pages parsed as JSON.
Solutions
- Log/inspect the actual response body and its keys shown in the message to see what the API returned
- Check whether the API returned an error/rate-limit payload (status code, Cloudflare challenge) and retry with backoff
- Update the provider's parsing to the current JustJoin API envelope if the shape legitimately changed
- Verify the request URL — use the default API_BASE rather than a hand-built entry.api URL with wrong parameters
Example fix
// before — passing a raw response straight to the mapper
const json = await res.json();
const jobs = json.data.map(toJob);
// after — guard the shape first
const json = await res.json();
if (!json || !Array.isArray(json.data)) {
throw new Error(`justjoin: unexpected API response, keys: ${json ? Object.keys(json) : 'null'}`);
}
const jobs = json.data.map(toJob); Defensive patterns
Strategy: try-catch
Validate before calling
function looksLikeOffersEnvelope(json) {
return Boolean(json) && typeof json === 'object' && Array.isArray(json.data);
}
if (!looksLikeOffersEnvelope(json)) throw new Error('unexpected justjoin response shape'); Type guard
const isOffersEnvelope = (j) => Boolean(j) && typeof j === 'object' && Array.isArray(j.data);
Try / catch
try {
const parsed = parseJustJoinResponse(json);
} catch (e) {
if (e.message.includes('unexpected API response')) {
console.error('JustJoin API envelope changed or returned an error payload; keys were:',
json && typeof json === 'object' ? Object.keys(json) : json);
// inspect body, back off / retry, or update parser
} else throw e;
} Prevention
- Log the raw response body (and HTTP status) whenever the shape check fails
- Watch for JustJoin API announcements; pin/verify the envelope after provider updates
- Treat a non-{data:[...]} body as a signal of WAF/rate-limit/error pages, not just a parser bug
When it happens
Trigger: ctx.fetchJson returns: null/undefined (empty body), an object without `data` (e.g. { error: '...', message: '...' }), an array at top level, or a JSON body whose `data` is an object/string instead of an array. Triggered on the first page of fetch() at providers/justjoin.mjs:118, or by any direct call to the exported parseJustJoinResponse.
Common situations: JustJoin changes or versions its candidate API (response envelope renamed); a Cloudflare/WAF block returns an HTML error page or JSON error object; rate limiting returns { errors: [...] }; the endpoint was hit with wrong query params producing an error envelope; a proxy returns an empty 200 body.
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
- 4dayweek: unexpected API response on page
- a16z-speedrun-talent: unexpected API response on page
- agentic-jobs: unexpected API response shape on page
- All active models failed. Last error
- arbeitnow: unexpected API response on page
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/98c70ef33c6b4bc2.
Report an issue: GitHub.
Appendix: source
Thrown at providers/justjoin.mjs:82
const parsed = assertJustJoinUrl(apiUrl);
if (parsed.pathname !== '/api/candidate-api/offers') {
parsed.pathname = '/api/candidate-api/offers';
parsed.search = '';
}
parsed.searchParams.set('from', String(from));
parsed.searchParams.set('itemsCount', String(Number(entry.page_size || PAGE_SIZE)));
parsed.searchParams.set('cityRadius', String(Number(entry.city_radius || 30)));
parsed.searchParams.set('currency', String(entry.currency || 'pln').toLowerCase());
parsed.searchParams.set('orderBy', 'descending');
parsed.searchParams.set('sortBy', 'publishedAt');
parsed.searchParams.set('keywordType', 'any');
parsed.searchParams.set('isPromoted', 'true');
return parsed.href;
}
export function parseJustJoinResponse(json) {
if (!json || !Array.isArray(json.data)) {
throw new Error(`justjoin: unexpected API response — expected { data: [...] }, got keys: [${json ? Object.keys(json).join(', ') : 'null'}]`);
}
return json.data
.filter(offer => offer && typeof offer === 'object')
.map(offer => {
const slug = String(offer.slug || '').trim();
const title = String(offer.title || '').trim();
if (!slug || !title) return null;
return {
title,
url: `${JOB_BASE}${slug}`,
company: String(offer.companyName || '').trim(),
location: normalizeLocation(offer),
postedAt: postedAtMillis(offer.publishedAt),
};
})
.filter(Boolean);
}
View on GitHub (pinned to aac998c7ed)