caddyserver/caddy · error
it is unnecessary to specify the TLS listener wrapper in the
Error message
it is unnecessary to specify the TLS listener wrapper in the first position because that is the default
What it means
The special 'tls' placeholder listener wrapper marks where TLS termination happens in the wrapper chain. Position 0 is the implicit default (all wrappers after the TLS handshake), so specifying it first is rejected as redundant. This is a pure configuration sanity check, not a runtime failure.
Source
Thrown at modules/caddyhttp/app.go:335
return fmt.Errorf("server %s, listener %d: %v", srvName, i, err)
}
srv.Listen[i] = lnOut
}
// set up each listener modifier
if srv.ListenerWrappersRaw != nil {
vals, err := ctx.LoadModule(srv, "ListenerWrappersRaw")
if err != nil {
return fmt.Errorf("loading listener wrapper modules: %v", err)
}
var hasTLSPlaceholder bool
for i, val := range vals.([]any) {
if _, ok := val.(*tlsPlaceholderWrapper); ok {
if i == 0 {
// putting the tls placeholder wrapper first is nonsensical because
// that is the default, implicit setting: without it, all wrappers
// will go after the TLS listener anyway
return fmt.Errorf("it is unnecessary to specify the TLS listener wrapper in the first position because that is the default")
}
if hasTLSPlaceholder {
return fmt.Errorf("TLS listener wrapper can only be specified once")
}
hasTLSPlaceholder = true
}
srv.listenerWrappers = append(srv.listenerWrappers, val.(caddy.ListenerWrapper))
}
// if any wrappers were configured but the TLS placeholder wrapper is
// absent, prepend it so all defined wrappers come after the TLS
// handshake; this simplifies logic when starting the server, since we
// can simply assume the TLS placeholder will always be there
if !hasTLSPlaceholder && len(srv.listenerWrappers) > 0 {
srv.listenerWrappers = append([]caddy.ListenerWrapper{new(tlsPlaceholderWrapper)}, srv.listenerWrappers...)
}
}
// set up each packet conn modifierView on GitHub (pinned to 50e54ee279)
Solutions
- Remove the tls entry from the first position — omit it entirely, or move it after the wrappers that must see raw/pretLS traffic (e.g. proxy_protocol)
- Re-validate the config
Example fix
// before
"listener_wrappers": ["tls", {"wrapper":"proxy_protocol"}]
// after
"listener_wrappers": [{"wrapper":"proxy_protocol"}, "tls"] Defensive patterns
Strategy: validation
Validate before calling
wrappers := srvCfg.ListenerWrappers
if len(wrappers) > 0 && wrappers[0] == "tls" {
return fmt.Errorf("tls wrapper must not be first (it is the default)")
} Prevention
- Omit the tls marker unless a wrapper must run before the TLS handshake
- Review generated listener_wrappers arrays after adapting Caddyfiles that use global options
When it happens
Trigger: Putting the tls placeholder wrapper in the first element of servers.<name>.listener_wrappers, e.g. ["tls", "proxy_protocol"].
Common situations: Users copy an example config that shows the tls marker and place it first 'for clarity'; misunderstanding that wrapper order is significant only when some wrappers must run before TLS.
Understand the failure class
- SSL/TLS and certificate errors — how TLS handshakes and certificate validation fail.
Related errors
- TLS listener wrapper can only be specified once
- consolidating TLS connection policies for server %d: %v
- server %s: setting up TLS connection policies: %v
- loading certificate loader modules: %s
- provisioning automation policy %d: %v
AI-assisted analysis of caddyserver/caddy@50e54ee279 (2026-08-15).
Data as JSON: /api/errors/19c59e1651affe1a.
Report an issue: GitHub.