{"record":{"id":"31072749a861b73b","repo":"santifer/career-ops","slug":"recruitee-invalid-url-url","errorCode":null,"errorMessage":"recruitee: invalid URL: ${url}","messagePattern":"recruitee: invalid URL: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/recruitee.mjs","lineNumber":19,"sourceCode":"// @ts-check\n/** @typedef {import('./_types.js').Provider} Provider */\n\n// Recruitee provider — hits the public per-tenant offers API.\n// Auto-detects from careers_url pattern `https://<slug>.recruitee.com`.\n// Per-tenant subdomains are the variable part — SSRF defence uses a\n// regex match on `<safe-slug>.recruitee.com` rather than a static\n// allowlist.\n\nimport { htmlToText } from './_html-to-text.mjs';\n\nconst RECRUITEE_HOST_RE = /^[a-z0-9][a-z0-9-]*\\.recruitee\\.com$/;\n\nfunction assertRecruiteeUrl(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`recruitee: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`recruitee: URL must use HTTPS: ${url}`);\n  if (!RECRUITEE_HOST_RE.test(parsed.hostname)) {\n    throw new Error(`recruitee: untrusted hostname \"${parsed.hostname}\" — must match <slug>.recruitee.com`);\n  }\n  return url;\n}\n\nfunction resolveApiUrl(entry) {\n  const raw = typeof entry.careers_url === 'string' ? entry.careers_url : '';\n  if (!raw) return null;\n  let parsed;\n  try {\n    parsed = new URL(raw);\n  } catch {\n    return null;\n  }\n  if (parsed.protocol !== 'https:') return null;","sourceCodeStart":1,"sourceCodeEnd":37,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/recruitee.mjs#L1-L37","documentation":"assertRecruiteeUrl() first requires the URL to be parseable by the URL constructor. If new URL(url) throws (malformed URL — missing scheme, illegal characters, empty string), this error is thrown before any host or protocol checks.","triggerScenarios":"assertRecruiteeUrl() (or fetch(), which routes through it) receives a string like \"acme.recruitee.com\" without a scheme, an empty string, a URL with spaces, or any otherwise unparseable value.","commonSituations":"A portals.yml entry records the bare hostname without https://; trailing whitespace or invisible characters in a copy-pasted URL; a careers_url field accidentally left blank or containing placeholder text.","solutions":["Prefix the URL with https:// if only the hostname was given","Trim whitespace and re-check for typos or placeholder text in the careers_url field","Validate the URL with new URL(raw) locally before adding it to portals.yml"],"exampleFix":"// before\ncareers_url: acme.recruitee.com\n// after\ncareers_url: https://acme.recruitee.com","handlingStrategy":"validation","validationCode":"function isValidUrl(s) {\n  try { new URL(s); return true; } catch { return false; }\n}\n// call before invoking the provider:\nif (!isValidUrl(entry.careers_url)) throw new Error(`Fix careers_url for ${entry.name}`);","typeGuard":"function isHttpUrl(v) {\n  if (typeof v !== 'string') return false;\n  try { const u = new URL(v); return u.protocol === 'http:' || u.protocol === 'https:'; } catch { return false; }\n}","tryCatchPattern":"try {\n  assertRecruiteeUrl(entry.careers_url);\n} catch (err) {\n  if (err.message.startsWith('recruitee: invalid URL')) {\n    console.error(`Add scheme: got \"${entry.careers_url}\", expected \"https://...\"`);\n  }\n  throw err;\n}","preventionTips":["Always store absolute URLs with scheme in portals.yml","Trim copy-pasted URLs to remove stray whitespace","Run new URL(raw) as a local sanity check before committing config"],"tags":["url-validation","config"],"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"}