caddyserver/caddy · error

parsing CIDR expression: '%s': %v

Error message

parsing CIDR expression: '%s': %v

What it means

Each entry in trusted_proxies is parsed at provision time: strings containing '/' go through netip.ParsePrefix (CIDR), everything else through netip.ParseAddr. A malformed CIDR (bad bits field, stray spaces, embedded hostname) or an invalid bare IP yields 'parsing CIDR expression: '%s': %v' or 'invalid IP address'. Note there is no hostname resolution — the value must already be an IP or CIDR literal.

Source

Thrown at modules/caddyhttp/reverseproxy/reverseproxy.go:324

		if err != nil {
			return fmt.Errorf("loading circuit breaker: %s", err)
		}
		h.CB = mod.(CircuitBreaker)
	}
	if h.DynamicUpstreamsRaw != nil {
		mod, err := ctx.LoadModule(h, "DynamicUpstreamsRaw")
		if err != nil {
			return fmt.Errorf("loading upstream source module: %v", err)
		}
		h.DynamicUpstreams = mod.(UpstreamSource)
	}

	// parse trusted proxy CIDRs ahead of time
	for _, str := range h.TrustedProxies {
		if strings.Contains(str, "/") {
			ipNet, err := netip.ParsePrefix(str)
			if err != nil {
				return fmt.Errorf("parsing CIDR expression: '%s': %v", str, err)
			}
			h.trustedProxies = append(h.trustedProxies, ipNet)
		} else {
			ipAddr, err := netip.ParseAddr(str)
			if err != nil {
				return fmt.Errorf("invalid IP address: '%s': %v", str, err)
			}
			ipNew := netip.PrefixFrom(ipAddr, ipAddr.BitLen())
			h.trustedProxies = append(h.trustedProxies, ipNew)
		}
	}

	// ensure any embedded headers handler module gets provisioned
	// (see https://caddy.community/t/set-cookie-manipulation-in-reverse-proxy/7666?u=matt
	// for what happens if we forget to provision it)
	if h.Headers != nil {
		err := h.Headers.Provision(ctx)
		if err != nil {

View on GitHub (pinned to 50e54ee279)

Solutions

  1. Replace hostnames with concrete IPs or CIDRs (resolve once, or maintain the list manually): trusted_proxies 10.0.0.0/8 192.168.0.0/16.
  2. For CIDRs, mask host bits and keep the prefix length valid (0–32 for v4, 0–128 for v6): use 10.0.0.0/24 not 10.0.0.1/24.
  3. If the list comes from an env placeholder, print the rendered config (caddy adapt) and sanitize the injected value.
  4. Use private_ranges only when full RFC1918+loopback trust is acceptable.

Example fix

# before
reverse_proxy localhost:8080 {
    trusted_proxies proxy.internal 10.0.0.1/24
}

# after
reverse_proxy localhost:8080 {
    trusted_proxies 10.0.0.0/24 192.168.1.10
}
Defensive patterns

Strategy: validation

Validate before calling

import "net/netip"

func validateTrustedProxies(entries []string) error {
    for _, s := range entries {
        if strings.Contains(s, "/") {
            if _, err := netip.ParsePrefix(s); err != nil {
                return fmt.Errorf("bad CIDR %q: %w", s, err)
            }
            continue
        }
        if _, err := netip.ParseAddr(s); err != nil {
            return fmt.Errorf("bad IP %q: %w", s, err)
        }
    }
    return nil
}

Prevention

When it happens

Trigger: trusted_proxies 10.0.0.0/8 192.168.1.5 works; trusted_proxies proxy.internal (a hostname), 10.0.0.0/33 (invalid prefix length), 10.0.0.1/24 (host bits set — ParsePrefix rejects), or a value with whitespace/quotes from env-var interpolation fails.

Common situations: See trigger scenarios.

Related errors


AI-assisted analysis of caddyserver/caddy@50e54ee279 (2026-08-15). Data as JSON: /api/errors/5cef1d1a6f0f7a64. Report an issue: GitHub.