@marcel91200 / caddy Mirror · read-only / commits / 16235cce
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, 08:30 UTC

intercept: fix replace_status being silently dropped (#7810)

* test: add failing tests for intercept replace_status (#7805) Add integration tests that verify replace_status actually substitutes the HTTP status code sent to the client. Currently these tests fail because replace_status is silently a no-op due to value-receiver boxing and shouldBuffer returning false. Tests added: - TestInterceptReplaceStatusWithMatcher: 500 -> 200 with @err matcher - TestInterceptReplaceStatusWithoutMatcher: 403 -> 200 unconditionally - TestInterceptReplaceStatusNotMatched: 200 passes through unchanged * fix: make intercept replace_status actually substitute the status code Fix #7805: replace_status was silently a no-op because: 1. shouldBuffer returned false when a replacement status was set, causing the original status to be streamed directly to the wire 2. The value-receiver WriteHeader method operated on a stale copy The fix: - shouldBuffer now returns true when replace_status matches, so the response is buffered instead of streamed - After next.ServeHTTP returns, if routes are nil (replace_status only), write the substituted status and buffered body to the client The interceptedResponseHandler.WriteHeader substitution branch is no longer needed for this path since substitution happens post-ServeHTTP. * refactor: remove dead WriteHeader method and resolved TODO The value-receiver WriteHeader on interceptedResponseHandler was unreachable dead code — the substitution is now handled post-ServeHTTP via buffering. Remove it along with the TODO comment that noted status code replacement was unfinished. * style: apply nit suggestions from dunglas code review Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

long.black committed Jun 13, 2026 at 06:50 UTC 16235cced55f137859aba2c7e590b014a3b92eb2
2 files changed +89 -14
caddytest/integration/intercept_test.go
+69
@@ -38,3 +38,72 @@ func TestIntercept(t *testing.T) {
38
39 tester.AssertGetResponse("http://localhost:9080/no-intercept", 200, "I'm not a teapot")
40 }
41 +
42 +func TestInterceptReplaceStatusWithMatcher(t *testing.T) {
43 + tester := caddytest.NewTester(t)
44 + tester.InitServer(`{
45 + skip_install_trust
46 + admin localhost:2999
47 + http_port 9080
48 + https_port 9443
49 + grace_period 1ns
50 + }
51 +
52 + localhost:9080 {
53 + respond /error "boom" 500
54 +
55 + intercept {
56 + @err status 5xx
57 + replace_status @err 200
58 + }
59 + }
60 + `, "caddyfile")
61 +
62 + tester.AssertGetResponse("http://localhost:9080/error", 200, "boom")
63 +}
64 +
65 +func TestInterceptReplaceStatusWithoutMatcher(t *testing.T) {
66 + tester := caddytest.NewTester(t)
67 + tester.InitServer(`{
68 + skip_install_trust
69 + admin localhost:2999
70 + http_port 9080
71 + https_port 9443
72 + grace_period 1ns
73 + }
74 +
75 + localhost:9080 {
76 + respond /forbidden "denied" 403
77 +
78 + intercept {
79 + replace_status 200
80 + }
81 + }
82 + `, "caddyfile")
83 +
84 + tester.AssertGetResponse("http://localhost:9080/forbidden", 200, "denied")
85 +}
86 +
87 +func TestInterceptReplaceStatusNotMatched(t *testing.T) {
88 + tester := caddytest.NewTester(t)
89 + tester.InitServer(`{
90 + skip_install_trust
91 + admin localhost:2999
92 + http_port 9080
93 + https_port 9443
94 + grace_period 1ns
95 + }
96 +
97 + localhost:9080 {
98 + respond /ok "all good" 200
99 +
100 + intercept {
101 + @err status 5xx
102 + replace_status @err 503
103 + }
104 + }
105 + `, "caddyfile")
106 +
107 + // 200 does not match @err (5xx), so status should pass through unchanged
108 + tester.AssertGetResponse("http://localhost:9080/ok", 200, "all good")
109 +}
modules/caddyhttp/intercept/intercept.go
+20 -14
@@ -97,8 +97,6 @@ var bufPool = sync.Pool{
97 },
98 }
99
100 -// TODO: handle status code replacement
101 -//
100 // EXPERIMENTAL: Subject to change or removal.
101 type interceptedResponseHandler struct {
102 caddyhttp.ResponseRecorder
@@ -108,17 +106,6 @@ type interceptedResponseHandler struct {
106 statusCode int
107 }
108
111 -// EXPERIMENTAL: Subject to change or removal.
112 -func (irh interceptedResponseHandler) WriteHeader(statusCode int) {
113 - if irh.statusCode != 0 && (statusCode < 100 || statusCode >= 200) {
114 - irh.ResponseRecorder.WriteHeader(irh.statusCode)
115 -
116 - return
117 - }
118 -
119 - irh.ResponseRecorder.WriteHeader(statusCode)
120 -}
121 -
109 // EXPERIMENTAL: Subject to change or removal.
110 func (irh interceptedResponseHandler) Unwrap() http.ResponseWriter {
111 return irh.ResponseRecorder
@@ -142,7 +129,7 @@ func (ir Intercept) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddy
129 rec.handlerIndex = i
130
131 // if configured to only change the status code,
145 - // do that then stream
132 + // buffer the response so we can substitute the status
133 if statusCodeStr := rh.StatusCode.String(); statusCodeStr != "" {
134 sc, err := strconv.Atoi(repl.ReplaceAll(statusCodeStr, ""))
135 if err != nil {
@@ -150,6 +137,8 @@ func (ir Intercept) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddy
137 } else {
138 rec.statusCode = sc
139 }
140 +
141 + return true
142 }
143
144 return rec.statusCode == 0
@@ -176,6 +165,23 @@ func (ir Intercept) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddy
165 c.Write(zap.Int("handler", rec.handlerIndex))
166 }
167
168 + // replace_status only: no routes to execute, just substitute status and write body
169 + if rec.handler.Routes == nil {
170 + if rec.statusCode == 0 {
171 + w.WriteHeader(rec.Status())
172 + } else {
173 + w.WriteHeader(rec.statusCode)
174 + }
175 +
176 + if buf.Len() > 0 {
177 + _, err := io.Copy(w, buf)
178 +
179 + return err
180 + }
181 +
182 + return nil
183 + }
184 +
185 // response recorder doesn't create a new copy of the original headers, they're
186 // present in the original response writer
187 // create a new recorder to see if any response body from the new handler is present,