@marcel91200 / caddy Mirror · read-only / commits / 89db4b75
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, 04:45 UTC

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>

WeidiDeng committed Sep 25, 2026 at 20:36 UTC 89db4b75c3ba8d6362ba1d51909063cd55481aa2
2 files changed +113
caddytest/integration/reverseproxy_test.go
+109
@@ -7,6 +7,7 @@ import (
7 "io"
8 "net"
9 "net/http"
10 + "net/http/httptest"
11 "net/textproto"
12 "os"
13 "runtime"
@@ -994,3 +995,111 @@ func TestWeightedRoundRobinSelectionValidation(t *testing.T) {
995 })
996 }
997 }
998 +
999 +func TestReverseProxyResponseHandling(t *testing.T) {
1000 + mux := http.NewServeMux()
1001 + mux.HandleFunc("/header", func(writer http.ResponseWriter, request *http.Request) {
1002 + writer.Header().Set("Content-Length", "500")
1003 + writer.WriteHeader(500)
1004 + _ = http.NewResponseController(writer).Flush()
1005 + panic(http.ErrAbortHandler)
1006 + })
1007 + mux.HandleFunc("/partial", func(writer http.ResponseWriter, request *http.Request) {
1008 + writer.Header().Set("Content-Length", "500")
1009 + writer.WriteHeader(500)
1010 + _, _ = io.WriteString(writer, "partial")
1011 + _ = http.NewResponseController(writer).Flush()
1012 + panic(http.ErrAbortHandler)
1013 + })
1014 + mux.HandleFunc("/full", func(writer http.ResponseWriter, request *http.Request) {
1015 + writer.Header().Set("Content-Length", "500")
1016 + writer.WriteHeader(500)
1017 + for range 125 {
1018 + _, _ = io.WriteString(writer, "full")
1019 + }
1020 + _ = http.NewResponseController(writer).Flush()
1021 + })
1022 + mux.HandleFunc("/empty", func(writer http.ResponseWriter, request *http.Request) {
1023 + panic(http.ErrAbortHandler)
1024 + })
1025 + ts := httptest.NewUnstartedServer(mux)
1026 + ts.Start()
1027 + t.Cleanup(func() {
1028 + ts.Close()
1029 + })
1030 +
1031 + tester := caddytest.NewTester(t)
1032 + tester.InitServer(fmt.Sprintf(`
1033 + {
1034 + skip_install_trust
1035 + admin localhost:2999
1036 + http_port 9080
1037 + https_port 9443
1038 + grace_period 1ns
1039 + }
1040 + http://localhost:9080 {
1041 + reverse_proxy %s
1042 + }
1043 + `, ts.URL), "caddyfile")
1044 +
1045 + for _, tc := range []struct {
1046 + endpoint string
1047 + status int
1048 + bodyType int // 0 = no body, 1 = partial, 2 = full
1049 + body string
1050 + }{
1051 + {
1052 + endpoint: "/header",
1053 + status: 500,
1054 + bodyType: 0,
1055 + body: "",
1056 + },
1057 + {
1058 + endpoint: "/partial",
1059 + status: 500,
1060 + bodyType: 1,
1061 + body: "partial",
1062 + },
1063 + {
1064 + endpoint: "/full",
1065 + status: 500,
1066 + bodyType: 2,
1067 + body: strings.Repeat("full", 125),
1068 + },
1069 + {
1070 + endpoint: "/empty",
1071 + status: 502,
1072 + bodyType: 2,
1073 + body: "",
1074 + },
1075 + } {
1076 + req, err := http.NewRequest("GET", "http://localhost:9080"+tc.endpoint, nil)
1077 + if err != nil {
1078 + t.Fatalf("unable to create request %s for endpoint %s", err, tc.endpoint)
1079 + }
1080 +
1081 + resp, err := tester.Client.Do(req)
1082 + if err != nil {
1083 + t.Fatalf("request failed: %s for endpoint %s", err, tc.endpoint)
1084 + }
1085 + if resp.StatusCode != tc.status {
1086 + t.Fatalf("unexpected status code for %s: got %d, want %d", tc.endpoint, resp.StatusCode, tc.status)
1087 + }
1088 +
1089 + body, err := io.ReadAll(resp.Body)
1090 + _ = resp.Body.Close()
1091 + switch tc.bodyType {
1092 + case 0, 1:
1093 + if err == nil {
1094 + t.Fatalf("expected error reading body for %s, got none", tc.endpoint)
1095 + }
1096 + case 2:
1097 + if err != nil {
1098 + t.Fatalf("error reading body for %s: %s", tc.endpoint, err)
1099 + }
1100 + }
1101 + if string(body) != tc.body {
1102 + t.Fatalf("unexpected body for %s: got %q, want %q", tc.endpoint, string(body), tc.body)
1103 + }
1104 + }
1105 +}
modules/caddyhttp/reverseproxy/reverseproxy.go
+4
@@ -1301,6 +1301,10 @@ func (h *Handler) finalizeResponse(
1301 if c := logger.Check(zapcore.WarnLevel, "aborting with incomplete response"); c != nil {
1302 c.Write(zap.Error(err))
1303 }
1304 + // flush the buffer to ensure the client sees the partial response
1305 + // see: https://github.com/caddyserver/caddy/issues/7845
1306 + //nolint:bodyclose
1307 + http.NewResponseController(rw).Flush()
1308 // no extra logging from stdlib
1309 panic(http.ErrAbortHandler)
1310 }