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
- Read the embedded message after 'JobBoardList failed:' — it is Gem's own GraphQL error and names the real cause.
- Re-derive/refresh the boardId for the entry and retry.
- Check whether Gem changed their GraphQL schema (field oatsExternalJobPostings) and update JOB_BOARD_LIST_QUERY accordingly.
- 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
- Refresh board ids when a board reports errors
- Watch for Gem GraphQL schema changes in their changelog/docs
- Distinguish transient (retry with backoff) from permanent (fix id/query) error messages
- Alert on this error so a broken board doesn't silently return zero jobs
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
- API error
- glints: HTTP
- glints: unexpected API response
- landingjobs: unexpected API response — expected a JSON…
- 4dayweek: unexpected API response on page
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)