santifer/career-ops · error · Error

plugin egress: cannot resolve

Error message

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

What it means

For non-IP-literal hostnames, resolveAndValidate performs a DNS lookup (dnsLookup with all: true) before checking addresses. If resolution itself fails, the original error message is wrapped and rethrown as this error. DNS failure means the egress guard cannot verify the destination, so the request is refused rather than passed through unresolved.

Solutions

  1. Check network/DNS connectivity (`ping`/`nslookup <hostname>`) and fix the resolver or reconnect before retrying.
  2. Correct the hostname in the plugin configuration if it is a typo.
  3. Connect the VPN or network on which the hostname resolves, if it is an internal-only name.
  4. Retry on transient failures — wrap the call in retry with backoff since DNS outages are often momentary.
  5. If the name is stable and known, use its public IP literal (which skips DNS) — but note it must not be in a blocked range.

Example fix

// before: single attempt dies on transient DNS failure
const addrs = await resolveAndValidate(hostname);

// after: retry a few times with backoff
let addrs;
for (let i = 0; i < 3; i++) {
  try { addrs = await resolveAndValidate(hostname); break; }
  catch (e) {
    if (!e.message.startsWith("plugin egress: cannot resolve") || i === 2) throw e;
    await new Promise(r => setTimeout(r, 500 * 2 ** i));
  }
}
Defensive patterns

Strategy: retry

Validate before calling

function isProbablyResolvable(hostname) {
  return typeof hostname === 'string' && hostname.length > 0 && !hostname.includes(' ') && !isIP(hostname);
}

Try / catch

async function resolveWithRetry(hostname, opts, attempts = 3) {
  for (let i = 0; i < attempts; i++) {
    try {
      return await resolveAndValidate(hostname, opts);
    } catch (err) {
      const isDnsFail = err.message.startsWith('plugin egress: cannot resolve ');
      if (!isDnsFail || i === attempts - 1) throw err;
      await new Promise(r => setTimeout(r, 500 * 2 ** i));
    }
  }
}

Prevention

When it happens

Trigger: Calling resolveAndValidate(hostname) where hostname has no DNS record (NXDOMAIN), the machine has no network/DNS connectivity, DNS times out, /etc/resolv.conf is broken, or the hostname is malformed so the resolver rejects it.

Common situations: Offline laptop or CI runner without network access; typo'd API hostname in plugin config; corporate DNS blocking the domain; hostname only resolvable on a VPN that is not connected; transient DNS server outage.

Related errors


AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16). Data as JSON: /api/errors/467f2fb0ad608e63. Report an issue: GitHub.

Appendix: 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 aac998c7ed)