{"record":{"id":"e40decd48c929e79","repo":"santifer/career-ops","slug":"csod-cannot-resolve-careersite-url-for-entry-na","errorCode":null,"errorMessage":"csod: cannot resolve careersite URL for ${entry.name}","messagePattern":"csod: cannot resolve careersite URL for (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/csod.mjs","lineNumber":174,"sourceCode":"  if (Number.isInteger(v) && v > 0) return Math.min(v, MAX_PAGES);\n  return MAX_PAGES;\n}\n\n/** @type {Provider} */\nexport default {\n  id: 'csod',\n\n  detect(entry) {\n    const url = entry.api || entry.careers_url || '';\n    if (typeof url !== 'string') return null;\n    // Host check (not a path substring) so evil.com/x.csod.com can't spoof it,\n    // and the URL must carry the careersite path shape we know how to drive.\n    return resolveConfig({ api: url }) ? { url } : null;\n  },\n\n  async fetch(entry, ctx) {\n    const cfg = resolveConfig(entry);\n    if (!cfg) throw new Error(`csod: cannot resolve careersite URL for ${entry.name}`);\n\n    // The bootstrap page yields two things, not one: the anonymous bearer\n    // token, and — on some tenants — the session cookies the search API\n    // insists on. careers-kln rejects an otherwise valid token+body with\n    // \"HTTP 401 CSOD Unauthorized\" until those cookies come back with it, so\n    // the token alone is not a sufficient credential. Prefer ctx.fetchResponse\n    // to see Set-Cookie; fall back to fetchText when the caller's ctx predates\n    // 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' } });","sourceCodeStart":156,"sourceCodeEnd":192,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/csod.mjs#L156-L192","documentation":"Thrown by the csod (Cornerstone OnDemand) provider's fetch() when resolveConfig(entry) returns null. resolveConfig needs an https URL whose host is exactly csod.com or ends in .csod.com, AND whose path carries the careersite shape /ux/ats/careersite/{digits}/ (a numeric siteId). Both the branded corporate page (careers_url) and the csod.com URL (api:) follow this convention. detect() applies the same check, so this fires only when fetch() is driven on a non-detecting entry.","triggerScenarios":"entry.api/entry.careers_url is missing, unparseable, non-https, not on *.csod.com, or its path lacks a numeric /ux/ats/careersite/{siteId}/ segment.","commonSituations":"Putting the branded corporate page (not on csod.com) in api:; using an http URL (rejected because session cookies would travel in clear); a URL whose siteId path was copied without the /ux/ats/careersite/ prefix; omitting the csod.com URL entirely.","solutions":["Set api: to the full csod.com careersite URL, e.g. https://career-ohb.csod.com/ux/ats/careersite/4/home?c=career-ohb.","Ensure the path contains /ux/ats/careersite/<numeric siteId>/ — the regex extracts the siteId from there.","Keep the branded page in careers_url and the csod.com URL in api: (same convention as workday/successfactors).","Gate with provider.detect(entry) before fetch()."],"exampleFix":"# before — branded host only, no csod.com URL\n- name: OHB\n  provider: csod\n  careers_url: https://www.career-ohb.com\n\n# after — csod.com careersite URL with numeric siteId\n- name: OHB\n  provider: csod\n  careers_url: https://www.career-ohb.com\n  api: https://career-ohb.csod.com/ux/ats/careersite/4/home?c=career-ohb","handlingStrategy":"validation","validationCode":"import csod from './providers/csod.mjs';\nif (!csod.detect(entry)) {\n  // entry.api/entry.careers_url is not https://*.csod.com/ux/ats/careersite/<id>/... — fix config\n}","typeGuard":"/** True when entry resolves to a CSOD careersite (https *.csod.com with /ux/ats/careersite/<digits>/). */\nfunction isCsodEntry(entry) {\n  const raw = typeof entry?.api === 'string' ? entry.api : (typeof entry?.careers_url === 'string' ? entry.careers_url : '');\n  if (!raw) return false;\n  let u;\n  try { u = new URL(raw); } catch { return false; }\n  if (u.protocol !== 'https:') return false;\n  const host = u.host.toLowerCase();\n  if (host !== 'csod.com' && !host.endsWith('.csod.com')) return false;\n  return /\\/ux\\/ats\\/careersite\\/\\d+/.test(u.pathname);\n}","tryCatchPattern":"try { await csod.fetch(entry, ctx); }\ncatch (e) {\n  if (/^csod: cannot resolve careersite URL/.test(e.message)) {\n    // config issue — set api: to the csod.com careersite URL; do not retry\n  } else throw e;\n}","preventionTips":["Put the branded page in careers_url and the csod.com URL in api:.","The path must carry /ux/ats/careersite/<numeric siteId>/.","https is mandatory (session cookies are replayed — no cleartext)."],"tags":["config","csod","cornerstone","ats","url-resolution","ssrf"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}