caddyserver/caddy · error

TLS listener wrapper can only be specified once

Error message

TLS listener wrapper can only be specified once

What it means

The tls placeholder wrapper may appear at most once in the listener_wrappers chain because it is only a position marker, not a real wrapper. A second occurrence makes the intended wrapper ordering ambiguous, so provisioning fails.

Source

Thrown at modules/caddyhttp/app.go:338

		}

		// 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 modifier
		if srv.PacketConnWrappersRaw != nil {
			vals, err := ctx.LoadModule(srv, "PacketConnWrappersRaw")
			if err != nil {

View on GitHub (pinned to 50e54ee279)

Solutions

  1. Delete all but one tls entry, keeping it where TLS termination should occur
  2. If unsure, remove it entirely — Caddy prepends it automatically when absent

Example fix

// before
"listener_wrappers": ["proxy_protocol", "tls", "tls"]
// after
"listener_wrappers": ["proxy_protocol", "tls"]
Defensive patterns

Strategy: validation

Validate before calling

count := 0
for _, w := range srvCfg.ListenerWrappers {
    if w == "tls" { count++ }
}
if count > 1 { return fmt.Errorf("tls wrapper specified %d times", count) }

Prevention

When it happens

Trigger: Listing the tls wrapper two or more times in servers.<name>.listener_wrappers, e.g. ["proxy_protocol", "tls", {"wrapper":"..."}, "tls"].

Common situations: Merging config fragments that each already contained a tls marker; hand-editing JSON and duplicating the entry.

Understand the failure class

Related errors


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