santifer/career-ops · error · Error

wttj: untrusted hostname " " — must be

Error message

wttj: untrusted ${label} hostname "${parsed.hostname}" — must be ${host}

What it means

assertHost's final check compares parsed.hostname (case-insensitively) against the exact expected host parameter. Any other hostname — a lookalike domain, a redirect target, or a typo — is rejected to keep fetches pinned to the legitimate WTTJ/ATS host.

Solutions

  1. Correct the URL to use the expected host named in the error message
  2. Check for typos or appended domains in the hostname
  3. If you passed the wrong host constant to assertHost, fix the call site

Example fix

// before
assertHost('https://jobs.welcome-to-the-jungle.com/...', 'www.wttj.fr', 'env');
// after
assertHost('https://www.wttj.fr/api/env', 'www.wttj.fr', 'env');
Defensive patterns

Strategy: validation

Validate before calling

function hostIs(u, expected) { try { return new URL(u).hostname === expected.toLowerCase(); } catch { return false; } }
if (!hostIs(entry.careers_url, 'www.wttj.fr')) throw new Error(`unexpected host in ${entry.careers_url}`);

Type guard

const hostIs = (u, expected) => { try { return new URL(u).hostname === expected.toLowerCase(); } catch { return false; } };

Try / catch

try { return await wttj.fetch(entry, ctx); } catch (e) { if (e.message.includes('untrusted')) { console.warn(`${entry.name}: hostname not pinned to ${e.message.match(/must be (\S+)/)?.[1]}`); return []; } throw e; }

Prevention

When it happens

Trigger: assertHost(url, host, label) where url parses to https but hostname differs from host, e.g. assertHost('https://evil.io/api/env', 'www.wttj.fr', 'env') or 'https://www.wttj.fr.evil.io/api/env'.

Common situations: A careers_url pointing at the company site rather than the WTTJ board domain; an attacker-shaped host in config; passing the wrong host argument order to assertHost.

Related errors


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

Appendix: source

Thrown at providers/wttj.mjs:67

// manager" alone returns ~14k hits — so Algolia's own relevance ranking, not the
// scanner's filters, decides which 200 are seen. A server-side `filters`
// expression cuts the result set to something a single request can actually
// exhaust (e.g. product-management + France + full_time is ~450), so the cap is
// raised to Algolia's per-request ceiling for this index when one is configured.
const FILTERED_MAX_HITS_CAP = 1000;
const FILTERS_MAX_LEN = 1000;

/** Pin a URL to an expected https host. */
function assertHost(url, host, label) {
  let parsed;
  try {
    parsed = new URL(url);
  } catch {
    throw new Error(`wttj: invalid URL: ${url}`);
  }
  if (parsed.protocol !== 'https:') throw new Error(`wttj: URL must use HTTPS: ${url}`);
  if (parsed.hostname !== host.toLowerCase()) {
    throw new Error(`wttj: untrusted ${label} hostname "${parsed.hostname}" — must be ${host}`);
  }
  return url;
}

/**
 * Parse the `window.env = {...}` payload served by /api/env and extract the
 * Algolia application id + client search key.
 * @param {string} text
 * @returns {{ appId: string, apiKey: string }}
 */
export function parseEnvPayload(text) {
  const start = text.indexOf('{');
  const end = text.lastIndexOf('}');
  if (start === -1 || end <= start) throw new Error('wttj: /api/env payload has no JSON object');
  let env;
  try {
    env = JSON.parse(text.slice(start, end + 1));
  } catch {

View on GitHub (pinned to aac998c7ed)