santifer/career-ops · error · Error
personio: invalid URL: ${url}
Error message
personio: invalid URL: ${url} What it means
Thrown by personio's assertPersonioUrl() when new URL(url) throws — the URL is syntactically unparseable. First of three SSRF gates (valid URL → HTTPS → trusted hostname regex) for Personio job boards. Personio career sites use the pattern <slug>.jobs.personio.(de|com), and the hostname regex enforces this tenant-subdomain structure.
Source
Thrown at providers/personio.mjs:21
// Personio provider — hits the public, no-auth XML jobs feed at
// `https://<slug>.jobs.personio.de/xml` (common across DACH/EU companies).
// Auto-detects from a `<slug>.jobs.personio.(de|com)` careers host like
// workable/recruitee. Per-tenant subdomains are the variable part, so the
// SSRF defence is an anchored host regex rather than a static allowlist.
//
// The feed is a flat, well-defined XML document, so it is parsed in-process
// with a tiny tag extractor (no new dependency — the repo ships none for XML).
const PERSONIO_HOST_RE = /^[a-z0-9][a-z0-9-]*\.jobs\.personio\.(de|com)$/;
/** @param {string} url */
function assertPersonioUrl(url) {
let parsed;
try {
parsed = new URL(url);
} catch {
throw new Error(`personio: invalid URL: ${url}`);
}
if (parsed.protocol !== 'https:') throw new Error(`personio: URL must use HTTPS: ${url}`);
if (!PERSONIO_HOST_RE.test(parsed.hostname))
throw new Error(`personio: untrusted hostname "${parsed.hostname}" — must match <slug>.jobs.personio.(de|com)`);
return url;
}
/**
* Resolve the tenant host (e.g. `acme.jobs.personio.de`) from a careers_url.
* Returns null for non-Personio or malformed URLs.
* @param {import('./_types.js').PortalEntry} entry
*/
function resolveHost(entry) {
const raw = typeof entry.careers_url === 'string' ? entry.careers_url : '';
if (!raw) return null;
let parsed;
try {
parsed = new URL(raw);View on GitHub (pinned to 9b17a8ac97)
Solutions
- Log the url passed to assertPersonioUrl to identify the malformed string.
- Ensure the portals.yml personio entry has careers_url set to https://<slug>.jobs.personio.de or .com.
- If resolveHost returned a bad host, check entry.careers_url — resolveHost extracts the hostname from it.
Example fix
// before careers_url: 'acme.jobs.personio.de' // missing https:// // after careers_url: 'https://acme.jobs.personio.de'
Defensive patterns
Strategy: validation
Validate before calling
/** Validate URL string is parseable before passing to assertPersonioUrl. */
function isValidUrlString(url) {
return typeof url === 'string'
&& url.length > 0
&& (() => { try { new URL(url); return true; } catch { return false; } })();
}
if (!isValidUrlString(entry.careers_url)) {
console.warn(`personio entry ${entry.name} has invalid URL`);
continue;
} Type guard
/** @param {unknown} url @returns {url is string} */
function isParseableUrl(url) {
if (typeof url !== 'string' || !url) return false;
try { new URL(url); return true; } catch { return false; }
} Try / catch
try {
await personioProvider.fetch(entry, ctx);
} catch (err) {
if (String(err.message).startsWith('personio: invalid URL')) {
console.warn(`skipping personio entry ${entry.name}: malformed URL`);
continue;
}
throw err;
} Prevention
- Validate URL fields in portals.yml at config-load time.
- Ensure Personio URLs include https:// and follow the <slug>.jobs.personio.(de|com) pattern.
- Call detect(entry) and skip null results before fetch().
When it happens
Trigger: Called with an unparseable URL: undefined, empty string, spaces, or a schemeless path like 'acme.jobs.personio.de/xml'. The guard is called from fetch() after constructing the feed URL from resolveHost(). Since fetch() builds the URL as `https://${host}/xml`, the host itself would need to be malformed for this to fire — more likely from a direct/test invocation of assertPersonioUrl with bad input.
Common situations: A portals.yml personio entry has careers_url missing or malformed. The entry was built programmatically without the URL field. resolveHost() somehow returned a value containing illegal hostname characters. Testing with a fixture path.
Related errors
- personio: URL must use HTTPS: ${url}
- flowxtra: untrusted hostname "${parsed.hostname}" — must be
- gem: invalid URL: ${url}
- gem: URL must use HTTPS: ${url}
- gem: untrusted hostname "${parsed.hostname}" — must be one o
AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13).
Data as JSON: /api/errors/9bfd7f3f71ef3a3f.
Report an issue: GitHub.