caddyserver/caddy · error

%s: route %d, matcher set %d, matcher %d, host matcher %d: %

Error message

%s: route %d, matcher set %d, matcher %d, host matcher %d: %v

What it means

Thrown during automatic HTTPS provisioning when a host matcher value in a server's route cannot have placeholders resolved. The HTTP app walks every route's matcher sets, finds MatchHost entries, and calls replacer.ReplaceOrErr(d, true, false); if a placeholder inside a host name errors (e.g. an environment variable placeholder that is unknown or empty with error-on-empty semantics), provisioning aborts with the server name and precise indexes of the offending matcher.

Source

Thrown at modules/caddyhttp/autohttps.go:158

			logger.Info("server is listening only on the HTTPS port but has no TLS connection policies; adding one to enable TLS",
				zap.String("server_name", srvName),
				zap.Int("https_port", app.httpsPort()),
			)
			srv.TLSConnPolicies = caddytls.ConnectionPolicies{new(caddytls.ConnectionPolicy)}
		}

		// find all qualifying domain names (deduplicated) in this server
		// (this is where we need the provisioned, decoded request matchers)
		serverDomainSet := make(map[string]struct{})
		for routeIdx, route := range srv.Routes {
			for matcherSetIdx, matcherSet := range route.MatcherSets {
				for matcherIdx, m := range matcherSet {
					if hm, ok := m.(*MatchHost); ok {
						for hostMatcherIdx, d := range *hm {
							var err error
							d, err = repl.ReplaceOrErr(d, true, false)
							if err != nil {
								return fmt.Errorf("%s: route %d, matcher set %d, matcher %d, host matcher %d: %v",
									srvName, routeIdx, matcherSetIdx, matcherIdx, hostMatcherIdx, err)
							}
							if !slices.Contains(srv.AutoHTTPS.Skip, d) {
								serverDomainSet[d] = struct{}{}
							}
						}
					}
				}
			}
		}

		// build the list of domains that could be used with ECH (if enabled)
		// so the TLS app can know to publish ECH configs for them
		echDomains := make([]string, 0, len(serverDomainSet))
		for d := range serverDomainSet {
			echDomains = append(echDomains, d)
		}
		app.tlsApp.RegisterServerNames(echDomains, httpsRRALPNs(srv))

View on GitHub (pinned to 50e54ee279)

Solutions

  1. Export the missing environment variable (e.g. `export SITE_NAME=example.com`) or define it in the Caddyfile env block before starting Caddy
  2. Fix the placeholder name to one the global replacer knows (env.* vars must exist at process start)
  3. If the value is optional, give it a default: `{$SITE_NAME:localhost}`
  4. Skip auto-HTTPS for that server (`auto_https off` or skip_auto_https) if you manage certs manually, so host matchers are not resolved this way

Example fix

# before
{$SITE_NAME} {
  respond "hi"
}
# after (run with the var set, or default it)
{$SITE_NAME:localhost} {
  respond "hi"
}
Defensive patterns

Strategy: validation

Validate before calling

# Caddyfile: verify placeholders resolve before start
DOMAIN={$SITE_NAME:?SITE_NAME is not set}  # fails fast in shell with message
# or use a default so resolution can never error:
# site address: {$SITE_NAME:localhost}

Prevention

When it happens

Trigger: A site block whose host matcher contains a placeholder like {$SITE_NAME} or {env.DOMAIN} that the global replacer cannot resolve at provision time (unset env var, unknown placeholder key, or a placeholder that resolves to empty string). Occurs only when auto-HTTPS runs, i.e. the server has HTTPS-capable addresses and skip_auto_https is not set.

Common situations: Running Caddy with a Caddyfile that uses {env.DOMAIN} as the site address but forgetting to export the variable; typos in placeholder names; using custom placeholder sources that are registered after provisioning; deploying the same config to a machine missing the env var.

Related errors


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