santifer/career-ops · error · Error

careerviet: untrusted hostname "${parsed.hostname}" — must b

Error message

careerviet: untrusted hostname "${parsed.hostname}" — must be ${TRUSTED_HOST}

What it means

CareerViet allows exactly one hostname: the TRUSTED_HOST constant 'careerviet.vn'. When the parsed URL is https but its hostname differs in any way, this error throws. Unlike the tenant-subdomain providers, there is no regex allowance for subdomains — www.careerviet.vn, any mirror, or a lookalike domain is rejected. This is the provider's SSRF/anti-mirror guard: fetches can only ever land on the genuine board.

Source

Thrown at providers/careerviet.mjs:110

const UPDATED_DATE_RE = /Cập nhật(?:<!--[\s\S]*?-->)?\s*:?\s*(?:<\/span>)?\s*<time>([\d/-]+)<\/time>/i;

/** @param {any} ctx @param {number} ms */
function sleep(ctx, ms) {
  if (typeof ctx?.sleep === 'function') return ctx.sleep(ms);
  return new Promise((r) => setTimeout(r, ms));
}

/** @param {string} url */
function assertCareerVietUrl(url) {
  let parsed;
  try {
    parsed = new URL(url);
  } catch {
    throw new Error(`careerviet: invalid URL: ${url}`);
  }
  if (parsed.protocol !== 'https:') throw new Error(`careerviet: URL must use HTTPS: ${url}`);
  if (parsed.hostname !== TRUSTED_HOST) {
    throw new Error(`careerviet: untrusted hostname "${parsed.hostname}" — must be ${TRUSTED_HOST}`);
  }
  return url;
}

/**
 * Collapse a markup fragment to its visible text.
 * @param {string} fragment
 * @returns {string}
 */
export function visibleText(fragment) {
  return decodeEntities(
    String(fragment ?? '')
      .replace(/<!--[\s\S]*?-->/g, ' ')
      .replace(/<[^>]+>/g, ' '),
  )
    .replace(/\s+/g, ' ')
    .trim();
}

View on GitHub (pinned to 1696bec4d0)

Solutions

  1. Set the URL host to exactly careerviet.vn (no www, no subdomain, no trailing dot), e.g. https://careerviet.vn/viec-lam
  2. Do not route third-party/mirror URLs through this provider — it fetches only the official board; use a generic provider for aggregates
  3. If validating redirect outcomes yourself, compare parsed.hostname === 'careerviet.vn' before trusting the response
  4. For local testing, mock at the HTTP layer (fetchJson/fetchText) rather than altering the hostname

Example fix

// before
careers_url: https://www.careerviet.vn/viec-lam
// after
careers_url: https://careerviet.vn/viec-lam
Defensive patterns

Strategy: validation

Validate before calling

function isTrustedCareervietUrl(u) {
  try {
    const p = new URL(u);
    return p.protocol === 'https:' && p.hostname === 'careerviet.vn';
  } catch { return false; }
}
// before fetch: if (!isTrustedCareervietUrl(entry.careers_url)) skipEntry(entry);

Type guard

function asCareervietUrl(value) {
  if (typeof value !== 'string') return null;
  try {
    const p = new URL(value);
    if (p.protocol !== 'https:' || p.hostname !== 'careerviet.vn') return null;
    return p;
  } catch { return null; }
}

Try / catch

try {
  await provider.fetch(entry, ctx);
} catch (err) {
  if (String(err.message).includes('untrusted hostname')) {
    console.error(`${entry.name}: only the exact host careerviet.vn is allowed — no www, subdomains, or mirrors`, err.message);
  } else throw err;
}

Prevention

When it happens

Trigger: Entry configured with a vanity domain, a country mirror, www. prefix, or an entirely different host routed to the careerviet provider; calling assertCareerVietUrl on a redirect target or a crafted URL like https://careerviet.vn.evil.com; hostname with trailing dot (careerviet.vn.) which fails strict equality.

Common situations: Pointing the provider at a third-party aggregator URL that happens to list CareerViet jobs; testing against a localhost/staging mirror; accidentally including a path-host mix-up; a scraper that followed a redirect off-domain and validates the result.

Related errors


AI-assisted analysis of santifer/career-ops@1696bec4d0 (2026-09-01). Data as JSON: /api/errors/d7f8862257a47b5d. Report an issue: GitHub.