gofiber/fiber · error · ErrUpstreamHostBlocked

%w: %s has no usable address

Error message

%w: %s has no usable address

What it means

dialValidatedIPs walks the resolved IP list and skips IPv6 addresses when dialDualStack is false (mirroring fasthttp's IPv4-only default). If every candidate is skipped (IPv6-only host with dialDualStack=false) or the list was empty, this error is returned instead of a dial error. If dials were attempted but all failed, the last dial error is returned instead — so this specific message means no dial was even attempted.

Source

Thrown at middleware/proxy/security.go:492

// "no usable address" error is returned; otherwise the last dial error
// is propagated so the caller can see why each attempt failed.
//
//nolint:revive // dialDualStack mirrors fasthttp.HostClient.DialDualStack
func dialValidatedIPs(ips []net.IP, host, port string, dialDualStack bool, dial ssrfDialFunc) (net.Conn, error) {
	var lastErr error
	for _, ip := range ips {
		// Mirror fasthttp's default of IPv4-only unless DialDualStack.
		if !dialDualStack && ip.To4() == nil {
			continue
		}
		conn, derr := dial("tcp", net.JoinHostPort(ip.String(), port))
		if derr == nil {
			return conn, nil
		}
		lastErr = derr
	}
	if lastErr == nil {
		lastErr = fmt.Errorf("%w: %s has no usable address", ErrUpstreamHostBlocked, host)
	}
	return nil, lastErr
}

// installHostClientGuard fits hc with the policy-aware, dial-time SSRF
// guard. It is installed through fasthttp.Client.ConfigureClient, which
// runs once per HostClient at creation — so the guard is present before the
// first dial to that host and, crucially, covers BOTH dial code paths:
// fasthttp's callDialFunc prefers DialTimeout over Dial, so guarding only
// Dial would let a client that sets DialTimeout dial unvalidated. We wrap
// DialTimeout when present (preserving its per-request timeout) and always
// wrap Dial so the nil-DialTimeout and default-dialer paths are guarded too.
func installHostClientGuard(hc *fasthttp.HostClient) {
	if hc.DialTimeout != nil {
		hc.DialTimeout = newGuardedClientDialerWithTimeout(hc.DialTimeout, hc.DialDualStack)
	}
	hc.Dial = newGuardedClientDialer(hc.Dial, hc.DialDualStack)
}

View on GitHub (pinned to a105acad6c)

Solutions

  1. Confirm the host has IPv4 (A) records if you cannot enable dual-stack: dig A hostname +short.
  2. If the upstream is legitimately IPv6-capable and your network supports it, enable DialDualStack on the HostClient / fasthttp.Client.
  3. If IPv4 is required, ask the upstream operator to publish A records.
  4. Verify your runtime network has IPv4 connectivity before relying on it.
  5. Log the resolved IPs at debug level to see what dialValidatedIPs received.

Example fix

// before: IPv6-only upstream, IPv4-default client
balancer.HostClient.DialDualStack = false // 197 fires

// after: enable dual stack
balancer.HostClient.DialDualStack = true
Defensive patterns

Strategy: validation

Validate before calling

func hasIPv4(host string) bool {
  ips, _ := net.LookupIP(host)
  for _, ip := range ips { if ip.To4() != nil { return true } }
  return false
}

Prevention

When it happens

Trigger: Hostname resolves to IPv6-only (AAAA-only, no A records) and the HostClient has DialDualStack=false (the fasthttp default); an empty resolved list slipped past earlier guards; all resolved IPs were skipped as IPv6.

Common situations: IPv6-only upstream service reached from an IPv4-default HostClient; a hostname that recently lost its A record; a service on an IPv6-only network (some mobile/telco backends); HostClient created without DialDualStack=true.

Related errors


AI-assisted analysis of gofiber/fiber@a105acad6c (2026-08-11). Data as JSON: /api/errors/06af572e6bebbf1c. Report an issue: GitHub.