santifer/career-ops · error · Error
smartrecruiters: untrusted hostname
Error message
smartrecruiters: untrusted hostname "${parsed.hostname}" — must be one of: ${[...ALLOWED_SMARTRECRUITERS_HOSTS].join(', ')} What it means
assertSmartRecruitersUrl pins the hostname to the allowlist ALLOWED_SMARTRECRUITERS_HOSTS, which contains only api.smartrecruiters.com. A syntactically valid HTTPS URL whose hostname is anything else (careers.smartrecruiters.com, a branded custom domain, a lookalike host) is rejected. This is an SSRF-style guard: only provider-constructed API URLs may be fetched.
Solutions
- Ensure only URLs on api.smartrecruiters.com are passed to the validator — derive them via buildPostingsUrl(slug)
- For a branded careers page, keep careers_url as-is and add api: 'https://careers.smartrecruiters.com/<slug>' so resolveSlug can pin the slug
- Fix typos in the hostname and re-run
- If a genuinely new official host appears, add it to ALLOWED_SMARTRECRUITERS_HOSTS in providers/smartrecruiters.mjs
Example fix
// before const url = 'https://careers.smartrecruiters.com/acme/postings'; // careers host, not API // after const url = 'https://api.smartrecruiters.com/v1/companies/acme/postings?limit=100&offset=0&status=PUBLIC';
Defensive patterns
Strategy: validation
Validate before calling
function isSrApiUrl(url) {
try {
const u = new URL(url);
return u.protocol === 'https:' && u.hostname === 'api.smartrecruiters.com';
} catch { return false; }
}
if (!isSrApiUrl(url)) throw new Error('only api.smartrecruiters.com URLs are fetchable'); Type guard
const isSrApiHost = (url) => {
try { return new URL(url).hostname === 'api.smartrecruiters.com'; } catch { return false; }
}; Try / catch
try {
await srProvider.fetch(entry, ctx);
} catch (e) {
if (String(e.message).includes('untrusted hostname')) {
console.error(`Entry ${entry.name}: careers pages are not API URLs — pin the slug via api: https://careers.smartrecruiters.com/<slug>`);
} else throw e;
} Prevention
- Never pass the public careers URL (careers/jobs.smartrecruiters.com) where an API URL is expected
- For branded domains, set entry.api to the careers.smartrecruiters.com slug URL and keep the branded page as careers_url
- Build fetch URLs only via buildPostingsUrl(slug)
- Check hostnames for typos — the allowlist is exact-match
When it happens
Trigger: Calling assertSmartRecruitersUrl with the public careers page (careers.smartrecruiters.com/...) instead of the API host; pointing the provider at a branded domain like jobs.continental.com without an api override; a mistyped host (api.smartrecruiter.com).
Common situations: Misunderstanding that careers_url feeds the validator directly — it only supplies the slug; typos in api.smartrecruiters.com; trying to scrape a custom-domain SmartRecruiters tenant by URL instead of configuring provider: smartrecruiters with an api pin.
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
- solidjobs: untrusted hostname
- senjob: untrusted hostname
- workable: untrusted hostname
- collage: untrusted hostname
- comeet: URL path must be the careers-api endpoint
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/3aa9baa212c23026.
Report an issue: GitHub.
Appendix: source
Thrown at providers/smartrecruiters.mjs:74
const parts = [];
for (const key of ['companyDescription', 'jobDescription', 'qualifications', 'additionalInformation']) {
const text = sections[key]?.text;
if (typeof text === 'string' && text.trim()) parts.push(text);
}
if (parts.length === 0) return '';
return htmlToText(parts.join('\n'));
}
function assertSmartRecruitersUrl(url) {
let parsed;
try {
parsed = new URL(url);
} catch {
throw new Error(`smartrecruiters: invalid URL: ${url}`);
}
if (parsed.protocol !== 'https:') throw new Error(`smartrecruiters: URL must use HTTPS: ${url}`);
if (!ALLOWED_SMARTRECRUITERS_HOSTS.has(parsed.hostname)) {
throw new Error(`smartrecruiters: untrusted hostname "${parsed.hostname}" — must be one of: ${[...ALLOWED_SMARTRECRUITERS_HOSTS].join(', ')}`);
}
return url;
}
function resolveSlug(entry) {
// entry.api takes precedence over careers_url (mirrors greenhouse/ashby) so a
// branded page (e.g. https://jobs.continental.com) can stay as careers_url
// while the SmartRecruiters slug is pinned via
// api: https://careers.smartrecruiters.com/<slug> in portals.yml.
for (const raw of [entry.api, entry.careers_url]) {
if (typeof raw !== 'string' || !raw) continue;
let parsed;
try {
parsed = new URL(raw);
} catch {
continue;
}
if (parsed.protocol !== 'https:') continue;View on GitHub (pinned to aac998c7ed)