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 {