santifer/career-ops · error · Error
thehub: unexpected API response on page ${page} — expected {
Error message
thehub: unexpected API response on page ${page} — expected { jobs: { docs: [...] } }, got keys: [${json ? Object.keys(json).join(', ') : 'null'}] What it means
fetchScope expects each page JSON to have shape { jobs: { docs: [...] } }. If json is null, json.jobs is missing, or json.jobs.docs is not an array, it throws and reports the actual top-level keys. On the first page (state.succeededOnce false) the throw propagates; on later pages the error is caught, logged, and the jobs collected so far are kept — so this error only surfaces when the very first requested page is malformed.
Source
Thrown at providers/thehub.mjs:137
*
* @param {string} query the query string beyond `?`, e.g. `countryCode=EU` or `isRemote=true`
* @param {number} maxPages
* @param {string | undefined} fallbackCompany
* @param {Map<string, {title: string, url: string, company: string, location: string}>} byUrl
* @param {{ fetchJson: (url: string, opts?: object) => Promise<any> }} ctx
* @param {{ succeededOnce: boolean }} state
* @returns {Promise<boolean>}
*/
async function fetchScope(query, maxPages, fallbackCompany, byUrl, ctx, state) {
for (let page = 1; page <= maxPages; page++) {
const url = `${FEED_BASE}?page=${page}&${query}`;
let jobs;
try {
// redirect:'error' prevents SSRF via server-side redirects
const json = await ctx.fetchJson(url, { redirect: 'error' });
jobs = json && json.jobs;
if (!jobs || !Array.isArray(jobs.docs)) {
throw new Error(
`thehub: unexpected API response on page ${page} — expected { jobs: { docs: [...] } }, got keys: [${json ? Object.keys(json).join(', ') : 'null'}]`,
);
}
} catch (err) {
if (!state.succeededOnce) throw err;
console.error(` ⚠ thehub: query "${query}" page ${page} failed (${err.message}) — keeping the ${byUrl.size} jobs collected so far`);
return false;
}
state.succeededOnce = true;
for (const j of jobs.docs) {
const normalized = normalizeHubJob(j, fallbackCompany);
if (normalized && !byUrl.has(normalized.url)) byUrl.set(normalized.url, normalized);
}
// Stop at the last page: a short page, or page >= the reported total pages.
if (jobs.docs.length < PER_PAGE) break;
if (Number.isInteger(jobs.pages) && page >= jobs.pages) break;
}
return true;View on GitHub (pinned to 9b17a8ac97)
Solutions
- Open the FEED_BASE URL (?page=1&<query>) in a browser and compare the actual keys to the error
- If the API is temporarily down, retry the scan later
- Verify the countryCode / query params in the thehub: block are values the API accepts
- If the contract changed, update normalizeHubJob and the docs-array check in providers/thehub.mjs
Defensive patterns
Strategy: try-catch
Type guard
/** @param {unknown} json @returns {json is { jobs: { docs: any[] } }} */
function isHubResponse(json) {
return !!json && typeof json === 'object'
&& !!json.jobs && typeof json.jobs === 'object'
&& Array.isArray(json.jobs.docs);
} Try / catch
try {
const jobs = await provider.fetch(entry, ctx);
} catch (err) {
if (/thehub: unexpected API response/.test(err.message)) {
console.warn(`${entry.name}: The Hub API shape changed or is down — skipping`);
return [];
}
throw err;
} Prevention
- Rely on the built-in partial-success behavior: later-page failures already degrade gracefully.
- Monitor the The Hub API contract after version bumps.
- Validate countryCode / query params against what the API accepts.
When it happens
Trigger: The Hub API (thehub.io/api/v2/jobsandfeatured) returned an error envelope, an HTML page, or a redesigned shape on page 1. A transient outage on the first page also throws. Later-page failures degrade gracefully instead.
Common situations: API contract change after a Hub upgrade, rate-limiting/CAPTCHA on the first request, an invalid countryCode scoping the query to an empty/bad response, or network returning a non-JSON body.
Related errors
- echojobs: unexpected API response on page ${page} — expected
- solidjobs: unexpected API response — expected { jobs: [...]
- themuse: unexpected API response on page ${page} — expected
- landingjobs: unexpected API response — expected a JSON array
- local parser JSON must be an array or contain jobs[]/results
AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13).
Data as JSON: /api/errors/cbeb0a7c4681d0f1.
Report an issue: GitHub.