{"record":{"id":"34f09f4ce6d893b4","repo":"santifer/career-ops","slug":"gem-unsupported-rest-response-envelope-expected-an-array-or","errorCode":null,"errorMessage":"gem: unsupported REST response envelope — expected an array or {job_posts: [...]}, got an object with keys: ${Object.keys(json).join(', ')}","messagePattern":"gem: unsupported REST response envelope — expected an array or (.+?), got an object with keys: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"providers/gem.mjs","lineNumber":285,"sourceCode":"};\n\n/**\n * Extract the row array from Gem's documented GET response. The endpoint has\n * appeared both as a bare array and wrapped in `job_posts`; accepting both\n * keeps the provider tolerant. `[]`/`{}`/`null` are legitimately contentless\n * (an empty board), so they resolve to no rows — but any OTHER nonempty\n * object shape is undocumented and gets rejected loudly rather than silently\n * read as \"zero jobs,\" which would make a changed Gem response look like an\n * empty board and drop every posting without a trace.\n * @param {any} json\n */\nfunction extractRestRows(json) {\n  if (Array.isArray(json)) return json;\n  if (json === null || json === undefined) return [];\n  if (typeof json === 'object') {\n    if (Array.isArray(json.job_posts)) return json.job_posts;\n    if (Object.keys(json).length === 0) return [];\n    throw new Error(\n      `gem: unsupported REST response envelope — expected an array or {job_posts: [...]}, got an object with keys: ${Object.keys(json).join(', ')}`\n    );\n  }\n  throw new Error(`gem: unsupported REST response envelope — expected an array or {job_posts: [...]}, got ${typeof json}`);\n}\n\n/**\n * Parse Gem's documented GET response.\n * @param {any} json\n * @param {string} companyName\n */\nexport function parseRestResponse(json, companyName) {\n  const rows = extractRestRows(json);\n  return rows.filter(j => j && typeof j.title === 'string' && j.title.trim() && typeof j.absolute_url === 'string')\n    .map(j => {\n      let url;\n      try {\n        const parsed = new URL(j.absolute_url);","sourceCodeStart":267,"sourceCodeEnd":303,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/gem.mjs#L267-L303","documentation":"Gem's REST job-board API may return either a plain array of postings or an envelope object {job_posts: [...]}. extractRestRows accepts those shapes (plus null/undefined and empty objects, treated as zero postings) and throws for any other object shape, listing its keys to aid debugging.","triggerScenarios":"The REST endpoint returned an unrecognized JSON object envelope — e.g. {data: [...]}, {results: [...]}, {jobs: [...]}, a paginated envelope {items, next_page}, or an error object {error: ...} returned with HTTP 200.","commonSituations":"Gem changed or versioned their REST response shape; hitting a different endpoint than the documented job-board API; a proxy or WAF returning its own JSON error body; a board whose API path resolves to a non-postings resource.","solutions":["Inspect the listed keys in the error message and check whether the real postings live under one of them.","Update the provider to unwrap the new envelope shape (add a branch for the observed key) after confirming Gem's current API docs.","Verify the resolved REST URL is Gem's documented job-board endpoint (resolveRestApiUrl), not a different resource.","If the body is actually an error object, handle/log it as an API error instead of treating it as a postings envelope."],"exampleFix":"// before\nif (Array.isArray(json.job_posts)) return json.job_posts;\n// after\nif (Array.isArray(json.job_posts)) return json.job_posts;\nif (Array.isArray(json.data)) return json.data; // new Gem envelope","handlingStrategy":"type-guard","validationCode":"const looksLikeEnvelope = (j) => Array.isArray(j) || (j && typeof j === 'object' && (Array.isArray(j.job_posts) || Object.keys(j).length === 0));","typeGuard":"const isPostingsEnvelope = (j) => Array.isArray(j) || (j !== null && typeof j === 'object' && Array.isArray(j.job_posts));","tryCatchPattern":"try { rows = extractRestRows(json); } catch (e) { if (e.message.includes('unsupported REST response envelope')) console.error('Gem envelope changed; keys seen:', e.message.split('keys: ')[1]); throw e; }","preventionTips":["Pin/monitor Gem's REST API version and changelog","Log unrecognized response bodies (sampled) to catch envelope drift early","Check for embedded error objects in 200 responses before parsing as postings","Add a contract test asserting the current envelope shape"],"tags":["api","response-shape","rest"],"backgroundTag":"unexpected-response-shape","analyzedSha":"aac998c7ed7248ea853b720ceeb1fdbeb322fc5d","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}