{"record":{"id":"60e1c1c98669199a","repo":"santifer/career-ops","slug":"plugin-egress-hostname-resolved-to-no-addresse","errorCode":null,"errorMessage":"plugin egress: ${hostname} resolved to no addresses","messagePattern":"plugin egress: (.+?) resolved to no addresses","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"plugins/_net.mjs","lineNumber":97,"sourceCode":"    if (isBlockedIp(hostname)) {\n      if (allowsLocalhost && isLoopbackLiteral(hostname)) return [hostname];\n      throw new Error(`plugin egress to ${hostname} is blocked (private/loopback/metadata range)`);\n    }\n    return [hostname];\n  }\n\n  if (allowsLocalhost && LOOPBACK_HOSTS.has(hostname.toLowerCase())) {\n    // Local-AI providers (Ollama/LM Studio). Resolve but allow loopback through.\n    return ['127.0.0.1'];\n  }\n\n  let addrs;\n  try {\n    addrs = await dnsLookup(hostname, { all: true });\n  } catch (err) {\n    throw new Error(`plugin egress: cannot resolve ${hostname} — ${err.message}`);\n  }\n  if (!addrs.length) throw new Error(`plugin egress: ${hostname} resolved to no addresses`);\n  for (const { address } of addrs) {\n    if (isBlockedIp(address)) {\n      if (allowsLocalhost && isLoopbackLiteral(address)) continue;\n      throw new Error(`plugin egress: ${hostname} resolves to a blocked address (${address}) — possible SSRF/rebinding`);\n    }\n  }\n  return addrs.map(a => a.address);\n}\n\nfunction isLoopbackLiteral(ip) {\n  if (ip === '::1') return true;\n  if (isIP(ip) === 4) return ip.split('.')[0] === '127';\n  return false;\n}\n","sourceCodeStart":79,"sourceCodeEnd":112,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/plugins/_net.mjs#L79-L112","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["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`.","Fix the plugin/config hostname value — typos or leftover placeholder hosts are the most common cause.","Check /etc/hosts and the container's DNS setup for entries that map the name to an empty value.","If DNS is intentionally empty but you connect by IP anyway, pass a literal IP instead of a hostname (literal IPs skip resolution)."],"exampleFix":"// before\nawait pluginFetch('https://api.internal.example/v1/runs');\n// after (host has no DNS records; use the configured literal IP)\nawait pluginFetch('https://10.20.0.5/v1/runs');","handlingStrategy":"validation","validationCode":"const dns = require('dns').promises;\nasync function hostnameResolvable(host) {\n  if (/^\\d{1,3}(\\.\\d{1,3}){3}$/.test(host)) return true; // literal IP skips DNS\n  const addrs = await dns.lookup(host, { all: true });\n  return Array.isArray(addrs) && addrs.length > 0;\n}","typeGuard":"function isResolvableHost(host) {\n  return typeof host === 'string' && host.length > 0 &&\n    !/\\s/.test(host);\n}","tryCatchPattern":"try {\n  const res = await pluginFetch(url);\n} catch (err) {\n  if (String(err.message).includes('resolved to no addresses')) {\n    // fall back to alternate host or surface a config error\n    return alternateFetch(url);\n  }\n  throw err;\n}","preventionTips":["Validate configured hostnames resolve at startup, not at request time.","Prefer literal IPs or well-known public endpoints over internal-only DNS names.","Check /etc/hosts and container DNS for empty/placeholder entries."],"tags":["network","dns","ssrf","egress"],"backgroundTag":"network-request-failed","analyzedSha":"aac998c7ed7248ea853b720ceeb1fdbeb322fc5d","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}