santifer/career-ops · error · Error
generalist-world: no gw-job-card anchors and no listing…
Error message
generalist-world: no gw-job-card anchors and no listing container in the response; the page structure likely changed
What it means
parseGeneralistWorldJobs throws when the rendered page contains neither gw-job-card anchors nor the board listing container (BOARD_MARKER_RE). A page with the container but no cards is an alive-but-empty board (returns []); a page with neither means the page structure changed or the response is not the board at all. This prevents a redesign or a challenge page from quietly reading as zero postings forever.
Solutions
- Inspect the HTML actually fetched from LIST_URL; identify whether it is a challenge/error page or a redesigned board
- If redesigned, update BOARD_MARKER_RE and CARDS_RE in providers/generalist-world.mjs to the new container and card selectors
- Retry the request — a transient challenge may resolve with different headers or after a delay
- If the board permanently moved URL/template, update LIST_URL or the provider wiring
Example fix
// before: stale marker misses new template const BOARD_MARKER_RE = /id="job-listing"/; // after: match the redesigned container const BOARD_MARKER_RE = /<div class="board[^"]*jobs?[^"]*"/i;
Defensive patterns
Strategy: fallback
Validate before calling
function looksLikeGwBoardOrEmpty(html) {
const s = String(html ?? '');
return /gw-job-card/.test(s) || /job-listing|board/.test(s) || !s.trim();
} Type guard
function isNonBoardHtml(html) {
const s = String(html ?? '');
return s.trim().length > 0 && !/gw-job-card/.test(s) && !/job-listing|board/.test(s);
} Try / catch
try {
const jobs = await gwProvider.fetch(entry, ctx);
} catch (e) {
if (e.message.includes('no listing container')) {
// retry once (transient challenge/error page), then alert on structure change
const jobs2 = await gwProvider.fetch(entry, ctx).catch(err => { alertScrapeRegression('generalist-world'); throw err; });
return jobs2;
}
throw e;
} Prevention
- Add a single retry with backoff before treating the throw as a permanent structure change — challenge pages are transient
- Log a trimmed HTML snippet with the error so responders can immediately tell challenge page vs redesign
- Keep BOARD_MARKER_RE covering the container's stable attributes (id/class) and update fixtures when the site redesigns
- Alert on this error distinctly from error 1: no cards at all vs cards-that-fail point to different fixes
When it happens
Trigger: Calling parseGeneralistWorldJobs on HTML with no gw-job-card anchors and no listing-container marker — e.g. the list page moved to a new template, a CDN/bot-challenge page was served, or an empty/whitespace body (which returns [] earlier) was not the case.
Common situations: Generalist World restructures its board page so the marker and card classes disappear; fetchText follows to an error or login page; an interposed proxy returns an HTML error page; tests feed a fixture from an unrelated page.
Related errors
- generalist-world: gw-job-card anchor(s) matched but none…
- avature: still contains JobDetail links but no article…
- itviec: still contains job cards but none could be parsed —…
- careerviet: still contains job cards but none could be…
- join: __NEXT_DATA__ not found or unexpected structure
AI-assisted analysis of santifer/career-ops@e7abd431fc (2026-09-22).
Data as JSON: /api/errors/16294a0686dccd1f.
Report an issue: GitHub.
Appendix: source
Thrown at providers/generalist-world.mjs:297
let cards = 0;
for (const m of rendered.matchAll(CARDS_RE)) {
cards++;
const job = cardToJob(m[1], m[2]);
if (!job || seen.has(job.url)) continue;
seen.add(job.url);
jobs.push(job);
}
// Anchors that all fail to yield a job mean an inner field moved, not an
// empty board: the listing container is still there, so without this check
// a card redesign would read as zero postings forever. A malformed card
// among usable ones is still just skipped.
if (jobs.length === 0 && cards > 0) {
throw new Error(
`generalist-world: ${cards} gw-job-card anchor(s) matched but none carried a usable title, employer and /jobs/{slug}/ link; the card markup likely changed`,
);
}
if (jobs.length === 0 && !BOARD_MARKER_RE.test(rendered)) {
throw new Error(
'generalist-world: no gw-job-card anchors and no listing container in the response; the page structure likely changed',
);
}
return jobs;
}
/** @param {import('./_types.js').PortalEntry | null | undefined} entry */
function detectGeneralistWorldEntry(entry) {
if (!entry || typeof entry !== 'object') return null;
if (entry.provider === 'generalist-world') return { url: LIST_URL };
if (entry.provider) return null;
for (const value of [entry.api, entry.careers_url]) {
if (typeof value !== 'string') continue;
try {
const parsed = new URL(value);
if (parsed.protocol === 'https:' && TRUSTED_HOSTS.has(parsed.hostname)) return { url: LIST_URL };
} catch {View on GitHub (pinned to e7abd431fc)