santifer/career-ops · error · Error
ibm: unexpected API response — expected hits.hits[], got…
Error message
ibm: unexpected API response — expected hits.hits[], got keys: [${json ? Object.keys(json).join(', ') : 'null'}] What it means
parseIbmResponse expects IBM's careers API (Elasticsearch-style) envelope: json.hits.hits must be an array. If the response is null, missing `hits`, or hits.hits is not an array, it throws with the top-level keys of the received object for diagnosis.
Solutions
- Call the IBM search URL manually and inspect the returned JSON keys to confirm the envelope changed
- Update parseIbmResponse to the new envelope (e.g. json.hits.hits -> json.results / json.documents)
- Check for an upstream error/rate-limit payload in the printed keys and fix the request (headers, page params)
- Skip the IBM board and report the provider breakage until the parser is updated
Example fix
// before const hits = json && json.hits && Array.isArray(json.hits.hits) ? json.hits.hits : null; // after const hits = (Array.isArray(json?.hits?.hits) && json.hits.hits) || (Array.isArray(json?.results) && json.results) || null;
Defensive patterns
Strategy: type-guard
Validate before calling
function looksLikeIbmEnvelope(json) {
return !!json && typeof json === 'object' && Array.isArray(json?.hits?.hits);
}
// before parsing each page: if (!looksLikeIbmEnvelope(json)) handle gracefully; Type guard
const hasHits = (v) => typeof v === 'object' && v !== null && Array.isArray(v.hits?.hits);
Try / catch
try {
const jobs = parseIbmResponse(json);
} catch (err) {
if (/expected hits\.hits/.test(err.message)) {
console.error('IBM careers API envelope changed:', err.message);
return [];
}
throw err;
} Prevention
- Save a sample API response as a fixture and regression-test parseIbmResponse against it
- Handle non-200/status errors before JSON shape checks so error bodies don't masquerade as schema changes
- Log received keys on failure (the error already does) to speed triage
- Monitor the IBM careers endpoint for backend migrations
When it happens
Trigger: The IBM search API returns an error object ({ error: ... }), an empty body parsed as null, a rate-limit/auth JSON, or IBM migrates to a new API whose envelope no longer nests hits.hits[].
Common situations: IBM changes its careers search backend (Elasticsearch replaced/rewrapped); the request URL is stale and hits a redirect/error page returning JSON; a proxy strips the body; per-page fetch 2 returns an error object mid-pagination.
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
- himalayas: unexpected API response - expected
- jobicy: unexpected API response — expected
- landingjobs: unexpected API response — expected a JSON…
- No free models found in API response
- nofluffjobs: unexpected API response — expected
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/c2b1f81c2f10dc4b.
Report an issue: GitHub.
Appendix: source
Thrown at providers/ibm.mjs:54
if (categories.length) {
must.push({ bool: { should: categories.map(c => ({ term: { field_keyword_08: c } })) } });
}
const country = typeof cfg.country === 'string' ? cfg.country.trim() : '';
if (country) must.push({ term: { field_keyword_05: country } });
return { bool: { must } };
}
/**
* Normalizes one page of the IBM careers API response into job entries.
* Throws if the response doesn't carry the expected `hits.hits[]` shape, so a
* silent endpoint change surfaces as a hard error instead of empty results.
* @param {any} json - A single API response page.
* @returns {Array<{title: string, url: string, company: string, location: string}>}
*/
export function parseIbmResponse(json) {
const hits = json && json.hits && Array.isArray(json.hits.hits) ? json.hits.hits : null;
if (!hits) {
throw new Error(`ibm: unexpected API response — expected hits.hits[], got keys: [${json ? Object.keys(json).join(', ') : 'null'}]`);
}
const out = [];
for (const h of hits) {
const s = (h && h._source) || {};
if (typeof s.title !== 'string' || s.title.trim() === '') continue;
if (typeof s.url !== 'string' || !/^https?:\/\//i.test(s.url.trim())) continue;
const loc = typeof s.field_keyword_19 === 'string' ? s.field_keyword_19.trim() : '';
const mode = typeof s.field_keyword_17 === 'string' ? s.field_keyword_17.trim() : '';
out.push({
title: s.title.trim(),
url: s.url.trim(),
company: 'IBM',
location: [loc, mode].filter(Boolean).join(' · '),
});
}
return out;
}View on GitHub (pinned to aac998c7ed)