santifer/career-ops · warning
Blocked request to restricted destination (DNS)
Error message
Blocked request to restricted destination (DNS): ${requestUrl} - ${err.message} What it means
This message is emitted by installEgressGuard, a Playwright route interceptor installed on the archival browser context. Before every outgoing request the guard calls validateUrlSecurity(requestUrl); if that check throws (typically because DNS resolution of the destination is disallowed, points at a restricted/private address, or the host is otherwise not permitted), the request is logged with this warning and aborted with 'blockedbyclient'. It is an intentional security guard, not a malfunction — the code is protecting the scraping browser from SSRF-style access to internal or blocked destinations while archiving job postings.
Solutions
- Inspect the URL in the warning: if it is a redirect target or sub-resource (font/tracker/analytics), it can be safely ignored — only the primary posting URL needs to be reachable.
- If the primary posting URL itself is blocked, verify the domain resolves: run `dig <host>` or `nslookup <host>` to confirm DNS works from this machine.
- If the host is legitimate but restricted, update the allow/deny configuration consumed by validateUrlSecurity (or its resolver policy) to permit the destination, then retry the archive run.
- If the URL points at internal infrastructure (localhost, 10.x, 192.168.x, 169.254.x), use the public externally-resolvable URL instead — the guard intentionally blocks private destinations.
Example fix
// before: archiving an internal/staging posting URL await archiveUrl(browser, 'http://staging.internal.company.com/jobs/123'); // warn: Blocked request to restricted destination (DNS) // after: use the public, externally resolvable posting URL await archiveUrl(browser, 'https://company.com/careers/jobs/123');
Defensive patterns
Strategy: validation
Validate before calling
import { validateUrlSecurity } from './archive-posting.mjs';
// Check before invoking the archive flow:
try {
await validateUrlSecurity(url);
} catch (err) {
console.error(`Refusing to archive unreachable/restricted URL ${url}: ${err.message}`);
return; // skip this URL instead of letting the route be aborted mid-scrape
} Type guard
function isPublicHttpUrl(url) {
try {
const u = new URL(url);
return (u.protocol === 'http:' || u.protocol === 'https:') &&
!/^(localhost|127\.|10\.|192\.168\.|169\.254\.|172\.(1[6-9]|2\d|3[01])\.)/.test(u.hostname) &&
!u.hostname.endsWith('.internal');
} catch { return false; }
} Try / catch
try {
await archiveUrl(browser, url, { company, role });
} catch (err) {
if (err.message.includes('blockedbyclient') || /restricted destination/i.test(err.message)) {
console.warn(`Skipping ${url}: blocked by egress guard — verify the domain is public and resolves.`);
} else {
throw err;
}
} Prevention
- Pre-validate every URL with validateUrlSecurity before feeding it to archiveUrl so blocked destinations fail fast with a clear message.
- Only archive externally resolvable public URLs; never point the archiver at localhost or internal staging hosts.
- Check company careers URLs for typos before batch runs; a dead domain wastes a browser session.
- Treat 'Blocked request' warnings for sub-resources (trackers, fonts) as expected noise and filter them in log review.
When it happens
Trigger: Calling archiveUrl() (or any code that installs the egress guard) and having the browser attempt to load a URL whose host fails validateUrlSecurity: a hostname that does not resolve, resolves to a private/loopback/link-local address, an IP-literal pointing at internal infrastructure, or a host on the configured deny list. The route.continue() path is skipped and route.abort('blockedbyclient') fires instead, printing this warn.
Common situations: A job posting URL redirects to or pulls resources from a blocked host (trackers, CDNs on the deny list, internal staging URLs); a JD link contains a dead domain whose DNS lookup fails; a developer tests against localhost or an internal staging server while running the archiver; a typo in a company careers URL (e.g. 'greenhous.xyz') causes resolution failure; running in an environment where DNS for the target host is not available.
Related errors
- plugin egress: resolves to a blocked address ( ) — possible…
- Access denied: Egress guard blocked private target IP
- Blocked request to restricted destination (DNS)
- Blocked request to restricted destination
- plugin egress: resolved to no addresses
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/831e78bf354b7bb9.
Report an issue: GitHub.
Appendix: source
Thrown at archive-posting.mjs:313
* catches a public hostname resolving into private space.
*
* @param {import('playwright').BrowserContext} context - Context to guard.
*/
export async function installEgressGuard(context) {
await context.route('**/*', async (route) => {
const requestUrl = route.request().url();
const verdict = rejectPrivateOrInvalid(requestUrl);
if (verdict) {
console.warn(` Blocked request to restricted destination: ${requestUrl} (${verdict.reason})`);
return route.abort('blockedbyclient');
}
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();View on GitHub (pinned to aac998c7ed)