santifer/career-ops · error · Error
personio: URL must use HTTPS
Error message
personio: URL must use HTTPS: ${url} What it means
assertPersonioUrl requires every URL it accepts to use HTTPS; a parsed URL with a different protocol (http:, ftp:) is rejected. Personio feeds contain candidate-facing job data and the provider enforces TLS both for security and to keep the SSRF guard simple — the same allowlisted host must be reached over https only.
Solutions
- Change the scheme to https:// in the portals.yml careers_url value.
- Confirm the tenant feed actually serves https (it always does for *.jobs.personio.de/com) by opening the URL in a browser.
- If the URL comes from a script that builds feed URLs, fix the template there (e.g. `https://${host}/xml`).
- Search the repo for `http://` occurrences in portals.yml to catch other insecure entries at once.
Example fix
// before (portals.yml) careers_url: http://acme.jobs.personio.de/xml // after careers_url: https://acme.jobs.personio.de/xml
Defensive patterns
Strategy: validation
Validate before calling
function isHttpsUrl(u) {
if (typeof u !== 'string') return false;
try { return new URL(u).protocol === 'https:'; } catch { return false; }
}
if (!isHttpsUrl(entry.careers_url)) throw new Error(`personio: careers_url for ${entry.name} must use https://`); Type guard
function isHttpsPersonioUrl(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: URL must use HTTPS')) {
logger.warn({ entry: entry.name, url: entry.careers_url }, 'insecure scheme — rewrite http:// to https:// in portals.yml');
return null;
}
throw e;
} Prevention
- Default to https:// when authoring any careers_url; never author http://.
- Lint portals.yml in CI: reject any careers_url whose parsed protocol is not https:.
- If a migration/old doc contains http links, bulk-replace schemes before committing config.
- Remember the provider appends /xml to an https base — keep the base https.
When it happens
Trigger: Calling any code path through assertPersonioUrl (personio.mjs line 24) with a careers_url whose scheme is http:// — most commonly http://acme.jobs.personio.de/xml — or whose scheme came out as something else due to a malformed string.
Common situations: Config authored by hand with http:// out of habit; an old bookmark or internal link pre-dating an HTTPS migration; documentation snippets copied with http; or a proxy setup that rewrites https to http internally.
Understand the failure class
Background: "Invalid URL" / "URL cannot be empty": fix the malformed or missing URL behind request-construction failures — this error's family across 50 libraries.
Related errors
- consider: needs an https careers_url on a public host
- itviec: URL must use HTTPS
- jobstreet: URL must use HTTPS
- personio: invalid URL
- personio: untrusted hostname
AI-assisted analysis of santifer/career-ops@e7abd431fc (2026-09-16).
Data as JSON: /api/errors/8348050da63b91e4.
Report an issue: GitHub.
Appendix: source
Thrown at providers/personio.mjs:24
// `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
// <slug>.jobs.personio.de. The slug is charset-restricted here and the
// resulting URL still goes through assertPersonioUrl(), so the host allowlistView on GitHub (pinned to e7abd431fc)