santifer/career-ops · error · Error

personio: invalid URL

Error message

personio: invalid URL: ${url}

What it means

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).

Solutions

  1. Open portals.yml and give careers_url a full absolute URL with scheme: https://<slug>.jobs.personio.de or .com.
  2. Quote the YAML value if it contains characters YAML may mangle (colons, #, leading spaces).
  3. Trim whitespace/visible characters from the value; check for line-wrap artifacts in the YAML editor.
  4. Test parsing first: new URL(value) in a node -e one-liner; if it throws, the string, not the network, is the problem.

Example fix

// before (portals.yml)
careers_url: acme.jobs.personio.de/xml
// after
careers_url: https://acme.jobs.personio.de/xml
Defensive patterns

Strategy: validation

Validate before calling

export function isWellFormedUrl(u) {
  if (typeof u !== 'string' || u.trim() === '') return false;
  try { const parsed = new URL(u); return parsed.protocol === 'https:'; } catch { return false; }
}
if (!isWellFormedUrl(entry.careers_url)) throw new Error(`personio: careers_url for ${entry.name} is not a valid absolute https URL`);

Type guard

function isPersonioUrlString(u) {
  if (typeof u !== 'string') return false;
  try {
    const parsed = new URL(u);
    return parsed.protocol === 'https:' &&
      /^[a-z0-9][a-z0-9-]*\.jobs\.personio\.(de|com)$/.test(parsed.hostname);
  } catch { return false; }
}

Try / catch

try {
  await personioProvider.fetch(entry, ctx);
} catch (e) {
  if (String(e.message).startsWith('personio: invalid URL')) {
    logger.warn({ entry: entry.name, url: entry.careers_url }, 'careers_url unparseable — add https:// scheme and fix YAML quoting');
    return null;
  }
  throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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).

Understand the failure class

Background: "Invalid URL" errors: why new URL(), URI.parse, and reqwest::Url reject your string — missing scheme, whitespace, and bad path format — this error's family across 39 libraries.

Related errors


AI-assisted analysis of santifer/career-ops@e7abd431fc (2026-09-16). Data as JSON: /api/errors/9bfd7f3f71ef3a3f. Report an issue: GitHub.

Appendix: source

Thrown at providers/personio.mjs:22

// 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
 */
const PERSONIO_SLUG_RE = /^[a-z0-9][a-z0-9-]{0,62}$/i;

function resolveHost(entry) {
  // An explicit `personio: <slug>` pins the tenant directly. Needed because many
  // companies embed the Personio tenant as an iframe on a branded careers page,
  // so careers_url points at the company domain while the feed lives at

View on GitHub (pinned to e7abd431fc)