{"record":{"id":"cefae138dee5e665","repo":"caddyserver/caddy","slug":"tls-listener-wrapper-can-only-be-specified-once","errorCode":null,"errorMessage":"TLS listener wrapper can only be specified once","messagePattern":"TLS listener wrapper can only be specified once","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"modules/caddyhttp/app.go","lineNumber":338,"sourceCode":"\t\t}\n\n\t\t// set up each listener modifier\n\t\tif srv.ListenerWrappersRaw != nil {\n\t\t\tvals, err := ctx.LoadModule(srv, \"ListenerWrappersRaw\")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"loading listener wrapper modules: %v\", err)\n\t\t\t}\n\t\t\tvar hasTLSPlaceholder bool\n\t\t\tfor i, val := range vals.([]any) {\n\t\t\t\tif _, ok := val.(*tlsPlaceholderWrapper); ok {\n\t\t\t\t\tif i == 0 {\n\t\t\t\t\t\t// putting the tls placeholder wrapper first is nonsensical because\n\t\t\t\t\t\t// that is the default, implicit setting: without it, all wrappers\n\t\t\t\t\t\t// will go after the TLS listener anyway\n\t\t\t\t\t\treturn fmt.Errorf(\"it is unnecessary to specify the TLS listener wrapper in the first position because that is the default\")\n\t\t\t\t\t}\n\t\t\t\t\tif hasTLSPlaceholder {\n\t\t\t\t\t\treturn fmt.Errorf(\"TLS listener wrapper can only be specified once\")\n\t\t\t\t\t}\n\t\t\t\t\thasTLSPlaceholder = true\n\t\t\t\t}\n\t\t\t\tsrv.listenerWrappers = append(srv.listenerWrappers, val.(caddy.ListenerWrapper))\n\t\t\t}\n\t\t\t// if any wrappers were configured but the TLS placeholder wrapper is\n\t\t\t// absent, prepend it so all defined wrappers come after the TLS\n\t\t\t// handshake; this simplifies logic when starting the server, since we\n\t\t\t// can simply assume the TLS placeholder will always be there\n\t\t\tif !hasTLSPlaceholder && len(srv.listenerWrappers) > 0 {\n\t\t\t\tsrv.listenerWrappers = append([]caddy.ListenerWrapper{new(tlsPlaceholderWrapper)}, srv.listenerWrappers...)\n\t\t\t}\n\t\t}\n\n\t\t// set up each packet conn modifier\n\t\tif srv.PacketConnWrappersRaw != nil {\n\t\t\tvals, err := ctx.LoadModule(srv, \"PacketConnWrappersRaw\")\n\t\t\tif err != nil {","sourceCodeStart":320,"sourceCodeEnd":356,"githubUrl":"https://github.com/caddyserver/caddy/blob/50e54ee279aa1e504fe218ca49ab6ae16c100410/modules/caddyhttp/app.go#L320-L356","documentation":"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.","triggerScenarios":"Listing the tls wrapper two or more times in servers.<name>.listener_wrappers, e.g. [\"proxy_protocol\", \"tls\", {\"wrapper\":\"...\"}, \"tls\"].","commonSituations":"Merging config fragments that each already contained a tls marker; hand-editing JSON and duplicating the entry.","solutions":["Delete all but one tls entry, keeping it where TLS termination should occur","If unsure, remove it entirely — Caddy prepends it automatically when absent"],"exampleFix":"// before\n\"listener_wrappers\": [\"proxy_protocol\", \"tls\", \"tls\"]\n// after\n\"listener_wrappers\": [\"proxy_protocol\", \"tls\"]","handlingStrategy":"validation","validationCode":"count := 0\nfor _, w := range srvCfg.ListenerWrappers {\n    if w == \"tls\" { count++ }\n}\nif count > 1 { return fmt.Errorf(\"tls wrapper specified %d times\", count) }","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Keep at most one tls marker per server config","Lint merged configs for duplicated marker entries"],"tags":["caddy","tls","listener-wrappers","config"],"backgroundTag":null,"analyzedSha":"50e54ee279aa1e504fe218ca49ab6ae16c100410","analyzedAt":"2026-08-15T09:20:21.641Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}