caddyserver/caddy · error

invalid IP address: '%s': %v

Error message

invalid IP address: '%s': %v

What it means

Thrown while provisioning the remote_ip/client_ip matcher when an entry has no '/', so it is parsed as a bare IP with netip.ParseAddr, which rejects it. Note that anything after '%' is stripped first (zone identifier handling), so the reported string may differ slightly from what you wrote. Config load fails and the old config (if any) keeps running.

Source

Thrown at modules/caddyhttp/ip_matchers.go:308

		// 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
	}

	// Some IPv6-Addresses can contain zone identifiers at the end,
	// which are separated with "%"
	zoneID := ""
	if strings.Contains(ipStr, "%") {

View on GitHub (pinned to 50e54ee279)

Solutions

  1. Replace the value with a valid literal IP: remote_ip 192.168.1.10.
  2. If you need to match a hostname, use the `host` matcher instead.
  3. If matching a whole subnet, add the mask so it goes down the CIDR path: 10.0.0.0/8.
  4. Sanitize generated configs (trim spaces, drop empty list entries) before feeding them to Caddy.

Example fix

// before
@lan remote_ip example.com

// after
@lan remote_ip 192.168.1.10
Defensive patterns

Strategy: validation

Validate before calling

import "net/netip"

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

Prevention

When it happens

Trigger: remote_ip set to a hostname (e.g. `remote_ip example.com`), a malformed address like `192.168.1.256` or `fe80:::1`, an empty string, or a value that became empty/invalid after the '%' zone split (`fe80::1%eth0` is fine, `%-bad` is not).

Common situations: Using hostnames or placeholders that expand to text instead of literals in IP matchers; trailing whitespace/comma artifacts from templated configs; expecting IPv4-mapped form `::ffff:192.168.1.1` to be normalized against an IPv4 list (it is not, by design).

Related errors


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