{"record":{"id":"9bfd7f3f71ef3a3f","repo":"santifer/career-ops","slug":"personio-invalid-url-url","errorCode":null,"errorMessage":"personio: invalid URL: ${url}","messagePattern":"personio: invalid URL: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/personio.mjs","lineNumber":22,"sourceCode":"\n// Personio provider — hits the public, no-auth XML jobs feed at\n// `https://<slug>.jobs.personio.de/xml` (common across DACH/EU companies).\n// Auto-detects from a `<slug>.jobs.personio.(de|com)` careers host like\n// workable/recruitee. Per-tenant subdomains are the variable part, so the\n// SSRF defence is an anchored host regex rather than a static allowlist.\n//\n// The feed is a flat, well-defined XML document, so it is parsed in-process\n// with a tiny tag extractor (no new dependency — the repo ships none for XML).\n\nconst PERSONIO_HOST_RE = /^[a-z0-9][a-z0-9-]*\\.jobs\\.personio\\.(de|com)$/;\n\n/** @param {string} url */\nfunction assertPersonioUrl(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`personio: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`personio: URL must use HTTPS: ${url}`);\n  if (!PERSONIO_HOST_RE.test(parsed.hostname))\n    throw new Error(`personio: untrusted hostname \"${parsed.hostname}\" — must match <slug>.jobs.personio.(de|com)`);\n  return url;\n}\n\n/**\n * Resolve the tenant host (e.g. `acme.jobs.personio.de`) from a careers_url.\n * Returns null for non-Personio or malformed URLs.\n * @param {import('./_types.js').PortalEntry} entry\n */\nconst PERSONIO_SLUG_RE = /^[a-z0-9][a-z0-9-]{0,62}$/i;\n\nfunction resolveHost(entry) {\n  // An explicit `personio: <slug>` pins the tenant directly. Needed because many\n  // companies embed the Personio tenant as an iframe on a branded careers page,\n  // so careers_url points at the company domain while the feed lives at","sourceCodeStart":4,"sourceCodeEnd":40,"githubUrl":"https://github.com/santifer/career-ops/blob/e7abd431fce9348a95261acac9e0c14779c35df8/providers/personio.mjs#L4-L40","documentation":"assertPersonioUrl in providers/personio.mjs validates URLs before any request to a Personio tenant. The string could not be parsed by the URL constructor at all, so the provider throws immediately. This happens before the HTTPS and hostname checks, meaning the input is not a usable absolute URL (missing scheme, spaces, typos).","triggerScenarios":"Calling fetch or any path that reaches assertPersonioUrl (personio.mjs line 22) with a careers_url that is not a valid absolute URL: 'acme.jobs.personio.de' without https://, an empty string, a URL with illegal characters, or a relative path.","commonSituations":"portals.yml entry missing the scheme (people copy the hostname from a browser address bar without https://); trailing whitespace or a stray character in the YAML value; a YAML scalar being mangled (e.g. colon in the URL without quoting causing truncation).","solutions":["Open portals.yml and give careers_url a full absolute URL with scheme: https://<slug>.jobs.personio.de or .com.","Quote the YAML value if it contains characters YAML may mangle (colons, #, leading spaces).","Trim whitespace/visible characters from the value; check for line-wrap artifacts in the YAML editor.","Test parsing first: new URL(value) in a node -e one-liner; if it throws, the string, not the network, is the problem."],"exampleFix":"// before (portals.yml)\ncareers_url: acme.jobs.personio.de/xml\n// after\ncareers_url: https://acme.jobs.personio.de/xml","handlingStrategy":"validation","validationCode":"export function isWellFormedUrl(u) {\n  if (typeof u !== 'string' || u.trim() === '') return false;\n  try { const parsed = new URL(u); return parsed.protocol === 'https:'; } catch { return false; }\n}\nif (!isWellFormedUrl(entry.careers_url)) throw new Error(`personio: careers_url for ${entry.name} is not a valid absolute https URL`);","typeGuard":"function isPersonioUrlString(u) {\n  if (typeof u !== 'string') return false;\n  try {\n    const parsed = new URL(u);\n    return parsed.protocol === 'https:' &&\n      /^[a-z0-9][a-z0-9-]*\\.jobs\\.personio\\.(de|com)$/.test(parsed.hostname);\n  } catch { return false; }\n}","tryCatchPattern":"try {\n  await personioProvider.fetch(entry, ctx);\n} catch (e) {\n  if (String(e.message).startsWith('personio: invalid URL')) {\n    logger.warn({ entry: entry.name, url: entry.careers_url }, 'careers_url unparseable — add https:// scheme and fix YAML quoting');\n    return null;\n  }\n  throw e;\n}","preventionTips":["Always write careers_url with the full https:// scheme in portals.yml.","Quote YAML scalars containing colons and avoid line-wrapping long URLs.","Run a config-lint script that does `new URL(value)` for every careers_url at load time.","Copy URLs from the browser address bar, not from prose or bookmarks."],"tags":["url-validation","config","personio","yaml"],"backgroundTag":"invalid-url-format","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"}