@marcel91200 / caddy Mirror · read-only / commits / 05848752
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, 02:15 UTC

headers: evaluate conditional defaults independently (#8217)

* headers: Check each ? default on its own and keep the response matcher Several ? defaults in one header block shared one require, so a single field already set on the response blocked all of them. A match in the block was also dropped for the ? fields, so they were set on every response. * headers: Reject a response matcher on a field that also has a ? default A ? default already requires its field to be absent, so a match on the same field in that block could only be dropped or never be true. It is now a Caddyfile error. * headers: Test that the last ? default for a field wins * headers: Allow an absence matcher on a field with a ? default `match header !X-Test` asks for what `?X-Test` already requires, so it loads again. Only a matcher that needs a value on that field is an error.

Jay Hemnani committed Oct 16, 2026 at 18:02 UTC 0584875297136c982cba95d0f889d6b851ace083
2 files changed +103 -23
caddytest/integration/caddyfile_test.go
+66
@@ -853,6 +853,72 @@ func TestHandleErrorSubHandlers(t *testing.T) {
853 tester.AssertGetResponse("http://localhost:9080/en/internalerr", 500, "English error")
854 }
855
856 +func TestHeaderDefaults(t *testing.T) {
857 + tester := caddytest.NewTester(t)
858 + tester.InitServer(`{
859 + admin localhost:2999
860 + http_port 9080
861 + }
862 + localhost:9080 {
863 + header Cache-Control "no-store"
864 + header {
865 + ?Cache-Control "max-age=60"
866 + ?X-Frame-Options "DENY"
867 + }
868 + header {
869 + match status 404
870 + ?X-Robots-Tag "noindex"
871 + }
872 + header {
873 + ?X-Dup "first"
874 + ?X-Dup "second"
875 + }
876 + header {
877 + match header !X-Absent
878 + ?X-Absent "fallback"
879 + }
880 + respond /missing 404
881 + respond "ok"
882 + }
883 + `, "caddyfile")
884 +
885 + resp, _ := tester.AssertGetResponse("http://localhost:9080/", 200, "ok")
886 + if got := resp.Header.Get("Cache-Control"); got != "no-store" {
887 + t.Errorf("Cache-Control = %q, want %q", got, "no-store")
888 + }
889 + if got := resp.Header.Get("X-Frame-Options"); got != "DENY" {
890 + t.Errorf("X-Frame-Options = %q, want %q", got, "DENY")
891 + }
892 + if got := resp.Header.Get("X-Robots-Tag"); got != "" {
893 + t.Errorf("X-Robots-Tag on a 200 = %q, want none", got)
894 + }
895 + if got := resp.Header.Get("X-Dup"); got != "second" {
896 + t.Errorf("X-Dup = %q, want %q", got, "second")
897 + }
898 + if got := resp.Header.Get("X-Absent"); got != "fallback" {
899 + t.Errorf("X-Absent = %q, want %q", got, "fallback")
900 + }
901 +
902 + resp, _ = tester.AssertGetResponse("http://localhost:9080/missing", 404, "")
903 + if got := resp.Header.Get("X-Robots-Tag"); got != "noindex" {
904 + t.Errorf("X-Robots-Tag on a 404 = %q, want %q", got, "noindex")
905 + }
906 +}
907 +
908 +func TestHeaderDefaultMatcherSameField(t *testing.T) {
909 + caddytest.AssertLoadError(t, `{
910 + admin localhost:2999
911 + http_port 9080
912 + }
913 + localhost:9080 {
914 + header {
915 + match header X-Frame-Options SAMEORIGIN
916 + ?x-frame-options "DENY"
917 + }
918 + }
919 + `, "caddyfile", "cannot match on response header X-Frame-Options and also set a default for it")
920 +}
921 +
922 func TestInvalidSiteAddressesAsDirectives(t *testing.T) {
923 type testCase struct {
924 config, expectedError string
modules/caddyhttp/headers/caddyfile.go
+37 -23
@@ -61,7 +61,19 @@ func parseCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)
61 },
62 }
63 }
64 - handler, handlerWithRequire := makeHandler(), makeHandler()
64 + handler := makeHandler()
65 +
66 + // one handler per '?' default, each requiring only its own field to be absent
67 + var defaultHandlers []Handler
68 + applyOp := func(field, value string, replacement *string) error {
69 + if !strings.HasPrefix(field, "?") {
70 + return applyHeaderOp(handler.Response.HeaderOps, handler.Response, field, value, replacement)
71 + }
72 + defaultHandler := makeHandler()
73 + defaultHandlers = append(defaultHandlers, defaultHandler)
74 + return applyHeaderOp(defaultHandler.Response.HeaderOps, defaultHandler.Response, field, value, replacement)
75 + }
76 + var responseMatcher *caddyhttp.ResponseMatcher
77
78 // first see if headers are in the initial line
79 var hasArgs bool
@@ -77,13 +89,7 @@ func parseCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)
89 arg := h.Val()
90 replacement = &arg
91 }
80 - err := applyHeaderOp(
81 - handler.Response.HeaderOps,
82 - handler.Response,
83 - field,
84 - value,
85 - replacement,
86 - )
92 + err := applyOp(field, value, replacement)
93 if err != nil {
94 return nil, h.Err(err.Error())
95 }
@@ -106,7 +112,7 @@ func parseCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)
112 return nil, err
113 }
114 matcher := responseMatchers["match"]
109 - handler.Response.Require = &matcher
115 + responseMatcher = &matcher
116 continue
117 }
118 if hasArgs {
@@ -128,29 +134,37 @@ func parseCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)
134 replacement = &arg
135 }
136
131 - handlerToUse := handler
132 - if strings.HasPrefix(field, "?") {
133 - handlerToUse = handlerWithRequire
134 - }
135 -
136 - err := applyHeaderOp(
137 - handlerToUse.Response.HeaderOps,
138 - handlerToUse.Response,
139 - field,
140 - value,
141 - replacement,
142 - )
137 + err := applyOp(field, value, replacement)
138 if err != nil {
139 return nil, h.Err(err.Error())
140 }
141 }
142
143 + // the response matcher also applies to the defaults; a default already
144 + // requires its own field to be absent, so the matcher can only ask the same
145 + if responseMatcher != nil {
146 + handler.Response.Require = responseMatcher
147 + for _, defaultHandler := range defaultHandlers {
148 + require := defaultHandler.Response.Require
149 + require.StatusCode = responseMatcher.StatusCode
150 + for field, vals := range responseMatcher.Headers {
151 + if _, ok := defaultHandler.Response.Set[http.CanonicalHeaderKey(field)]; ok {
152 + if vals == nil {
153 + continue
154 + }
155 + return nil, h.Errf("cannot match on response header %s and also set a default for it", http.CanonicalHeaderKey(field))
156 + }
157 + require.Headers[field] = vals
158 + }
159 + }
160 + }
161 +
162 var configValues []httpcaddyfile.ConfigValue
163 if !reflect.DeepEqual(handler, makeHandler()) {
164 configValues = append(configValues, h.NewRoute(matcherSet, handler)...)
165 }
152 - if !reflect.DeepEqual(handlerWithRequire, makeHandler()) {
153 - configValues = append(configValues, h.NewRoute(matcherSet, handlerWithRequire)...)
166 + for _, defaultHandler := range defaultHandlers {
167 + configValues = append(configValues, h.NewRoute(matcherSet, defaultHandler)...)
168 }
169
170 return configValues, nil