gofiber/fiber · error

proxy: invalid dial address

Error message

proxy: invalid dial address %q: %w

What it means

In newSSRFDialer (the Balancer-installed dial-time guard), the inbound addr from fasthttp is split via net.SplitHostPort. If the address is not a valid host:port form, the split error is wrapped. This guard is purely structural — the address fasthttp hands the dialer should always be host:port, so this fires only on a misconfigured/buggy HostClient address.

Solutions

  1. Inspect the quoted addr in the message — it shows exactly what was passed to the dialer.
  2. Ensure every Balancer.Servers and HostClient.Addr entry is host:port (or [ipv6]:port for IPv6).
  3. Validate upstream addresses with net.SplitHostPort at config load and fail fast.
  4. For IPv6 upstreams, always wrap the host in brackets: "http://[::1]:8080".

Example fix

// before: missing port on balancer entry
balancer.Servers = []string{"http://upstream"}

// after: explicit port
balancer.Servers = []string{"http://upstream:8080"}
Defensive patterns

Strategy: validation

Validate before calling

func validHostPort(addr string) error {
  _, _, err := net.SplitHostPort(addr); return err
}

Prevention

When it happens

Trigger: fasthttp invoked the dialer with an address missing a port (e.g. "host"), with an empty host (":8080"), with too many colons in a non-bracketed IPv6 literal ("::1:8080" instead of "[::1]:8080"), or with a non-numeric port.

Common situations: Balancer.Servers entry missing a port; a custom LBClient.Config.Client whose Addr is malformed; an IPv6 upstream entered without brackets; programmatic upstream construction that drops the port.

Related errors


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

Appendix: source

Thrown at middleware/proxy/security.go:429

	}
	return nil
}

// newSSRFDialer returns a fasthttp DialFunc that resolves the target host
// (with a bounded timeout), rejects the connection if any resolved
// address falls in a blocked range, and then dials a validated address.
// Performing the check at dial time — rather than only up front — defeats
// DNS-rebinding attacks (the check/use gap) where a resolver returns a
// public address during validation and a private one at connect time. It
// is only installed when the active policy disallows private IPs.
//
//nolint:revive // dialDualStack mirrors fasthttp.HostClient.DialDualStack
func newSSRFDialer(dialDualStack bool) fasthttp.DialFunc {
	dialer := &net.Dialer{Timeout: dnsLookupTimeout}
	return func(addr string) (net.Conn, error) {
		host, port, err := net.SplitHostPort(addr)
		if err != nil {
			return nil, fmt.Errorf("proxy: invalid dial address %q: %w", addr, err)
		}
		ips, err := resolveAndValidateHost(host)
		if err != nil {
			return nil, err
		}
		return dialValidatedIPs(ips, host, port, dialDualStack, dialer.Dial)
	}
}

// resolveAndValidateHost looks up host (or treats it as an IP literal),
// then enforces the SSRF blocklist on every returned address. A single
// blocked answer fails the whole resolution so a mixed public/private
// reply cannot slip past the guard.
func resolveAndValidateHost(host string) ([]net.IP, error) {
	var ips []net.IP
	if ip := net.ParseIP(host); ip != nil {
		ips = []net.IP{ip}
	} else {

View on GitHub (pinned to a105acad6c)