santifer/career-ops · error

gem: JobBoardList failed

Error message

gem: JobBoardList failed: ${listResult.errors[0]?.message || 'unknown GraphQL error'}

What it means

Gem's GraphQL JobBoardList endpoint can return a top-level errors array even with HTTP 200. When the first response object in the batch contains GraphQL errors, the provider surfaces the first error message verbatim inside this wrapper so the caller sees the actual server-side reason.

Solutions

  1. Read the embedded message after 'JobBoardList failed:' — it is Gem's own GraphQL error and names the real cause.
  2. Re-derive/refresh the boardId for the entry and retry.
  3. Check whether Gem changed their GraphQL schema (field oatsExternalJobPostings) and update JOB_BOARD_LIST_QUERY accordingly.
  4. Retry after a delay if the message indicates throttling or a transient upstream issue.

Example fix

// no call-site code fix; fix is on the request side
// before (stale board id)
const boardId = 'old-board-id';
// after
const boardId = 'acme-456'; // refreshed from the Gem board URL
Defensive patterns

Strategy: try-catch

Validate before calling

// No pre-call check catches server-side GraphQL errors; validate boardId format client-side:
if (!/^[\w-]+$/.test(boardId)) throw new Error(`Suspicious boardId: ${boardId}`);

Type guard

const hasGraphQLErrors = (r) => Array.isArray(r?.errors) && r.errors.length > 0;

Try / catch

try { const jobs = await provider.fetch(ctx, entry); } catch (e) { if (e.message.includes('JobBoardList failed')) { const reason = e.message.split('failed: ')[1]; log.warn(`Gem rejected board ${entry.name}: ${reason}`); } throw e; }

Prevention

When it happens

Trigger: The boardId sent in the JobBoardList operation is invalid/expired/forbidden; the GraphQL query was rejected (e.g. a schema change on Gem's side deprecating oatsExternalJobPostings); transient upstream failures reported as GraphQL errors instead of HTTP errors.

Common situations: A board was deleted or its id rotated; Gem shipped a GraphQL schema change breaking the pinned JOB_BOARD_LIST_QUERY; rate-limiting or auth policies returning error payloads with 200 status.

Related errors


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

Appendix: source

Thrown at providers/gem.mjs:216

    const boardId = resolveBoardId(entry);
    if (!boardId) throw new Error(`gem: cannot derive board id for ${entry.name}`);
    assertGemUrl(GEM_API_URL);

    const body = JSON.stringify([
      { operationName: 'JobBoardList', variables: { boardId }, query: JOB_BOARD_LIST_QUERY },
    ]);
    // redirect:'error' prevents SSRF via server-side redirects; combined with
    // assertGemUrl above it guarantees the final hostname stays in the allowlist.
    const json = /** @type {any} */ (await ctx.fetchJson(GEM_API_URL, {
      method: 'POST',
      headers: { 'content-type': 'application/json', batch: 'true' },
      body,
      redirect: 'error',
    }));

    const listResult = json?.[0];
    if (Array.isArray(listResult?.errors) && listResult.errors.length > 0) {
      throw new Error(`gem: JobBoardList failed: ${listResult.errors[0]?.message || 'unknown GraphQL error'}`);
    }
    const postings = listResult?.data?.oatsExternalJobPostings?.jobPostings;
    if (!Array.isArray(postings)) return [];

    const validPostings = postings.filter(/** @param {any} p */ p => p.extId && p.title);

    // Enrichment, not core data — postedAt/description matter but their
    // absence shouldn't fail the whole board. One extra batched POST (one
    // ExternalJobPostingQuery op per job) rather than N round-trips.
    const postedAtByExtId = new Map();
    const descriptionByExtId = new Map();
    if (validPostings.length > 0) {
      try {
        const detailBody = JSON.stringify(
          validPostings.map(/** @param {any} p */ p => ({
            operationName: 'ExternalJobPostingQuery',
            variables: { boardId, extId: p.extId },
            query: JOB_DETAIL_QUERY,

View on GitHub (pinned to aac998c7ed)