santifer/career-ops · error · Error

plugin egress: resolved to no addresses

Error message

plugin egress: ${hostname} resolved to no addresses

What it means

The plugin egress guard resolves the requested hostname via DNS (dnsLookup with all:true) before any socket is opened, to enforce an SSRF blocklist on the resolved IPs. This error is thrown when the resolver succeeds but returns an empty address list, meaning the name is technically resolvable per the resolver but yields no usable addresses. It indicates the hostname cannot be connected to, so the request is refused before dialing.

Solutions

  1. Verify the hostname is correct and actually has A/AAAA records: run `node -e "require('dns').promises.lookup('<host>',{all:true}).then(console.log)"` or `dig <host> A +short`.
  2. Fix the plugin/config hostname value — typos or leftover placeholder hosts are the most common cause.
  3. Check /etc/hosts and the container's DNS setup for entries that map the name to an empty value.
  4. If DNS is intentionally empty but you connect by IP anyway, pass a literal IP instead of a hostname (literal IPs skip resolution).

Example fix

// before
await pluginFetch('https://api.internal.example/v1/runs');
// after (host has no DNS records; use the configured literal IP)
await pluginFetch('https://10.20.0.5/v1/runs');
Defensive patterns

Strategy: validation

Validate before calling

const dns = require('dns').promises;
async function hostnameResolvable(host) {
  if (/^\d{1,3}(\.\d{1,3}){3}$/.test(host)) return true; // literal IP skips DNS
  const addrs = await dns.lookup(host, { all: true });
  return Array.isArray(addrs) && addrs.length > 0;
}

Type guard

function isResolvableHost(host) {
  return typeof host === 'string' && host.length > 0 &&
    !/\s/.test(host);
}

Try / catch

try {
  const res = await pluginFetch(url);
} catch (err) {
  if (String(err.message).includes('resolved to no addresses')) {
    // fall back to alternate host or surface a config error
    return alternateFetch(url);
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling any plugin network helper (e.g. fetchJson via the egress wrapper in plugins/_net.mjs) with a hostname whose DNS lookup returns an empty array — e.g. a name with only exotic/unsupported record types, an empty hosts-file entry, or a resolver returning zero A/AAAA records.

Common situations: Typo'd or placeholder hostnames in plugin config (e.g. 'api.example.invalid' configured with an empty record); corporate DNS or /etc/hosts entries mapping a name to nothing; split-horizon DNS returning no addresses from inside a container; IPv6-only environments where a lookup mode yields no records.

Related errors


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

Appendix: source

Thrown at plugins/_net.mjs:97

    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)