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
- Verify the hostname resolves from the same machine: `nslookup <hostname>` or `getent hosts <hostname>`.
- Fix typos in portals.yml / plugin URLs.
- If DNS is transiently failing (EAI_AGAIN), retry the scan; consider retry-with-backoff at the caller.
- 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
- Validate hostnames resolve in a pre-scan config check.
- Treat EAI_AGAIN as transient — retry with backoff, but fail on ENOTFOUND.
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
- plugin egress: ${hostname} resolved to no addresses
- plugin egress: ${hostname} resolves to a blocked address (${
- plugin egress to ${hostname} is blocked (private/loopback/me
- DNS resolution returned no addresses for ${hostname}
- [models] Failed to fetch free model list: ${reason}. Check t
AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13).
Data as JSON: /api/errors/467f2fb0ad608e63.
Report an issue: GitHub.