caddyserver/caddy · error

parsing CIDR expression '%s': %v

Error message

parsing CIDR expression '%s': %v

What it means

Thrown while provisioning the remote_ip/client_ip matcher when an entry contains '/' and is therefore treated as a CIDR, but netip.ParsePrefix rejects it. The string is passed through verbatim in the message, so the offending entry is visible. This is a config-load-time error: Caddy refuses to start/apply the config containing the bad matcher.

Source

Thrown at modules/caddyhttp/ip_matchers.go:302

func provisionCidrsZonesFromRanges(ranges []string) ([]*netip.Prefix, []string, error) {
	cidrs := []*netip.Prefix{}
	zones := []string{}
	repl := caddy.NewReplacer()
	for _, str := range ranges {
		str = repl.ReplaceAll(str, "")
		// Exclude the zone_id from the IP
		if strings.Contains(str, "%") {
			split := strings.Split(str, "%")
			str = split[0]
			// write zone identifiers in m.zones for matching later
			zones = append(zones, split[1])
		} else {
			zones = append(zones, "")
		}
		if strings.Contains(str, "/") {
			ipNet, err := netip.ParsePrefix(str)
			if err != nil {
				return nil, nil, fmt.Errorf("parsing CIDR expression '%s': %v", str, err)
			}
			cidrs = append(cidrs, &ipNet)
		} else {
			ipAddr, err := netip.ParseAddr(str)
			if err != nil {
				return nil, nil, fmt.Errorf("invalid IP address: '%s': %v", str, err)
			}
			ipNew := netip.PrefixFrom(ipAddr, ipAddr.BitLen())
			cidrs = append(cidrs, &ipNew)
		}
	}
	return cidrs, zones, nil
}

func parseIPZoneFromString(address string) (netip.Addr, string, error) {
	ipStr, _, err := net.SplitHostPort(address)
	if err != nil {
		ipStr = address // OK; probably didn't have a port

View on GitHub (pinned to 50e54ee279)

Solutions

  1. Correct the CIDR to a valid prefix: IPv4 masks 0-32, IPv6 masks 0-128 (e.g. 192.168.1.0/24).
  2. If you meant a single host, drop the slash: remote_ip 10.0.0.1.
  3. Verify entries before deploying with a quick Go check: netip.ParsePrefix("192.168.1.0/24").
  4. Run `caddy adapt --config Caddyfile --adapter caddyfile` then `caddy validate` to catch this before reload.

Example fix

// before (Caddyfile)
@internal remote_ip 192.168.1.0/33

// after
@internal remote_ip 192.168.1.0/24
Defensive patterns

Strategy: validation

Validate before calling

// Go: pre-validate every CIDR entry before building the config
import "net/netip"

func validCIDRs(entries []string) bool {
	for _, e := range entries {
		if !strings.Contains(e, "/") {
			continue
		}
		if _, err := netip.ParsePrefix(e); err != nil {
			return false
		}
	}
	return true
}

Prevention

When it happens

Trigger: Configuring `remote_ip 192.168.1.0/33` (mask too long), `10.0.0.0/8/24`, an IPv6 prefix like `fe80::/10x`, or a bare IP with a trailing slash `10.0.0.1/`. Also triggered when a zone identifier was stripped (everything after '%') leaving a malformed prefix.

Common situations: Typos in subnet masks in a Caddyfile or JSON config; assuming an arbitrary bit count is valid (IPv4 max /32, IPv6 max /128); copy-pasting routes that include both an IP and mask separated incorrectly; IPv4-mapped IPv6 notation that netip refuses in prefixes.

Related errors


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