{"record":{"id":"771fb968851f7b53","repo":"santifer/career-ops","slug":"jobvite-invalid-url-url","errorCode":null,"errorMessage":"jobvite: invalid URL: ${url}","messagePattern":"jobvite: invalid URL: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/jobvite.mjs","lineNumber":103,"sourceCode":"\n// The XML feed inlines every job's FULL HTML description, so it is large and\n// slow by construction rather than occasionally: Tyler Technologies returns\n// 1.88 MB for 236 jobs in ~11s. That overshoots the shared 10s default in\n// _http.mjs by a second, which aborted the whole tenant and reported it as a\n// network failure. Sized to absorb a genuinely big tenant on a slow link; the\n// board page (a normal HTML document) keeps the default.\nconst FEED_TIMEOUT_MS = 45_000;\n\n/**\n * Pin a URL to the two known Jobvite hosts over HTTPS.\n * @param {string} url\n */\nfunction assertJobviteHost(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`jobvite: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:')\n    throw new Error(`jobvite: URL must use HTTPS: ${url}`);\n  if (!ALLOWED_HOSTS.has(parsed.hostname))\n    throw new Error(`jobvite: untrusted hostname \"${parsed.hostname}\" — must be ${BOARD_HOST} or ${FEED_HOST}`);\n  return url;\n}\n\n// NaN-safe Date.parse → epoch ms.\n/** @param {string} value */\nfunction toEpochMs(value) {\n  if (!value) return undefined;\n  const parsed = Date.parse(value);\n  return Number.isNaN(parsed) ? undefined : parsed;\n}\n\n/**\n * The vanity slug from a Jobvite careers URL, or null.","sourceCodeStart":85,"sourceCodeEnd":121,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/jobvite.mjs#L85-L121","documentation":"The jobvite provider's assertJobviteHost() validates URLs before fetching. When the input cannot be parsed by the URL constructor at all (malformed, empty, missing scheme), it throws 'jobvite: invalid URL'. This runs before the HTTPS and hostname checks, so it is the first-line guard on any board or feed URL.","triggerScenarios":"A portals.yml jobvite entry whose `careers_url`/`api:` value is empty, missing the scheme (jobs.boardcx.com/...), contains spaces, or is otherwise not an absolute URL.","commonSituations":"Leaving the api: value blank; pasting a relative path from a Jobvite admin page; YAML quoting issues that truncate the URL; typos like 'htp://'.","solutions":["Correct the URL in portals.yml to an absolute URL, e.g. https://jobs.boardcx.com/<company> or https://jobs.jobvite.com/<company>.","For feed URLs, use the full https:// feed host with the ?c=<companyEid> query parameter intact.","Prefer setting company_eid: so the provider builds a valid URL itself instead of parsing a hand-written one."],"exampleFix":"// before (portals.yml)\nprovider: jobvite\ncareers_url: jobs.jobvite.com/acme\n// after\nprovider: jobvite\ncareers_url: https://jobs.jobvite.com/acme","handlingStrategy":"validation","validationCode":"let u; try { u = new URL(entry.careers_url); } catch { throw new Error(`jobvite entry ${entry.name} has an unparseable URL`); }","typeGuard":"const isParseableUrl = (s) => { try { new URL(s); return true; } catch { return false; } };","tryCatchPattern":"try {\n  await provider.fetch(entry, ctx);\n} catch (e) {\n  if (e.message.includes('jobvite: invalid URL')) {\n    entry.careers_url = `https://jobs.jobvite.com/${entry.slug}`;\n  }\n}","preventionTips":["Store absolute URLs with scheme in portals.yml; quote values containing ? or &.","Validate all careers_url values with new URL() at config load time.","Prefer company_eid: over hand-written feed URLs."],"tags":["url-parsing","config","jobvite"],"backgroundTag":"invalid-url-format","analyzedSha":"aac998c7ed7248ea853b720ceeb1fdbeb322fc5d","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}