@marcel91200 / caddy Mirror · read-only / commits / ac63faab
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 04:45 UTC

headers: Preserve separate Set-Cookie values (#8080)

Hitanshu committed Oct 3, 2026 at 07:38 UTC ac63faabb8cf4e765c2982fd3c2a35d5a0dfa608
2 files changed +82 -1
modules/caddyhttp/headers/headers.go
+24 -1
@@ -18,6 +18,7 @@ import (
18 "fmt"
19 "net/http"
20 "regexp"
21 + "sort"
22 "strings"
23
24 "github.com/caddyserver/caddy/v2"
@@ -136,6 +137,19 @@ func (ops *HeaderOps) Provision(_ caddy.Context) error {
137 if ops == nil {
138 return nil // it's possible no ops are configured; fix #6893
139 }
140 + if len(ops.Set) > 0 {
141 + set := make(http.Header, len(ops.Set))
142 + var fields []string
143 + for fieldName := range ops.Set {
144 + fields = append(fields, fieldName)
145 + }
146 + sort.Strings(fields)
147 + for _, fieldName := range fields {
148 + canonical := http.CanonicalHeaderKey(fieldName)
149 + set[canonical] = append(set[canonical], ops.Set[fieldName]...)
150 + }
151 + ops.Set = set
152 + }
153 for fieldName, replacements := range ops.Replace {
154 for i, r := range replacements {
155 if r.SearchRegexp == "" {
@@ -242,13 +256,22 @@ func (ops *HeaderOps) ApplyTo(hdr http.Header, repl *caddy.Replacer) {
256
257 // set
258 for fieldName, vals := range ops.Set {
245 - fieldName = repl.ReplaceKnown(fieldName, "")
259 + if expanded := repl.ReplaceKnown(fieldName, ""); expanded != fieldName {
260 + fieldName = http.CanonicalHeaderKey(expanded)
261 + }
262 var newVals []string
263 for i := range vals {
264 // append to new slice so we don't overwrite
265 // the original values in ops.Set
266 newVals = append(newVals, repl.ReplaceKnown(vals[i], ""))
267 }
268 + if fieldName == "Set-Cookie" && len(newVals) > 1 {
269 + hdr.Del(fieldName)
270 + for _, val := range newVals {
271 + hdr.Add(fieldName, val)
272 + }
273 + continue
274 + }
275 hdr.Set(fieldName, strings.Join(newVals, ","))
276 }
277
modules/caddyhttp/headers/headers_test.go
+58
@@ -267,6 +267,64 @@ func TestHandler(t *testing.T) {
267 }
268 }
269
270 +func TestHeaderOpsSetMultipleCookies(t *testing.T) {
271 + want := []string{"a=1; Path=/", "b=2; Path=/"}
272 + for _, tc := range []struct {
273 + fieldName, provisionedName string
274 + }{
275 + {"Set-Cookie", "Set-Cookie"},
276 + {"set-cookie", "Set-Cookie"},
277 + {"sEt-CoOkIe", "Set-Cookie"},
278 + {"{cookie_header}", "{cookie_header}"},
279 + {"set-{cookie_suffix}", "set-{cookie_suffix}"},
280 + } {
281 + t.Run(tc.fieldName, func(t *testing.T) {
282 + ops := HeaderOps{Set: http.Header{tc.fieldName: want}}
283 + if err := ops.Provision(caddy.Context{}); err != nil {
284 + t.Fatal(err)
285 + }
286 + if !reflect.DeepEqual(ops.Set, http.Header{tc.provisionedName: want}) {
287 + t.Fatalf("provisioned Set = %v, want field %q", ops.Set, tc.provisionedName)
288 + }
289 + repl := caddy.NewReplacer()
290 + repl.Set("cookie_header", "set-cookie")
291 + repl.Set("cookie_suffix", "cookie")
292 + hdr := http.Header{"Set-Cookie": {"old=1"}}
293 + ops.ApplyTo(hdr, repl)
294 + if got := hdr.Values("Set-Cookie"); !reflect.DeepEqual(got, want) {
295 + t.Errorf("Set-Cookie values = %q, want %q", got, want)
296 + }
297 + })
298 + }
299 +}
300 +
301 +func TestHandlerProvisionSetCollision(t *testing.T) {
302 + for _, name := range []string{"request", "response"} {
303 + t.Run(name, func(t *testing.T) {
304 + ops := &HeaderOps{Set: http.Header{
305 + "set-cookie": {"b=2; Path=/"},
306 + "Set-Cookie": {"a=1; Path=/"},
307 + }}
308 + h := Handler{Request: ops}
309 + if name == "response" {
310 + h = Handler{Response: &RespHeaderOps{HeaderOps: ops}}
311 + }
312 + if err := h.Provision(caddy.Context{}); err != nil {
313 + t.Fatal(err)
314 + }
315 + want := http.Header{"Set-Cookie": {"a=1; Path=/", "b=2; Path=/"}}
316 + if !reflect.DeepEqual(ops.Set, want) {
317 + t.Fatalf("provisioned Set = %v, want %v", ops.Set, want)
318 + }
319 + hdr := make(http.Header)
320 + ops.ApplyTo(hdr, caddy.NewReplacer())
321 + if !reflect.DeepEqual(hdr, want) {
322 + t.Errorf("headers = %v, want %v", hdr, want)
323 + }
324 + })
325 + }
326 +}
327 +
328 type nextHandler func(http.ResponseWriter, *http.Request) error
329
330 func (f nextHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) error {