JuliusBrussee/caveman · error
ssrf: DNS resolution failed for %q: %w
Error message
ssrf: DNS resolution failed for %q: %w
What it means
Pre-flight DNS failure in ssrf.ValidateURL: net.DefaultResolver.LookupIPAddr returned an error for the hostname. The original resolver error is wrapped with %w, so the cause (NXDOMAIN, timeout, SERVFAIL) is visible in the chain.
Source
Thrown at shared/platform/ssrf/ssrf.go:220
// If host is an IP literal, check it directly without a DNS round-trip.
if addr, err := netip.ParseAddr(host); err == nil {
return checkAddr(addr, host, port, cfg)
}
// "localhost" is explicitly blocked regardless of what DNS says — unless a
// self-hosted operator allowlisted it (resolution still runs, so every
// resolved address is range-checked below like any other).
if strings.EqualFold(host, "localhost") && !(!cfg.ManagedMode && isInAllowList(host, port, cfg.AllowList)) {
return fmt.Errorf("ssrf: host %q is blocked (loopback)", host)
}
// Resolve ALL addresses the hostname currently maps to. A hostname that
// returns even one blocked address is rejected (defense-in-depth against
// split-horizon / DNS rebinding scenarios where the pre-flight check and
// the dial see different answers).
addrs, err := net.DefaultResolver.LookupIPAddr(ctx, host)
if err != nil {
return fmt.Errorf("ssrf: DNS resolution failed for %q: %w", host, err)
}
if len(addrs) == 0 {
return fmt.Errorf("ssrf: host %q resolved to no addresses", host)
}
for _, ia := range addrs {
a, ok := netip.AddrFromSlice(ia.IP)
if !ok {
return fmt.Errorf("ssrf: could not parse resolved IP %v for host %q", ia.IP, host)
}
a = a.Unmap() // normalise ::ffff:x.x.x.x → x.x.x.x
if err := checkAddr(a, host, port, cfg); err != nil {
return err
}
}
return nil
}
View on GitHub (pinned to 27d5a3981a)
Solutions
- dig/host the name from the same container/host to confirm it resolves: `dig +short example.com`.
- Fix the typo or use the canonical hostname from the provider docs.
- If the resolver is the problem, repair container DNS (resolv.conf, cluster DNS) or add the host to a local zone — do not bypass the SSRF check.
- Retry once for transient resolver timeouts before surfacing the error to the user.
Defensive patterns
Strategy: retry
Validate before calling
if _, err := net.DefaultResolver.LookupIPAddr(ctx, u.Hostname()); err != nil {
return fmt.Errorf("host does not resolve: %w", err) // fail before building the request
} Type guard
func isDNSFailure(err error) bool {
var dnsErr *net.DNSError
return errors.As(err, &dnsErr)
} Try / catch
err := ssrf.ValidateURL(ctx, raw, cfg)
if err != nil {
var dnsErr *net.DNSError
if errors.As(err, &dnsErr) && dnsErr.IsTimeout && !dnsErr.IsNotFound {
// transient resolver issue: retry once
} else if dnsErr != nil && dnsErr.IsNotFound {
// permanent: bad hostname, surface to user
}
} Prevention
- Validate hostnames at config save time so users see DNS errors immediately.
- Distinguish NXDOMAIN (permanent) from timeouts (transient) before retrying.
When it happens
Trigger: Calling ssrf.ValidateURL with a hostname that does not exist (NXDOMAIN), a resolver that times out, or a host with only a malformed/unresolvable record. IP-literal hosts skip this path entirely.
Common situations: Typo'd domains in webhook config; recently-expired or not-yet-propagated DNS records; broken /etc/resolv.conf or blocked DNS egress in containers; split-horizon DNS where the name only resolves inside another network.
Understand the failure class
- DNS resolution errors: ENOTFOUND and getaddrinfo failures — how hostname lookups fail and how to debug them.
Related errors
- ssrf: host %q resolved to no addresses
- ssrf: could not parse resolved IP %v for host %q
- ssrf: scheme %q not permitted (managed mode requires https)
- ssrf: host %q is blocked (loopback)
- ssrf: destination %s (for host %q) is in blocked range %s
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/5856e8b1cbf6ee63.
Report an issue: GitHub.