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

  1. Call the IBM search URL manually and inspect the returned JSON keys to confirm the envelope changed
  2. Update parseIbmResponse to the new envelope (e.g. json.hits.hits -> json.results / json.documents)
  3. Check for an upstream error/rate-limit payload in the printed keys and fix the request (headers, page params)
  4. 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

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


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)