santifer/career-ops · error

refusing to archive restricted destination after redirect: $

Error message

refusing to archive restricted destination after redirect: ${postGuard.reason}

What it means

Thrown by archiveUrl() after page.goto() settles: it re-runs rejectPrivateOrInvalid() on page.url(), the URL actually landed on. This is defence-in-depth behind the per-request route guard (which aborts private redirect hops as 'blockedbyclient') — a public first-hop URL that ends up on a private/loopback/invalid destination is rejected before any content is saved.

Source

Thrown at archive-posting.mjs:327

  if (preGuard) {
    throw new Error(`refusing to archive restricted destination: ${preGuard.reason}`);
  }

  const context = await browser.newContext();
  await installEgressGuard(context);
  const page = await context.newPage();

  try {
    const response = await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
    const httpStatus = response?.status() ?? 0;

    // Re-check where we actually landed. The route guard already inspects every
    // redirect hop, so this is defence-in-depth: a first-hop-only check is the
    // classic miss here, and asserting on the settled URL costs nothing.
    const landedUrl = page.url();
    const postGuard = rejectPrivateOrInvalid(landedUrl);
    if (postGuard) {
      throw new Error(`refusing to archive restricted destination after redirect: ${postGuard.reason}`);
    }

    // Give SPAs (Ashby, Lever, Workday) time to hydrate
    await page.waitForTimeout(2000);

    const pageTitle = await page.title();
    const h1Text = await page.$eval('h1', el => el.innerText.trim()).catch(() => '');
    const urlCompany = extractCompanyFromUrl(url);

    // Parse page title first — it usually has "Role | Company" or "Company | Role".
    // Fall back to h1 for the role when the page title doesn't yield one cleanly.
    const detected = parsePageTitle(pageTitle);
    const resolvedCompany = overrideCompany || companyHint || detected.company || urlCompany || 'unknown';
    const resolvedRole = overrideRole || roleHint || detected.role || h1Text || 'job';

    // Strip noisy prefixes common on Greenhouse/Lever ("Job Application for …")
    const company = resolvedCompany.replace(/^job\s+application\s+for\s+/i, '').trim();
    const role = resolvedRole.replace(/^job\s+application\s+for\s+/i, '').trim();

View on GitHub (pinned to 60398d6549)

Solutions

  1. Trace the chain yourself (curl -sIL <url>) and pass the final public URL directly to archive-posting
  2. If the final destination is legitimately internal, archive it manually into jds/ — the guard is intentional and not bypassable via this tool
  3. If a public posting unexpectedly redirects to a private host, treat the link as broken/untrusted, drop it from the pipeline, and note it in the tracker

Example fix

# before
node archive-posting.mjs https://short.example.com/j/9317   # 302 -> http://10.2.3.4/ats/9317

# after
curl -sIL https://short.example.com/j/9317 | grep -i '^location'
node archive-posting.mjs https://ats.acme-public.com/careers/9317   # archive the real public destination
Defensive patterns

Strategy: try-catch

Try / catch

try {
  await archiveUrl(browser, url);
} catch (e) {
  if (/restricted destination after redirect/.test(e.message)) {
    // redirect target unknowable beforehand: resolve the chain out-of-band,
    // archive the final PUBLIC url, or fall back to a manual capture.
    console.warn(`redirect guard: ${url} -> private destination; skipping`);
    continue; // next queue entry
  }
  throw e;
}

Prevention

When it happens

Trigger: A public job URL that 30x-redirects to an internal host (open redirect, vendor SSO bouncing to an intranet FQDN that matches a private pattern, or a misconfigured shortlink); any case where the settled URL differs from the requested one and lands on a blocked host.

Common situations: ATS links that bounce through an SSO domain resolving to private space; a posting service redirecting to an internal staging host; tampered or rotting shortlinks in an old pipeline.md queue.

Related errors


AI-assisted analysis of santifer/career-ops@60398d6549 (2026-08-20). Data as JSON: /api/errors/88d131c2a38875db. Report an issue: GitHub.