{"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":17,"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\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":35,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/recruitee.mjs#L1-L35","documentation":"assertRecruiteeUrl throws this when new URL(url) itself raises — i.e. the string is not a parseable absolute URL at all (no scheme, invalid characters, unbalanced brackets). This is the first guard in the chain, before the HTTPS and hostname checks. It means the URL is structurally broken, not merely wrong-hosted.","triggerScenarios":"A URL string like 'acme.recruitee.com' (no protocol), 'https://' (bare scheme), 'ht!tps://x.recruitee.com', or a non-string accidentally coerced. Any value that makes the URL constructor throw.","commonSituations":"The careers_url was stored without a protocol; a template/format string produced an empty or partial URL; user input was not trimmed or validated before reaching the provider.","solutions":["Prepend 'https://' if the hostname is present but the scheme is missing.","Validate the URL string with new URL() or a URL-pattern check before passing it into the provider.","Check for stray characters, leading/trailing whitespace, or smart quotes in the source config.","Ensure the value is actually a string and not null/undefined/number."],"exampleFix":"// before\nconst url = entry.careers_url; // 'acme.recruitee.com'\nassertRecruiteeUrl(url);\n// after\nconst url = entry.careers_url.startsWith('http') ? entry.careers_url : `https://${entry.careers_url}`;","handlingStrategy":"validation","validationCode":"function ensureAbsoluteHttpsUrl(raw) {\n  if (typeof raw !== 'string' || !raw.trim()) return null;\n  let s = raw.trim();\n  if (!/^https?:\\/\\//i.test(s)) s = 'https://' + s;\n  try { new URL(s); return s; } catch { return null; }\n}\nconst safe = ensureAbsoluteHttpsUrl(entry.careers_url);\nif (!safe) { console.warn(`skip ${entry.name}: unparseable URL`); }","typeGuard":"null","tryCatchPattern":"try {\n  await provider.fetch(entry, ctx);\n} catch (e) {\n  if (/recruitee: invalid URL/.test(e.message)) {\n    console.warn(`[skip] ${entry.name}: structurally invalid URL — ${entry.careers_url}`);\n  } else throw e;\n}","preventionTips":["Store URLs always with an explicit https:// scheme.","Trim whitespace from user-supplied URLs before persistence.","Run new URL() at config-load to catch malformed entries early."],"tags":["url-validation","provider","recruitee","malformed-url"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}