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 modifier

View on GitHub (pinned to 50e54ee279)

Solutions

  1. 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)
  2. 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

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

Related errors


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