santifer/career-ops · error · Error
personio: cannot derive feed URL for
Error message
personio: cannot derive feed URL for ${entry.name} What it means
The Personio provider derives its XML feed URL from a tenant host resolved from the entry (resolveHost). When no allowlisted *.jobs.personio.(de|com) host can be extracted, fetch() throws instead of building a URL from unknown input. Note that fetch applies the same host regex, so this is thrown even for URLs the generic URL parser accepts — the host just isn't a Personio tenant.
Solutions
- Add careers_url to the portals.yml entry pointing at https://<slug>.jobs.personio.de or .com (the /xml suffix is appended by the provider).
- Verify the field name is exactly careers_url and the provider id is personio.
- Run audit-portals.mjs to see which provider actually claims this careers_url; reassign the entry if the company uses a different ATS.
- If the slug is unknown, check the company's careers page HTML for the personio tenant host and use it verbatim.
Example fix
// before (portals.yml) - name: Acme provider: personio // after - name: Acme provider: personio careers_url: https://acme.jobs.personio.de
Defensive patterns
Strategy: validation
Validate before calling
function personioHostResolvable(entry) {
const raw = typeof entry.careers_url === 'string' ? entry.careers_url : '';
if (!raw) return false;
try {
return PERSONIO_HOST_RE.test(new URL(raw).hostname);
} catch { return false; }
}
if (!personioHostResolvable(entry)) throw new Error(`personio: entry ${entry.name} has no derivable feed host`); Type guard
function hasPersonioCareersUrl(entry) {
return entry != null && typeof entry === 'object' &&
typeof entry.careers_url === 'string' && entry.careers_url.length > 0 &&
isPersonioTenantUrl(entry.careers_url);
} Try / catch
try {
await personioProvider.fetch(entry, ctx);
} catch (e) {
if (String(e.message).startsWith('personio: cannot derive feed URL')) {
logger.warn({ entry: entry.name }, 'no careers_url / not a Personio host — check provider assignment');
return null;
}
throw e;
} Prevention
- Require careers_url on every personio entry via a schema check before scanning.
- Verify provider ids with audit-portals.mjs when adding companies.
- Watch for key typos (career_url, careersUrl) in portals.yml — they silently yield undefined.
- Document each entry's tenant slug next to the URL for future maintenance.
When it happens
Trigger: Calling providers/personio.mjs fetch(entry, ctx) where entry.careers_url is absent, not a string, points to another ATS (Greenhouse/Lever), or points to a vanity domain; also when the YAML field name is misspelled so careers_url is undefined.
Common situations: Entry added with only a company name and no careers_url; wrong provider id assigned to the company; field typo (career_url / careersUrl) in portals.yml; company migrated off Personio.
Understand the failure class
Background: "missing required config value" errors: why libraries refuse to start when a configuration key is empty, unset, or blank — this error's family across 48 libraries.
Related errors
- oraclecloud: cannot derive API URL for
- phenom: cannot resolve origin for
- collage: cannot derive API URL for
- jibeapply: cannot derive API URL for
- jobbankca: entry " " has no jobbankca.keywords[] and no…
AI-assisted analysis of santifer/career-ops@e7abd431fc (2026-09-16).
Data as JSON: /api/errors/35e14bfe571d67a3.
Report an issue: GitHub.
Appendix: source
Thrown at providers/personio.mjs:79
// NaN-safe Date.parse — `|| undefined` would also coerce a valid epoch 0.
function toEpochMs(value) {
if (!value) return undefined;
const parsed = Date.parse(value);
return Number.isNaN(parsed) ? undefined : parsed;
}
/** @type {Provider} */
export default {
id: 'personio',
detect(entry) {
const host = resolveHost(entry);
return host ? { url: `https://${host}/xml` } : null;
},
async fetch(entry, ctx) {
const host = resolveHost(entry);
if (!host) throw new Error(`personio: cannot derive feed URL for ${entry.name}`);
const feedUrl = `https://${host}/xml`;
assertPersonioUrl(feedUrl);
// redirect:'error' prevents SSRF via server-side redirects; combined with
// assertPersonioUrl above it guarantees the final hostname stays in-domain.
try {
const text = await ctx.fetchText(feedUrl, { redirect: 'error' });
return parsePersonioXml(text, entry.name, host);
} catch (err) {
if (err?.status !== 404) throw err;
// Some tenants disable the public XML feed. The careers page itself is
// still server-rendered with the full job list in the initial HTML, so
// fall back to scraping it directly instead of giving up.
// ?language=en forces English titles — unlike the XML feed (which has
// no language param and always renders in the tenant's default
// language), the HTML page respects it.
const pageUrl = `https://${host}/?language=en`;
assertPersonioUrl(pageUrl);
const html = await ctx.fetchText(pageUrl, { redirect: 'error' });View on GitHub (pinned to e7abd431fc)