santifer/career-ops · error
eightfold: untrusted hostname
Error message
eightfold: untrusted hostname "${parsed.hostname}" — must match *.eightfold.ai What it means
assertEightfoldUrl is the provider's SSRF guard: every request URL must parse, use HTTPS, and have a hostname matching /^[a-z0-9-]+\.eightfold\.ai$/i before it is fetched. This error is thrown when a URL passes parsing and the HTTPS check but its hostname is not an *.eightfold.ai tenant host — e.g. a branded CNAME like careers.<company>.com or a hand-edited host. Eightfold's jobs API is host-pinned to *.eightfold.ai, so non-canonical hosts are refused rather than fetched.
Solutions
- Change the portal entry's careers_url (or set entry.api) to the canonical https://<tenant>.eightfold.ai form; branded CNAMEs are deliberately not accepted.
- Verify the hostname matches a single-label subdomain of eightfold.ai — no multi-level subdomains (a.b.eightfold.ai fails the regex) and no paths/ports; the API is inferred from the tenant host alone.
- If you need to keep a branded careers page for display, keep it in careers_url and pin the tenant host via entry.api, which takes precedence.
- If this fires unexpectedly in your own code, log parsed.hostname from the thrown message and diff it against EIGHTFOLD_HOST_RE before retrying.
Example fix
// before (portals.yml entry) - name: Bayer careers_url: https://talent.bayer.com/careers // after - name: Bayer careers_url: https://bayer.eightfold.ai/careers
Defensive patterns
Strategy: validation
Validate before calling
function isEightfoldUrl(url) {
try {
const u = new URL(url);
return u.protocol === 'https:' && /^[a-z0-9-]+\.eightfold\.ai$/i.test(u.hostname);
} catch { return false; }
}
// before calling: if (!isEightfoldUrl(entry.careers_url)) throw ... Type guard
const isTrustedEightfoldHost = (hostname) => typeof hostname === 'string' && /^[a-z0-9-]+\.eightfold\.ai$/i.test(hostname);
Try / catch
try {
await provider.fetch(entry, ctx);
} catch (err) {
if (String(err.message).startsWith('eightfold: untrusted hostname')) {
console.error(`Config error: ${entry.name} must use an *.eightfold.ai host, got: ${entry.careers_url}`);
return [];
}
throw err;
} Prevention
- Always point eightfold entries at https://<tenant>.eightfold.ai, never branded CNAMEs
- Pin the tenant host via entry.api when careers_url must stay branded
- Validate portal entries with the same regex at config load time
- Remember the guard rejects multi-level subdomains and non-https schemes
When it happens
Trigger: Calling provider.fetch() (which calls assertEightfoldUrl before every page request) with a portal entry whose api/careers_url points at a non-eightfold.ai host — practically this means the built-in apiUrl was tampered with, a custom caller passes its own URL into an exported path, or entry data was mutated between resolveTenant and fetch. resolveTenant itself rejects off-host entries, so the realistic trigger is feeding this guard a URL directly or config drift.
Common situations: Configuring a portal entry with the company's branded careers CNAME (talent.bayer.com) instead of the canonical bayer.eightfold.ai; copying a jobs URL that includes a path on a different subdomain; typos like .eightfold.com lookalike hosts; tests asserting the guard fires for hostile hostnames (SSRF probing).
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
- Access denied: Localhost or internal domain target detected.
- agentic-jobs: untrusted hostname
- feishu-jobs: careers_url must use HTTPS on…
- flowxtra: invalid URL
- landingjobs: untrusted hostname
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/c4e525f55b05f12c.
Report an issue: GitHub.
Appendix: source
Thrown at providers/eightfold.mjs:73
const RETRY_POLICY = { retries: 3, baseDelayMs: 500, maxDelayMs: 8_000 };
/**
* SSRF guard — every request URL passes through here before it is fetched.
*
* @param {string} url
* @returns {string} the same URL, when it is a trusted Eightfold endpoint.
*/
function assertEightfoldUrl(url) {
let parsed;
try {
parsed = new URL(url);
} catch {
throw new Error(`eightfold: invalid URL: ${url}`);
}
if (parsed.protocol !== 'https:') throw new Error(`eightfold: URL must use HTTPS: ${url}`);
if (!EIGHTFOLD_HOST_RE.test(parsed.hostname)) {
throw new Error(`eightfold: untrusted hostname "${parsed.hostname}" — must match *.eightfold.ai`);
}
return url;
}
/** @param {number} ms @param {any} ctx */
function sleep(ms, ctx) {
if (typeof ctx?.sleep === 'function') return ctx.sleep(ms);
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Eightfold reports timestamps as epoch SECONDS (`t_create`, `t_update`), not
* the ISO strings every other provider gets. Converted here; anything
* non-finite or non-positive is dropped rather than guessed at.
*
* @param {unknown} value
* @returns {number|undefined} epoch ms, or undefined.
*/View on GitHub (pinned to aac998c7ed)