santifer/career-ops · error

gem: unsupported REST response envelope — expected an array…

Error message

gem: unsupported REST response envelope — expected an array or {job_posts: [...]}, got ${typeof json}

What it means

The non-object branch of extractRestRows: after ruling out arrays, null/undefined, and objects, the parsed JSON body was some other JSON type (string, number, boolean). The provider cannot treat that as a postings collection and throws the same envelope message with the actual typeof value.

Solutions

  1. Check typeof in the error message: a string usually means the wrong URL was hit — verify the REST endpoint path.
  2. Ensure fetchJson is pointed at the documented job-board postings endpoint.
  3. If Gem now wraps postings as a JSON-encoded string, JSON.parse the inner string before extractRestRows.
  4. Log the raw response body once to confirm what the server actually returns.

Example fix

// before
const rows = extractRestRows(await ctx.fetchJson(url));
// after
let body = await ctx.fetchJson(url);
if (typeof body === 'string') body = JSON.parse(body);
const rows = extractRestRows(body);
Defensive patterns

Strategy: type-guard

Validate before calling

if (typeof body !== 'object' && typeof body !== 'undefined') throw new Error(`Gem REST returned ${typeof body}, expected object/array`);

Type guard

const isJsonContainer = (v) => v === null || v === undefined || typeof v === 'object';

Try / catch

try { rows = extractRestRows(json); } catch (e) { if (/got (string|number|boolean)/.test(e.message)) console.error('Non-object JSON body — likely wrong endpoint URL'); throw e; }

Prevention

When it happens

Trigger: The endpoint returned a JSON scalar — e.g. a bare string body ("OK"), a quoted error message, or an API version returning a count/number instead of a list; or ctx.fetchJson returning a pre-decoded primitive.

Common situations: Hitting a health-check or index endpoint instead of the postings endpoint; Gem returning a JSON-encoded string wrapper; a misconfigured api URL pointing at a non-API resource.

Related errors


AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16). Data as JSON: /api/errors/a5fda50d0e9ea42e. Report an issue: GitHub.

Appendix: source

Thrown at providers/gem.mjs:289

 * appeared both as a bare array and wrapped in `job_posts`; accepting both
 * keeps the provider tolerant. `[]`/`{}`/`null` are legitimately contentless
 * (an empty board), so they resolve to no rows — but any OTHER nonempty
 * object shape is undocumented and gets rejected loudly rather than silently
 * read as "zero jobs," which would make a changed Gem response look like an
 * empty board and drop every posting without a trace.
 * @param {any} json
 */
function extractRestRows(json) {
  if (Array.isArray(json)) return json;
  if (json === null || json === undefined) return [];
  if (typeof json === 'object') {
    if (Array.isArray(json.job_posts)) return json.job_posts;
    if (Object.keys(json).length === 0) return [];
    throw new Error(
      `gem: unsupported REST response envelope — expected an array or {job_posts: [...]}, got an object with keys: ${Object.keys(json).join(', ')}`
    );
  }
  throw new Error(`gem: unsupported REST response envelope — expected an array or {job_posts: [...]}, got ${typeof json}`);
}

/**
 * Parse Gem's documented GET response.
 * @param {any} json
 * @param {string} companyName
 */
export function parseRestResponse(json, companyName) {
  const rows = extractRestRows(json);
  return rows.filter(j => j && typeof j.title === 'string' && j.title.trim() && typeof j.absolute_url === 'string')
    .map(j => {
      let url;
      try {
        const parsed = new URL(j.absolute_url);
        if (parsed.protocol !== 'https:' || parsed.hostname !== 'jobs.gem.com' || !GEM_POSTING_PATH_RE.test(parsed.pathname)) return null;
        url = parsed.href;
      } catch {
        return null;

View on GitHub (pinned to aac998c7ed)