santifer/career-ops · error
refusing to archive restricted destination: ${preGuard.reaso
Error message
refusing to archive restricted destination: ${preGuard.reason} What it means
Thrown by archiveUrl() in archive-posting.mjs before any Playwright navigation starts. It runs rejectPrivateOrInvalid() (exported from liveness-browser.mjs) — the repo's SSRF/egress guard — which rejects (a) unparseable URLs, (b) non-http(s) protocols, and (c) hosts matching private/loopback ranges: localhost, 0.0.0.0, 127/8, 10/8, 172.16-31, 192.168/16, 169.254/16, ::1, ::, fc00::/7 (fc..), link-local fe80::, including ::ffff:-mapped IPv4 forms. The appended reason is one of: 'invalid URL', 'unsupported protocol <p>', 'blocked host <h>'.
Source
Thrown at archive-posting.mjs:310
try {
await validateUrlSecurity(requestUrl);
return route.continue();
} catch (err) {
console.warn(` Blocked request to restricted destination (DNS): ${requestUrl} - ${err.message}`);
return route.abort('blockedbyclient');
}
});
}
export async function archiveUrl(browser, url, { company: companyHint, role: roleHint } = {}) {
console.log(`\n🔗 ${url}`);
// Refuse before launching any navigation, so an obviously-internal target
// never reaches Playwright at all.
const preGuard = rejectPrivateOrInvalid(url);
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}`);
}View on GitHub (pinned to 60398d6549)
Solutions
- Archive the public https:// URL of the posting instead of an internal mirror
- If the URL lost its scheme, re-add it: https://<host>/<path> — scheme-less strings fail URL parsing outright
- For genuinely internal portals the guard is by design: save the page manually (print to PDF) into jds/ instead of routing it through archive-posting
- Never 'fix' this by pointing at a public DNS name that resolves to a private IP — the DNS-level egress guard will still block it
Example fix
# before node archive-posting.mjs http://localhost:3000/jobs/123 # after node archive-posting.mjs https://boards.greenhouse.io/acme/jobs/123
Defensive patterns
Strategy: validation
Validate before calling
import { rejectPrivateOrInvalid } from './liveness-browser.mjs';
function assertPublicHttpUrl(url) {
const rejected = rejectPrivateOrInvalid(url);
if (rejected) throw new Error(`URL refused (${rejected.code}): ${url}`);
}
assertPublicHttpUrl(url);
await archiveUrl(browser, url); Type guard
function isArchivableUrl(url) {
try { return rejectPrivateOrInvalid(url) === null; } catch { return false; }
} Prevention
- Always include the scheme (https://) — scheme-less strings fail URL parsing before any network call
- Pre-screen URL lists with the exported rejectPrivateOrInvalid() to report all offenders at once instead of failing one by one
- Treat the refusal as correct behavior: internal targets must be archived manually, never by widening the guard
When it happens
Trigger: `node archive-posting.mjs http://10.0.0.7/jobs/1` or any RFC1918/link-local/localhost target; ftp:// or file:// URLs; a pasted URL missing its scheme ('boards.greenhouse.io/jobs/123' fails new URL() → 'invalid URL').
Common situations: Testing against a locally running ATS or a staging instance on an internal network; intranet job boards that are private by nature; stripping the https:// prefix while pasting; corporate portals behind link-local addresses.
Related errors
- refusing to archive restricted destination after redirect: $
- Invalid or blocked URL: ${rejected.reason}
- Invalid or blocked URL after redirect: ${finalRejected.reaso
- Access denied: Egress guard blocked private target IP ${ip}
- plugin egress to ${hostname} is blocked (private/loopback/me
AI-assisted analysis of santifer/career-ops@60398d6549 (2026-08-20).
Data as JSON: /api/errors/9928ffc8581a53ce.
Report an issue: GitHub.