{"record":{"id":"76f3c74b22942c80","repo":"santifer/career-ops","slug":"gem-jobboardlist-failed-listresult-errors-0","errorCode":null,"errorMessage":"gem: JobBoardList failed: ${listResult.errors[0]?.message || 'unknown GraphQL error'}","messagePattern":"gem: JobBoardList failed: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/gem.mjs","lineNumber":170,"sourceCode":"    const boardId = resolveBoardId(entry);\n    if (!boardId) throw new Error(`gem: cannot derive board id for ${entry.name}`);\n    assertGemUrl(GEM_API_URL);\n\n    const body = JSON.stringify([\n      { operationName: 'JobBoardList', variables: { boardId }, query: JOB_BOARD_LIST_QUERY },\n    ]);\n    // redirect:'error' prevents SSRF via server-side redirects; combined with\n    // assertGemUrl above it guarantees the final hostname stays in the allowlist.\n    const json = /** @type {any} */ (await ctx.fetchJson(GEM_API_URL, {\n      method: 'POST',\n      headers: { 'content-type': 'application/json', batch: 'true' },\n      body,\n      redirect: 'error',\n    }));\n\n    const listResult = json?.[0];\n    if (Array.isArray(listResult?.errors) && listResult.errors.length > 0) {\n      throw new Error(`gem: JobBoardList failed: ${listResult.errors[0]?.message || 'unknown GraphQL error'}`);\n    }\n    const postings = listResult?.data?.oatsExternalJobPostings?.jobPostings;\n    if (!Array.isArray(postings)) return [];\n\n    const validPostings = postings.filter(/** @param {any} p */ p => p.extId && p.title);\n\n    // Enrichment, not core data — postedAt/description matter but their\n    // absence shouldn't fail the whole board. One extra batched POST (one\n    // ExternalJobPostingQuery op per job) rather than N round-trips.\n    const postedAtByExtId = new Map();\n    const descriptionByExtId = new Map();\n    if (validPostings.length > 0) {\n      try {\n        const detailBody = JSON.stringify(\n          validPostings.map(/** @param {any} p */ p => ({\n            operationName: 'ExternalJobPostingQuery',\n            variables: { boardId, extId: p.extId },\n            query: JOB_DETAIL_QUERY,","sourceCodeStart":152,"sourceCodeEnd":188,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/gem.mjs#L152-L188","documentation":"gem.mjs throws this after the batched GraphQL POST returns, when the first element of the response array carries a non-empty errors[] array. The Gem batch endpoint returns one result object per operation; for the JobBoardList operation an errors entry means the GraphQL layer rejected the query — most often an invalid/unknown boardId, a board that is disabled/private, or a schema/operation change upstream.","triggerScenarios":"The boardId derived from careers_url is wrong or no longer published (board taken private/deleted); Gem renamed the JobBoardList operation or the oatsExternalJobPostings field and the shipped query no longer compiles server-side; the board requires auth/cookies that the public endpoint no longer grants; a transient server-side error surfaced in errors[0].message.","commonSituations":"A company migrated off Gem or made its board private; the board id in portals.yml was mistyped; Gem shipped a schema bump (the query is reverse-engineered, per the file header); rate-limiting/abuse detection returned a GraphQL error instead of an HTTP 4xx.","solutions":["Read errors[0].message from the thrown text — it usually states the exact GraphQL problem (e.g. 'board not found', field deprecation).","Confirm the boardId by opening https://jobs.gem.com/<boardId> in a browser; if it 404s, update careers_url to the correct board or disable the entry.","If the board is genuinely gone/private, set enabled: false or remove the entry.","If the message indicates a schema/operation change, re-capture the current JobBoardList query from a live browser session and update JOB_BOARD_LIST_QUERY."],"exampleFix":"# before\n- name: Acme\n  provider: gem\n  careers_url: https://jobs.gem.com/acme-old   # board disabled -> GraphQL error\n\n# after\n- name: Acme\n  provider: gem\n  careers_url: https://jobs.gem.com/acme      # correct, live board id","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"// Detect a GraphQL errors[] payload in a Gem batch result.\nfunction gemListHasErrors(json) {\n  const first = Array.isArray(json) ? json[0] : null;\n  return !!first && Array.isArray(first.errors) && first.errors.length > 0;\n}","tryCatchPattern":"// Distinguish a gone-private board (disable entry) from a transient/schema error (surface).\ntry {\n  const jobs = await gemProvider.fetch(entry, ctx);\n} catch (err) {\n  const m = /gem: JobBoardList failed: (.*)/.exec(err.message);\n  if (m && /not found|board/i.test(m[1])) {\n    console.error(`gem board for ${entry.name} unavailable: ${m[1]} — disabling entry`);\n    return [];\n  }\n  throw err;\n}","preventionTips":["Periodically re-verify each Gem boardId still resolves in a browser.","Capture errors[0].message in logs so a schema change is distinguishable from a per-board problem.","Keep JOB_BOARD_LIST_QUERY aligned with a recently captured live browser request."],"tags":["graphql","third-party-api","gem","runtime","provider-config"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}