santifer/career-ops · error
garena: unexpected API response — expected jobs[], got keys
Error message
garena: unexpected API response — expected jobs[], got keys: [${json ? Object.keys(json).join(', ') : 'null'}] What it means
parseGarenaResponse in providers/garena.mjs expects the Garena jobs API JSON to contain a jobs array at the top level (json.jobs). If it is absent or not an array, the function throws with the actual top-level keys listed for diagnosis. This is a strict contract check that fails fast instead of silently producing zero jobs.
Solutions
- Inspect the 'got keys' in the message: 'data' suggests a new envelope — unwrap it; 'message'/'error' suggests an API error to address.
- Check whether the Garena careers API moved or restructured; update parseGarenaResponse to the new schema.
- Verify the resolved office/URL parameters (resolveOffice + urlSegment) produce a valid endpoint that returns the jobs list.
- Log the raw response body once to confirm whether you are receiving the jobs API at all versus an HTML/interstitial response.
Example fix
// before const jobs = json && Array.isArray(json.jobs) ? json.jobs : null; // after const jobs = Array.isArray(json?.jobs) ? json.jobs : Array.isArray(json?.data?.jobs) ? json.data.jobs : null;
Defensive patterns
Strategy: type-guard
Validate before calling
function hasJobsArray(json) {
return !!json && typeof json === 'object' && Array.isArray(json.jobs);
} Type guard
const isGarenaJobsResponse = (json) => typeof json === 'object' && json !== null && Array.isArray(json.jobs);
Try / catch
try {
const jobs = parseGarenaResponse(json, entry);
} catch (e) {
if (e.message.includes('unexpected API response')) {
console.error('Garena schema drift or API error; keys:', Object.keys(json ?? {}).join(','));
return [];
}
throw e;
} Prevention
- Snapshot-test parseGarenaResponse against a captured real response so schema drift is caught early.
- Check the provider's developer announcements for API envelope changes before upgrading.
- Treat zero-result scans from one provider as a signal to dump and inspect the raw body.
When it happens
Trigger: The Garena endpoint returns {error: ...}, {data: {jobs: [...]}} (wrapped envelope), an empty object {}, or a JSON error body — anything where Array.isArray(json.jobs) is false.
Common situations: Garena changing their API to wrap results in a data envelope; region/office parameter rejected server-side yielding an error JSON; WAF or rate-limiter returning a JSON block page; office config resolving to an endpoint that answers with a different schema.
Related errors
- collage: unrecognized response envelope (expected a…
- flowxtra: unexpected API response on page
- jobicy: unexpected API response — expected
- manfred: unexpected API response — expected a JSON array of…
- 4dayweek: unexpected API response on page
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/2416f03b13c471b1.
Report an issue: GitHub.
Appendix: source
Thrown at providers/garena.mjs:40
// Response shape: { jobs: [{ id, title, tags: { location: string[], ... },
// description }] }. One request, no pagination — `jobs` is the whole board.
const API_URL = 'https://careers.garena.com/api/job/list';
const HOST = 'careers.garena.com';
const DEFAULT_OFFICE = 'global';
/**
* Normalizes one Garena API response into job entries.
* Throws if the response doesn't carry the expected `jobs[]` shape, so a
* silent endpoint change surfaces as a hard error instead of empty results.
* @param {any} json - The API response.
* @param {{ name?: string, garena?: { office?: string } }} entry
* @returns {Array<{title: string, url: string, company: string, location: string, description?: string}>}
*/
export function parseGarenaResponse(json, entry) {
const jobs = json && Array.isArray(json.jobs) ? json.jobs : null;
if (!jobs) {
throw new Error(`garena: unexpected API response — expected jobs[], got keys: [${json ? Object.keys(json).join(', ') : 'null'}]`);
}
const officeSegment = urlSegment('office', resolveOffice(entry));
const company = (entry && entry.name) || 'Garena';
const out = [];
for (const j of jobs) {
if (!j || typeof j.title !== 'string' || j.title.trim() === '') continue;
const id = j.id != null ? String(j.id).trim() : '';
if (!id) continue;
// `id` is remote API data: a lone UTF-16 surrogate makes encodeURIComponent
// throw URIError, and a `.`/`..` would be a traversal segment. Either way,
// drop just this posting — the same thing the `!id` guard above does —
// instead of the throw aborting the whole page's parse loop (#3513).
const idSegment = idUrlSegment(id);
if (idSegment === null) continue;
const locations = Array.isArray(j.tags && j.tags.location)View on GitHub (pinned to aac998c7ed)