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

This commit is contained in:
Hitanshu authored and GitHub committed 2026-10-02 22:08:23 -04:00
1 parent 55a95ad253
commit ac63faabb8
2 files changed
+82 -1

No files matched your search

+24 -1
View File
@@ -18,6 +18,7 @@ import (
"fmt"
"net/http"
"regexp"
"sort"
"strings"
"github.com/caddyserver/caddy/v2"
@@ -136,6 +137,19 @@ func (ops *HeaderOps) Provision(_ caddy.Context) error {
if ops == nil {
return nil // it's possible no ops are configured; fix #6893
}
if len(ops.Set) > 0 {
set := make(http.Header, len(ops.Set))
var fields []string
for fieldName := range ops.Set {
fields = append(fields, fieldName)
}
sort.Strings(fields)
for _, fieldName := range fields {
canonical := http.CanonicalHeaderKey(fieldName)
set[canonical] = append(set[canonical], ops.Set[fieldName]...)
}
ops.Set = set
}
for fieldName, replacements := range ops.Replace {
for i, r := range replacements {
if r.SearchRegexp == "" {
@@ -242,13 +256,22 @@ func (ops *HeaderOps) ApplyTo(hdr http.Header, repl *caddy.Replacer) {
// set
for fieldName, vals := range ops.Set {
fieldName = repl.ReplaceKnown(fieldName, "")
if expanded := repl.ReplaceKnown(fieldName, ""); expanded != fieldName {
fieldName = http.CanonicalHeaderKey(expanded)
}
var newVals []string
for i := range vals {
// append to new slice so we don't overwrite
// the original values in ops.Set
newVals = append(newVals, repl.ReplaceKnown(vals[i], ""))
}
if fieldName == "Set-Cookie" && len(newVals) > 1 {
hdr.Del(fieldName)
for _, val := range newVals {
hdr.Add(fieldName, val)
}
continue
}
hdr.Set(fieldName, strings.Join(newVals, ","))
}
+58
View File
@@ -267,6 +267,64 @@ func TestHandler(t *testing.T) {
}
}
func TestHeaderOpsSetMultipleCookies(t *testing.T) {
want := []string{"a=1; Path=/", "b=2; Path=/"}
for _, tc := range []struct {
fieldName, provisionedName string
}{
{"Set-Cookie", "Set-Cookie"},
{"set-cookie", "Set-Cookie"},
{"sEt-CoOkIe", "Set-Cookie"},
{"{cookie_header}", "{cookie_header}"},
{"set-{cookie_suffix}", "set-{cookie_suffix}"},
} {
t.Run(tc.fieldName, func(t *testing.T) {
ops := HeaderOps{Set: http.Header{tc.fieldName: want}}
if err := ops.Provision(caddy.Context{}); err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(ops.Set, http.Header{tc.provisionedName: want}) {
t.Fatalf("provisioned Set = %v, want field %q", ops.Set, tc.provisionedName)
}
repl := caddy.NewReplacer()
repl.Set("cookie_header", "set-cookie")
repl.Set("cookie_suffix", "cookie")
hdr := http.Header{"Set-Cookie": {"old=1"}}
ops.ApplyTo(hdr, repl)
if got := hdr.Values("Set-Cookie"); !reflect.DeepEqual(got, want) {
t.Errorf("Set-Cookie values = %q, want %q", got, want)
}
})
}
}
func TestHandlerProvisionSetCollision(t *testing.T) {
for _, name := range []string{"request", "response"} {
t.Run(name, func(t *testing.T) {
ops := &HeaderOps{Set: http.Header{
"set-cookie": {"b=2; Path=/"},
"Set-Cookie": {"a=1; Path=/"},
}}
h := Handler{Request: ops}
if name == "response" {
h = Handler{Response: &RespHeaderOps{HeaderOps: ops}}
}
if err := h.Provision(caddy.Context{}); err != nil {
t.Fatal(err)
}
want := http.Header{"Set-Cookie": {"a=1; Path=/", "b=2; Path=/"}}
if !reflect.DeepEqual(ops.Set, want) {
t.Fatalf("provisioned Set = %v, want %v", ops.Set, want)
}
hdr := make(http.Header)
ops.ApplyTo(hdr, caddy.NewReplacer())
if !reflect.DeepEqual(hdr, want) {
t.Errorf("headers = %v, want %v", hdr, want)
}
})
}
}
type nextHandler func(http.ResponseWriter, *http.Request) error
func (f nextHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) error {