santifer/career-ops · error · Error

recruitee: untrusted hostname

Error message

recruitee: untrusted hostname "${parsed.hostname}" — must match <slug>.recruitee.com

What it means

The Recruitee API may only be called on <slug>.recruitee.com hosts; this is the provider's SSRF defence, since the fetched URL is server-side requested. Any hostname not matching the /^\[a-z0-9\]\[a-z0-9-\]*\.recruitee\.com$/ regex (custom domains, lookalike hosts, bare domains) is rejected.

Solutions

  1. Resolve the custom domain to its underlying <slug>.recruitee.com host and configure that as careers_url
  2. Check the DNS/CNAME of the custom domain — it usually aliases <slug>.recruitee.com
  3. Ensure the slug uses only lowercase letters, digits and hyphens and is a single label under recruitee.com
  4. If the board is genuinely not on recruitee.com, this provider does not apply — use the correct provider

Example fix

// before
careers_url: https://careers.hostaway.com
// after
careers_url: https://hostaway.recruitee.com
Defensive patterns

Strategy: validation

Validate before calling

const RE = /^[a-z0-9][a-z0-9-]*\.recruitee\.com$/;
function isRecruiteeHost(s) {
  try { return RE.test(new URL(s).hostname); } catch { return false; }
}
if (!isRecruiteeHost(entry.careers_url)) throw new Error(`Resolve custom domain to <slug>.recruitee.com for ${entry.name}`);

Type guard

function isRecruiteeTenantUrl(v) {
  try {
    const u = new URL(String(v));
    return u.protocol === 'https:' && /^[a-z0-9][a-z0-9-]*\.recruitee\.com$/.test(u.hostname);
  } catch { return false; }
}

Try / catch

try {
  assertRecruiteeUrl(entry.careers_url);
} catch (err) {
  const m = err.message.match(/untrusted hostname "([^"]+)"/);
  if (m) console.error(`Host ${m[1]} is not *.recruitee.com — resolve its CNAME to find the tenant slug`);
  throw err;
}

Prevention

When it happens

Trigger: A careers_url pointing at a custom careers domain (careers.example.com), a different tenant host, a subdomain of recruitee.com deeper than one label, or uppercase/invalid slug characters reaches assertRecruiteeUrl.

Common situations: A company serves its Recruitee board behind its own domain (very common — e.g. careers.hostaway.com) while the config stores only that custom URL; someone configures a proxy or mirror host; a typo like acme.recruitee.com.evil.io.

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


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

Appendix: source

Thrown at providers/recruitee.mjs:23

// Auto-detects from careers_url pattern `https://<slug>.recruitee.com`.
// Per-tenant subdomains are the variable part — SSRF defence uses a
// regex match on `<safe-slug>.recruitee.com` rather than a static
// allowlist.

import { htmlToText } from './_html-to-text.mjs';

const RECRUITEE_HOST_RE = /^[a-z0-9][a-z0-9-]*\.recruitee\.com$/;

function assertRecruiteeUrl(url) {
  let parsed;
  try {
    parsed = new URL(url);
  } catch {
    throw new Error(`recruitee: invalid URL: ${url}`);
  }
  if (parsed.protocol !== 'https:') throw new Error(`recruitee: URL must use HTTPS: ${url}`);
  if (!RECRUITEE_HOST_RE.test(parsed.hostname)) {
    throw new Error(`recruitee: untrusted hostname "${parsed.hostname}" — must match <slug>.recruitee.com`);
  }
  return url;
}

function resolveApiUrl(entry) {
  const raw = typeof entry.careers_url === 'string' ? entry.careers_url : '';
  if (!raw) return null;
  let parsed;
  try {
    parsed = new URL(raw);
  } catch {
    return null;
  }
  if (parsed.protocol !== 'https:') return null;
  if (!RECRUITEE_HOST_RE.test(parsed.hostname)) return null;
  return `https://${parsed.hostname}/api/offers/`;
}

View on GitHub (pinned to aac998c7ed)