{"record":{"id":"467f2fb0ad608e63","repo":"santifer/career-ops","slug":"plugin-egress-cannot-resolve-hostname-err","errorCode":null,"errorMessage":"plugin egress: cannot resolve ${hostname} — ${err.message}","messagePattern":"plugin egress: cannot resolve (.+?) — (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"plugins/_net.mjs","lineNumber":95,"sourceCode":"  // An IP literal host: validate directly (no DNS).\n  if (isIP(hostname)) {\n    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":77,"sourceCodeEnd":112,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/plugins/_net.mjs#L77-L112","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":null,"handlingStrategy":"retry","validationCode":"import { lookup } from 'node:dns/promises';\n// Pre-resolve hostnames to fail fast on typos before a scan.\nasync function assertResolves(hostname) {\n  try {\n    await lookup(hostname, { all: true });\n  } catch {\n    throw new Error(`Config error: '${hostname}' does not resolve — check the spelling.`);\n  }\n}\nawait assertResolves(new URL(entry.url).hostname);","typeGuard":null,"tryCatchPattern":"async function safeResolve(hostname, retries = 2) {\n  for (let i = 0; i <= retries; i++) {\n    try { return await resolveAndValidate(hostname); }\n    catch (err) {\n      if (/cannot resolve/.test(err.message) && i < retries) {\n        await new Promise(r => setTimeout(r, 500 * (i + 1)));\n        continue;\n      }\n      throw err;\n    }\n  }\n}","preventionTips":["Validate hostnames resolve in a pre-scan config check.","Treat EAI_AGAIN as transient — retry with backoff, but fail on ENOTFOUND."],"tags":["network","dns","egress","plugin","configuration"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}