caddyserver/caddy · error

due to parsing difficulties, placeholders are not allowed wh

Error message

due to parsing difficulties, placeholders are not allowed when an upstream address contains a scheme

What it means

parseUpstreamDialAddress rejects upstream addresses that contain both a scheme (detected via '://') and a '{' placeholder character. url.Parse cannot handle Caddy placeholders, so instead of an opaque parse error Caddy fails fast with this explicit message telling you the two features are mutually exclusive.

Source

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

}

func (p parsedAddr) isUnix() bool {
	return caddy.IsUnixNetwork(p.network)
}

// parseUpstreamDialAddress parses configuration inputs for
// the dial address, including support for a scheme in front
// as a shortcut for the port number, and a network type,
// for example 'unix' to dial a unix socket.
func parseUpstreamDialAddress(upstreamAddr string) (parsedAddr, error) {
	var network, scheme, host, port string

	if strings.Contains(upstreamAddr, "://") {
		// we get a parsing error if a placeholder is specified
		// 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 {

View on GitHub (pinned to 50e54ee279)

Solutions

  1. Remove the scheme and keep the placeholder, e.g. 'reverse_proxy {env.BACKEND_HOST}:443' (or use transport block with tls for HTTPS semantics).
  2. If the scheme must vary, resolve the full address (scheme included is still impossible; resolve host+port) outside Caddy via environment variables per deployment.
  3. Use dynamic upstream modules (e.g. 'dynamic srv' or 'dynamic a') when the backend set must be computed at runtime.

Example fix

# before (Caddyfile)
reverse_proxy https://{env.BACKEND_HOST}

# after
reverse_proxy {env.BACKEND_HOST}:443 {
  transport http {
    tls
  }
}
Defensive patterns

Strategy: validation

Validate before calling

func hasSchemePlaceholder(addr string) bool {
	return strings.Contains(addr, "://") && strings.Contains(addr, "{")
}

for _, u := range cfg.Upstreams {
	if hasSchemePlaceholder(u.Dial) {
		return fmt.Errorf("upstream %q mixes a scheme with a placeholder; move the port into the value or use a transport block", u.Dial)
	}
}

Prevention

When it happens

Trigger: Configuring a reverse_proxy upstream like 'https://{env.BACKEND_HOST}' or 'http://{upstream_host}:8080' where the address string contains both a scheme and any placeholder.

Common situations: Trying to select the backend scheme from environment variables or request-derived placeholders; forgetting that the port can be inferred from the scheme and that placeholders work only in scheme-less upstream addresses.

Related errors


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