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

  1. Ensure only URLs on api.smartrecruiters.com are passed to the validator — derive them via buildPostingsUrl(slug)
  2. For a branded careers page, keep careers_url as-is and add api: 'https://careers.smartrecruiters.com/<slug>' so resolveSlug can pin the slug
  3. Fix typos in the hostname and re-run
  4. 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

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


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)