santifer/career-ops · error · Error

plugin egress: cannot resolve ${hostname} — ${err.message}

Error message

plugin egress: cannot resolve ${hostname} — ${err.message}

What it means

Thrown by `resolveAndValidate` (plugins/_net.mjs:95) when `dnsLookup` rejects for a non-IP hostname. Node's `node:dns/promises` lookup fails (e.g. ENOTFOUND, EAI_AGAIN) and the message is wrapped to name the hostname and the underlying resolver error. This is egress validation failing at the DNS step before any connection is made.

Source

Thrown at plugins/_net.mjs:95

  // An IP literal host: validate directly (no DNS).
  if (isIP(hostname)) {
    if (isBlockedIp(hostname)) {
      if (allowsLocalhost && isLoopbackLiteral(hostname)) return [hostname];
      throw new Error(`plugin egress to ${hostname} is blocked (private/loopback/metadata range)`);
    }
    return [hostname];
  }

  if (allowsLocalhost && LOOPBACK_HOSTS.has(hostname.toLowerCase())) {
    // Local-AI providers (Ollama/LM Studio). Resolve but allow loopback through.
    return ['127.0.0.1'];
  }

  let addrs;
  try {
    addrs = await dnsLookup(hostname, { all: true });
  } catch (err) {
    throw new Error(`plugin egress: cannot resolve ${hostname} — ${err.message}`);
  }
  if (!addrs.length) throw new Error(`plugin egress: ${hostname} resolved to no addresses`);
  for (const { address } of addrs) {
    if (isBlockedIp(address)) {
      if (allowsLocalhost && isLoopbackLiteral(address)) continue;
      throw new Error(`plugin egress: ${hostname} resolves to a blocked address (${address}) — possible SSRF/rebinding`);
    }
  }
  return addrs.map(a => a.address);
}

function isLoopbackLiteral(ip) {
  if (ip === '::1') return true;
  if (isIP(ip) === 4) return ip.split('.')[0] === '127';
  return false;
}

View on GitHub (pinned to 9b17a8ac97)

Solutions

  1. Verify the hostname resolves from the same machine: `nslookup <hostname>` or `getent hosts <hostname>`.
  2. Fix typos in portals.yml / plugin URLs.
  3. If DNS is transiently failing (EAI_AGAIN), retry the scan; consider retry-with-backoff at the caller.
  4. On an air-gapped/CI runner, ensure a resolver is configured (`/etc/resolv.conf`) or run only providers that use raw IPs/localhost.
Defensive patterns

Strategy: retry

Validate before calling

import { lookup } from 'node:dns/promises';
// Pre-resolve hostnames to fail fast on typos before a scan.
async function assertResolves(hostname) {
  try {
    await lookup(hostname, { all: true });
  } catch {
    throw new Error(`Config error: '${hostname}' does not resolve — check the spelling.`);
  }
}
await assertResolves(new URL(entry.url).hostname);

Try / catch

async function safeResolve(hostname, retries = 2) {
  for (let i = 0; i <= retries; i++) {
    try { return await resolveAndValidate(hostname); }
    catch (err) {
      if (/cannot resolve/.test(err.message) && i < retries) {
        await new Promise(r => setTimeout(r, 500 * (i + 1)));
        continue;
      }
      throw err;
    }
  }
}

Prevention

When it happens

Trigger: A plugin fetches a hostname that does not exist in DNS (typo, decommissioned domain), or DNS is temporarily unavailable (EAI_AGAIN), or the resolver is misconfigured. resolveAndValidate is called for every plugin fetch target that is a hostname (not a raw IP, not an opted-in loopback host).

Common situations: Typo in portals.yml hostname (e.g. `greehouse.io`); a provider domain changed or was retired; offline/air-gapped run with no DNS; a corporate DNS server returning SERVFAIL; transient network blip during a scan; an Airgapped CI runner with no resolver.

Related errors


AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13). Data as JSON: /api/errors/467f2fb0ad608e63. Report an issue: GitHub.