{"record":{"id":"c3ef00235e940af1","repo":"santifer/career-ops","slug":"csod-no-anonymous-token-on-cfg-homeurl","errorCode":null,"errorMessage":"csod: no anonymous token on ${cfg.homeUrl}","messagePattern":"csod: no anonymous token on (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/csod.mjs","lineNumber":200,"sourceCode":"    // it (older embedders and test mocks), which keeps the pre-cookie\n    // behaviour intact for tenants that never needed it.\n    //\n    // cfg.homeUrl and cfg.searchApi are both built from the same parsed\n    // origin, so replaying these cookies cannot reach a third-party host.\n    // redirect:'error' on the bootstrap keeps that true: origin validation\n    // covers the URL we ask for, not wherever a 3xx would send us.\n    let html;\n    let cookie = '';\n    if (typeof ctx.fetchResponse === 'function') {\n      const res = await ctx.fetchResponse(cfg.homeUrl, { redirect: 'error', headers: { accept: 'text/html' } });\n      const setCookies = typeof res?.headers?.getSetCookie === 'function' ? res.headers.getSetCookie() : [];\n      cookie = cookieHeaderFrom(setCookies);\n      html = await res.text();\n    } else {\n      html = await ctx.fetchText(cfg.homeUrl, { redirect: 'error', headers: { accept: 'text/html' } });\n    }\n    const token = extractToken(html);\n    if (!token) throw new Error(`csod: no anonymous token on ${cfg.homeUrl}`);\n\n    const wait = (ms) => (ctx.sleep ? ctx.sleep(ms) : new Promise((r) => setTimeout(r, ms)));\n    const maxPages = resolveMaxPages(entry);\n    const jobs = [];\n    const seen = new Set();\n    let total = null;\n\n    for (let page = 1; page <= maxPages; page++) {\n      if (page > 1) await wait(PAGE_DELAY_MS);\n      const json = await ctx.fetchJson(cfg.searchApi, {\n        method: 'POST',\n        redirect: 'error',\n        headers: {\n          'content-type': 'application/json',\n          accept: 'application/json',\n          authorization: `Bearer ${token}`,\n          ...(cookie ? { cookie } : {}),\n        },","sourceCodeStart":182,"sourceCodeEnd":218,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/csod.mjs#L182-L218","documentation":"Thrown by the csod provider after it fetches the careersite bootstrap page (cfg.homeUrl) and extractToken(html) returns '' — the page HTML contained no match for /\"token\"\\s*:\\s*\"([A-Za-z0-9._-]+)\"/. The bootstrap page is supposed to embed an anonymous JWT; without it the subsequent search API call cannot be authorised. This is a runtime/site-state error, not a config error: the URL resolved fine, but the page did not contain what was expected.","triggerScenarios":"The bootstrap GET returned an error/interstitial page (WAF challenge, login redirect, maintenance page, empty body); the tenant changed their page structure so the token is no longer in a \"token\":\"...\" literal; the wrong siteId was resolved so the page exists but is not the careersite bootstrap; or a network/proxy returned a rewritten body.","commonSituations":"A tenant's WAF blocking the datacenter egress IP and returning a challenge page instead of the bootstrap HTML; a Cornerstone version upgrade that moved the token into a different JSON shape; a corporate proxy injecting a block page; the siteId in the URL pointing at a non-careersite page.","solutions":["Manually open cfg.homeUrl in a browser and confirm a \"token\":\"...\" literal is present in the HTML; if the shape changed, update the extractToken regex (providers/csod.mjs).","Check the fetched HTML (log it once) for a WAF/login/maintenance page — if so, route the request through a residential IP or a different egress, or add the tenant to a skip list.","Verify the resolved siteId/corpName in cfg.homeUrl actually points at the careersite home and not a corporate landing page.","Retry transient interstitials; treat a persistent failure as a site-incompatibility and disable the entry."],"exampleFix":"// before — token regex misses a moved/renamed field\nconst m = html.match(/\"token\"\\s*:\\s*\"([A-Za-z0-9._-]+)\"/);\n\n// after — after confirming the new shape on the live page, e.g.\nconst m = html.match(/\"anonymousToken\"\\s*:\\s*\"([A-Za-z0-9._-]+)\"/);","handlingStrategy":"try-catch","validationCode":"// Cannot be fully prevented statically (the page must be fetched). Best-effort pre-flight:\nasync function csodBootstrapLooksHealthy(ctx, homeUrl) {\n  try {\n    const html = await ctx.fetchText(homeUrl, { redirect: 'error', headers: { accept: 'text/html' } });\n    return /\"token\"\\s*:\\s*\"[A-Za-z0-9._-]+\"/.test(html);\n  } catch { return false; }\n}","typeGuard":"// After fetching the bootstrap page, check the token shape before driving the search API.\nfunction htmlHasCsodToken(html) {\n  return typeof html === 'string' && /\"token\"\\s*:\\s*\"[A-Za-z0-9._-]+\"/.test(html);\n}","tryCatchPattern":"try { await csod.fetch(entry, ctx); }\ncatch (e) {\n  if (/^csod: no anonymous token/.test(e.message)) {\n    // runtime/site-state: open cfg.homeUrl in a browser; if shape changed, update extractToken regex;\n    // if a WAF/challenge page was returned, retry from a different egress or disable the entry\n  } else throw e;\n}","preventionTips":["Keep the extractToken regex in sync with the live page shape — verify after Cornerstone upgrades.","Expect some tenants' WAFs to block datacenter IPs; route via a residential egress or skip those tenants.","Log the fetched HTML length once on failure to distinguish empty/challenge/shape-change."],"tags":["csod","cornerstone","runtime","token","waf","parsing"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}