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

  1. 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.
  2. 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.
  3. 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.
  4. 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

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


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)