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
}