reverseproxy: Fix partial response not flushed to clients (#7849)

* flush buffer before aborting
* add tests
* close test http server after tests

---------

Signed-off-by: WeidiDeng <weidi_deng@icloud.com>
This commit is contained in:
WeidiDeng authored and GitHub committed 2026-09-25 22:36:28 +10:00
1 parent 4845150fa6
commit 89db4b75c3
2 files changed
+113

No files matched your search

+109
View File
@@ -7,6 +7,7 @@ import (
"io"
"net"
"net/http"
"net/http/httptest"
"net/textproto"
"os"
"runtime"
@@ -994,3 +995,111 @@ func TestWeightedRoundRobinSelectionValidation(t *testing.T) {
})
}
}
func TestReverseProxyResponseHandling(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/header", func(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set("Content-Length", "500")
writer.WriteHeader(500)
_ = http.NewResponseController(writer).Flush()
panic(http.ErrAbortHandler)
})
mux.HandleFunc("/partial", func(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set("Content-Length", "500")
writer.WriteHeader(500)
_, _ = io.WriteString(writer, "partial")
_ = http.NewResponseController(writer).Flush()
panic(http.ErrAbortHandler)
})
mux.HandleFunc("/full", func(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set("Content-Length", "500")
writer.WriteHeader(500)
for range 125 {
_, _ = io.WriteString(writer, "full")
}
_ = http.NewResponseController(writer).Flush()
})
mux.HandleFunc("/empty", func(writer http.ResponseWriter, request *http.Request) {
panic(http.ErrAbortHandler)
})
ts := httptest.NewUnstartedServer(mux)
ts.Start()
t.Cleanup(func() {
ts.Close()
})
tester := caddytest.NewTester(t)
tester.InitServer(fmt.Sprintf(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
grace_period 1ns
}
http://localhost:9080 {
reverse_proxy %s
}
`, ts.URL), "caddyfile")
for _, tc := range []struct {
endpoint string
status int
bodyType int // 0 = no body, 1 = partial, 2 = full
body string
}{
{
endpoint: "/header",
status: 500,
bodyType: 0,
body: "",
},
{
endpoint: "/partial",
status: 500,
bodyType: 1,
body: "partial",
},
{
endpoint: "/full",
status: 500,
bodyType: 2,
body: strings.Repeat("full", 125),
},
{
endpoint: "/empty",
status: 502,
bodyType: 2,
body: "",
},
} {
req, err := http.NewRequest("GET", "http://localhost:9080"+tc.endpoint, nil)
if err != nil {
t.Fatalf("unable to create request %s for endpoint %s", err, tc.endpoint)
}
resp, err := tester.Client.Do(req)
if err != nil {
t.Fatalf("request failed: %s for endpoint %s", err, tc.endpoint)
}
if resp.StatusCode != tc.status {
t.Fatalf("unexpected status code for %s: got %d, want %d", tc.endpoint, resp.StatusCode, tc.status)
}
body, err := io.ReadAll(resp.Body)
_ = resp.Body.Close()
switch tc.bodyType {
case 0, 1:
if err == nil {
t.Fatalf("expected error reading body for %s, got none", tc.endpoint)
}
case 2:
if err != nil {
t.Fatalf("error reading body for %s: %s", tc.endpoint, err)
}
}
if string(body) != tc.body {
t.Fatalf("unexpected body for %s: got %q, want %q", tc.endpoint, string(body), tc.body)
}
}
}
@@ -1301,6 +1301,10 @@ func (h *Handler) finalizeResponse(
if c := logger.Check(zapcore.WarnLevel, "aborting with incomplete response"); c != nil {
c.Write(zap.Error(err))
}
// flush the buffer to ensure the client sees the partial response
// see: https://github.com/caddyserver/caddy/issues/7845
//nolint:bodyclose
http.NewResponseController(rw).Flush()
// no extra logging from stdlib
panic(http.ErrAbortHandler)
}