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
- Set the URL host to exactly careerviet.vn (no www, no subdomain, no trailing dot), e.g. https://careerviet.vn/viec-lam
- Do not route third-party/mirror URLs through this provider — it fetches only the official board; use a generic provider for aggregates
- If validating redirect outcomes yourself, compare parsed.hostname === 'careerviet.vn' before trusting the response
- 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
- Use exactly careerviet.vn as the hostname — strict equality, so no www. and no trailing dot
- Never route aggregator or mirror URLs through this provider; it fetches only the official board
- If validating redirect outcomes, compare parsed.hostname === 'careerviet.vn' before trusting responses
- Mock at the network layer for tests instead of pointing the provider at a different host
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
- breezy: untrusted hostname "${parsed.hostname}" — must match
- refusing to archive restricted destination: ${preGuard.reaso
- Invalid or blocked URL: ${rejected.reason}
- Access denied: Localhost or internal domain target detected.
- 4dayweek: untrusted hostname "${parsed.hostname}" — must be
AI-assisted analysis of santifer/career-ops@1696bec4d0 (2026-09-01).
Data as JSON: /api/errors/d7f8862257a47b5d.
Report an issue: GitHub.