caddyserver/caddy · error

parsing upstream URL: parse "%v": port range invalid: %v

Error message

parsing upstream URL: parse "%v": port range invalid: %v

What it means

url.Parse failed with an 'invalid port' error containing '-', so Caddy assumed a port range and replaced everything after the last ':' with a dummy port — but the extracted port range does not contain exactly one '-' (strings.Count(portRange, "-") != 1). Only a single contiguous range like '8080-8090' is supported.

Source

Thrown at modules/caddyhttp/reverseproxy/addresses.go:88

		// so we return a more user-friendly error message instead
		// to explain what to do instead
		if strings.Contains(upstreamAddr, "{") {
			return parsedAddr{}, fmt.Errorf("due to parsing difficulties, placeholders are not allowed when an upstream address contains a scheme")
		}

		toURL, err := url.Parse(upstreamAddr)
		if err != nil {
			// if the error seems to be due to a port range,
			// try to replace the port range with a dummy
			// single port so that url.Parse() will succeed
			if strings.Contains(err.Error(), "invalid port") && strings.Contains(err.Error(), "-") {
				index := strings.LastIndex(upstreamAddr, ":")
				if index == -1 {
					return parsedAddr{}, fmt.Errorf("parsing upstream URL: %v", err)
				}
				portRange := upstreamAddr[index+1:]
				if strings.Count(portRange, "-") != 1 {
					return parsedAddr{}, fmt.Errorf("parsing upstream URL: parse \"%v\": port range invalid: %v", upstreamAddr, portRange)
				}
				toURL, err = url.Parse(strings.ReplaceAll(upstreamAddr, portRange, "0"))
				if err != nil {
					return parsedAddr{}, fmt.Errorf("parsing upstream URL: %v", err)
				}
				port = portRange
			} else {
				return parsedAddr{}, fmt.Errorf("parsing upstream URL: %v", err)
			}
		}
		if port == "" {
			port = toURL.Port()
		}

		// there is currently no way to perform a URL rewrite between choosing
		// a backend and proxying to it, so we cannot allow extra components
		// in backend URLs
		if toURL.Path != "" || toURL.RawQuery != "" || toURL.Fragment != "" {

View on GitHub (pinned to 50e54ee279)

Solutions

  1. Use a single contiguous port range: 'http://host:8080-8090'.
  2. Split discontiguous ranges into multiple upstreams: 'reverse_proxy host:8080-8090 host:9090'.
  3. Wrap IPv6 literals in brackets so the last colon is the port separator: 'http://[fe80::1]:8080'.

Example fix

# before
reverse_proxy http://backend:8080-8090-8100

# after
reverse_proxy http://backend:8080-8090 http://backend:8100
Defensive patterns

Strategy: validation

Validate before calling

func validPortRange(addr string) bool {
	i := strings.LastIndex(addr, ":")
	if i < 0 || i == len(addr)-1 {
		return false
	}
	pr := addr[i+1:]
	return strings.Count(pr, "-") == 1 && pr[0] != '-'
}

for _, u := range upstreams {
	if strings.Contains(u, "://") && strings.Contains(u, "-") && !validPortRange(u) {
		return fmt.Errorf("upstream %q: only single contiguous port ranges (start-end) allowed", u)
	}
}

Prevention

When it happens

Trigger: Upstream addresses such as 'http://host:8080-8090-8100' (two dashes), 'http://host:-8080' (leading dash), or where the last segment after the colon coincidentally contains multiple dashes, e.g. IPv6 or hyphenated hostnames misparsed as ports.

Common situations: Trying to express multiple discontiguous port ranges in one upstream; hyphenated hostnames where the colon detection lands in the wrong place.

Related errors


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