{"record":{"id":"d2ae3fe030232a12","repo":"santifer/career-ops","slug":"lever-invalid-url-url","errorCode":null,"errorMessage":"lever: invalid URL: ${url}","messagePattern":"lever: invalid URL: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/lever.mjs","lineNumber":22,"sourceCode":"// Lever provider — hits the public postings endpoint.\n// Auto-detects from careers_url via jobs.(eu.)?lever.co/<slug>.\n// Handles both explicit `api:` URLs and auto-detection from `careers_url`.\n\nconst ALLOWED_LEVER_HOSTS = new Set(['api.lever.co', 'api.eu.lever.co']);\n\n// The v0 postings endpoint returns the whole board in one response, with every\n// description inlined, so a large board outgrows _http.mjs's 10s default:\n// jobgether is 42.8 MB and aborted at 10s on its own (#4177). Same value and\n// reasoning as ASHBY_TIMEOUT_MS, the other one-response board-wide ATS feed.\nconst LEVER_TIMEOUT_MS = 30_000;\n\n/** @param {string} url */\nfunction assertLeverUrl(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`lever: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`lever: URL must use HTTPS: ${url}`);\n  if (!ALLOWED_LEVER_HOSTS.has(parsed.hostname))\n    throw new Error(`lever: untrusted hostname \"${parsed.hostname}\" — must be one of: ${[...ALLOWED_LEVER_HOSTS].join(', ')}`);\n  return url;\n}\n\n/** @param {import('./_types.js').PortalEntry} entry */\nfunction resolveApiUrl(entry) {\n  // Explicit api: wins — lets an entry keep a human-facing corporate\n  // careers_url (e.g. https://www.coalfire.com/careers) while still pinning\n  // the Lever postings board (mirrors greenhouse's api: precedence).\n  if (entry.api) {\n    assertLeverUrl(entry.api);\n    return entry.api;\n  }\n  let url;\n  try {","sourceCodeStart":4,"sourceCodeEnd":40,"githubUrl":"https://github.com/santifer/career-ops/blob/e7abd431fce9348a95261acac9e0c14779c35df8/providers/lever.mjs#L4-L40","documentation":"The lever provider validates URLs with assertLeverUrl; anything the URL constructor cannot parse throws this error. Unlike the single-host providers, Lever uses a host allowlist (ALLOWED_LEVER_HOSTS), but malformed URLs are rejected first at the parse stage. The module prefix 'lever:' identifies the validator in logs.","triggerScenarios":"Calling assertLeverUrl (directly or via the provider) with an unparseable string: empty value, missing scheme, spaces, or an unresolved placeholder such as '${entry.api}'.","commonSituations":"portals.yml entry where the api/careers URL field is blank or truncated, interpolation of an undefined variable producing 'undefined', or hand-editing that splits the URL across lines.","solutions":["Supply a complete absolute URL, e.g. 'https://api.lever.co/v0/postings/company?mode=json'.","Check the portals.yml entry for empty/missing fields or unresolved variables.","Validate with new URL(u) in a try/catch before calling the provider.","Inspect the interpolated value in the message — it shows exactly what string reached the validator."],"exampleFix":"// before\nassertLeverUrl(`https://api.lever.co/v0/postings/${entry.slug}`);\n// after (entry.slug undefined -> invalid)\nif (!entry?.slug) throw new Error('lever: missing company slug');\nassertLeverUrl(`https://api.lever.co/v0/postings/${entry.slug}`);","handlingStrategy":"validation","validationCode":"function isValidUrl(u) { try { new URL(u); return true; } catch { return false; } }\nif (!isValidUrl(url)) throw new Error(`skipping lever entry: invalid URL ${url}`);","typeGuard":"function isParseableUrl(v) { return typeof v === 'string' && (() => { try { new URL(v); return true; } catch { return false; } })(); }","tryCatchPattern":"try {\n  provider.fetch(entry, ctx);\n} catch (e) {\n  if (e.message.startsWith('lever: invalid URL')) {\n    console.error(`Bad lever config/entry: ${e.message}`);\n    return null;\n  }\n  throw e;\n}","preventionTips":["Guard optional fields (slug, api) before interpolating into URLs","Validate generated URLs with new URL() before use","Fail soft per entry so one bad entry doesn't abort the scan","Avoid undefined/empty template placeholders in URL construction"],"tags":["url-validation","config","input-validation"],"backgroundTag":"invalid-url","analyzedSha":"e7abd431fce9348a95261acac9e0c14779c35df8","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}