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

caddyhttp: only bound HTTP/2 body reads in flight with the idle timeout (#8125)

* caddyhttp: only bound HTTP/2 body reads in flight with the idle timeout On HTTP/2, the read deadline is a timer that fails the request body when it fires, even with no read in flight. IdleTimeoutReader left the idle deadline armed between reads, so a handler that stopped reading for longer than read_body_idle (1 minute by default) lost the body, although the client was only held back by flow control. For example, reverse_proxy uploading to an upstream that's slow to accept the body failed with a 502. This is the read side of #8118. Like IdleTimeoutWriter does since read on HTTP/2, so the idle deadline only bounds reads in flight. A client that stops sending is still cut off, since the deadline is set before each read. HTTP/1 and HTTP/3 only check the deadline during a read, so they're unchanged. Written with Claude Code (Claude Opus 5.5); the human submitter is responsible for reviewing it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * caddyhttp: track a released hard read deadline as armed From review: with HardDeadline set, releasing the idle deadline between reads left an HTTP/2 timer armed (at HardDeadline) while deadlineSet recorded that none was, so clearDeadlineLocked would skip clearing it. Track which deadline is armed (none, idle, or only the hard one) instead of a bool, and have each check ask the question it means: re-arming and releasing only apply to an armed idle deadline, while clearing applies to any. The writer's deadlineSet already only means "idle deadline armed", since nothing clears the hard deadline it leaves; document that. Written with Claude Code (Claude Opus 5.5); the human submitter is responsible for reviewing it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * caddyhttp: clear released hard deadline at handler end, skip no-op sets From review: - After a partial read releases the idle deadline to HardDeadline, a handler that returns without reading again left that timer armed past HandlerDone, which only cleared after a terminal error. HandlerDone now also clears a released hard deadline. - When the idle deadline is capped to HardDeadline, the reader and writer set the same deadline again when releasing it, and the writer once more in HandlerDone. On HTTP/2 each is a message to the serve loop. Both now remember the deadline they armed and skip setting it again. Within a request, only these wrappers set the client's deadlines. Replaces the earlier test that faked a terminal error with the partial read followed directly by HandlerDone. Written with Claude Code (Claude Opus 5.5); the human submitter is responsible for reviewing it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * caddyhttp: only skip re-setting an armed hard deadline Skipping any deadline equal to the one already armed also skipped idle deadlines on Windows, where two calls to time.Now in the same clock tick return the same time, so successive idle deadlines can be equal. That broke tests counting deadline calls there. Only skip HardDeadline, the case from review where the idle deadline is capped to it; other deadlines are always set. Written with Claude Code (Claude Opus 5.5); the human submitter is responsible for reviewing it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * caddyhttp: restore deadlines without trusting cached transport state Always set the requested read and write deadlines because another handler can change them through its own response controller. Remove the armedAt shortcut and cover intervening deadline changes for reads, writes, ReadFrom, flushes, and HandlerDone. AI-assisted with OpenAI Codex (GPT-6); tested under human direction. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

Francis Lavoie committed Oct 5, 2026 at 17:47 UTC 30d3bd2a8959d4d00ca72446f23a59a09f02a83b
4 files changed +338 -29
modules/caddyhttp/idletimeout.go
+86 -24
@@ -122,13 +122,38 @@ type IdleTimeoutReader struct {
122 // HTTP/2) for other protocols or requests without a body.
123 DrainDeadline bool
124
125 + // ClearBetweenReads makes the reader put back HardDeadline (no
126 + // deadline unless a hard ceiling is configured) after every
127 + // successful read, so the idle deadline only bounds reads in
128 + // flight. It is required for HTTP/2, where the read deadline is a
129 + // timer that fails the body when it fires, even with no read in
130 + // flight: otherwise a handler that stops reading for longer than the
131 + // timeout (e.g. while an upstream is slow to accept the body) loses
132 + // the body, although the client is only held back by flow control.
133 + // Other protocols only check the deadline during a read.
134 + ClearBetweenReads bool
135 +
136 mu sync.Mutex
137 unsupported bool
127 - deadlineSet bool
138 + armed armedDeadline
139 finished bool
140 terminalErr error
141 }
142
143 +// armedDeadline is which read deadline IdleTimeoutReader has armed.
144 +type armedDeadline uint8
145 +
146 +const (
147 + // deadlineNone means no deadline is armed.
148 + deadlineNone armedDeadline = iota
149 + // deadlineIdle means the idle deadline from IdleDeadline.next is
150 + // armed.
151 + deadlineIdle
152 + // deadlineHard means only HardDeadline is armed, as put back
153 + // between reads with ClearBetweenReads.
154 + deadlineHard
155 +)
156 +
157 func (r *IdleTimeoutReader) Read(p []byte) (int, error) {
158 r.mu.Lock()
159 if r.terminalErr != nil {
@@ -150,6 +175,8 @@ func (r *IdleTimeoutReader) Read(p []byte) (int, error) {
175 if !r.finished {
176 r.clearDeadlineLocked()
177 }
178 + } else if r.ClearBetweenReads && !r.finished {
179 + r.releaseDeadlineLocked()
180 }
181 r.mu.Unlock()
182
@@ -158,8 +185,9 @@ func (r *IdleTimeoutReader) Read(p []byte) (int, error) {
185
186 // HandlerDone prevents later body reads from using the response controller.
187 // If the body is not finished and DrainDeadline is set, it leaves an idle
161 -// deadline armed for net/http's post-handler drain. It must be called before
162 -// the installing handler returns.
188 +// deadline armed for net/http's post-handler drain. Otherwise, it clears a
189 +// deadline left from a finished body or put back between reads. It must be
190 +// called before the installing handler returns.
191 func (r *IdleTimeoutReader) HandlerDone() {
192 r.mu.Lock()
193 defer r.mu.Unlock()
@@ -167,9 +195,9 @@ func (r *IdleTimeoutReader) HandlerDone() {
195 if r.finished {
196 return
197 }
170 - if r.terminalErr != nil {
198 + if r.terminalErr != nil || r.armed == deadlineHard {
199 r.clearDeadlineLocked()
172 - } else if r.DrainDeadline && !r.deadlineSet && !r.unsupported {
200 + } else if r.DrainDeadline && r.armed != deadlineIdle && !r.unsupported {
201 r.setDeadlineLocked("could not set final read deadline")
202 }
203 r.finished = true
@@ -199,34 +227,61 @@ func (r *IdleTimeoutReader) Override(timeout time.Duration, minRate int64) (rest
227
228 // rearmLocked moves an armed deadline to what the current settings give.
229 func (r *IdleTimeoutReader) rearmLocked() {
202 - if r.deadlineSet && !r.finished && !r.unsupported {
230 + if r.armed == deadlineIdle && !r.finished && !r.unsupported {
231 r.setDeadlineLocked("could not set read deadline")
232 }
233 }
234
235 func (r *IdleTimeoutReader) setDeadlineLocked(logMessage string) {
208 - if err := r.Ctrl.SetReadDeadline(r.Deadline.next()); err != nil {
236 + if r.setReadDeadlineLocked(r.Deadline.next(), logMessage) {
237 + r.armed = deadlineIdle
238 + }
239 +}
240 +
241 +// setReadDeadlineLocked always sets the requested deadline: another handler
242 +// may have changed it through its own response controller since our last call.
243 +// It reports whether the deadline was set successfully.
244 +func (r *IdleTimeoutReader) setReadDeadlineLocked(deadline time.Time, logMessage string) bool {
245 + if err := r.Ctrl.SetReadDeadline(deadline); err != nil {
246 r.unsupported = true
247 if c := r.Logger.Check(zapcore.DebugLevel, logMessage); c != nil {
248 c.Write(zap.Error(err))
249 }
250 + return false
251 + }
252 + return true
253 +}
254 +
255 +// releaseDeadlineLocked replaces the idle deadline with HardDeadline.
256 +func (r *IdleTimeoutReader) releaseDeadlineLocked() {
257 + if r.armed != deadlineIdle || r.unsupported {
258 return
259 }
215 - r.deadlineSet = true
260 +
261 + // once the hard deadline has passed, the deadline set before the
262 + // read (capped to it) has already expired; keep it
263 + if !r.Deadline.HardDeadline.IsZero() && !time.Now().Before(r.Deadline.HardDeadline) {
264 + return
265 + }
266 +
267 + if !r.setReadDeadlineLocked(r.Deadline.HardDeadline, "could not release read deadline") {
268 + return
269 + }
270 + if r.Deadline.HardDeadline.IsZero() {
271 + r.armed = deadlineNone
272 + } else {
273 + r.armed = deadlineHard
274 + }
275 }
276
277 func (r *IdleTimeoutReader) clearDeadlineLocked() {
219 - if !r.deadlineSet {
278 + if r.armed == deadlineNone {
279 return
280 }
222 - if err := r.Ctrl.SetReadDeadline(time.Time{}); err != nil {
223 - r.unsupported = true
224 - if c := r.Logger.Check(zapcore.DebugLevel, "could not clear read deadline"); c != nil {
225 - c.Write(zap.Error(err))
226 - }
281 + if !r.setReadDeadlineLocked(time.Time{}, "could not clear read deadline") {
282 return
283 }
229 - r.deadlineSet = false
284 + r.armed = deadlineNone
285 }
286
287 // IdleTimeoutWriter wraps a ResponseWriter with IdleDeadline, resetting
@@ -270,6 +325,8 @@ type IdleTimeoutWriter struct {
325 ClearBetweenWrites bool
326
327 unsupported bool
328 + // deadlineSet is whether the idle deadline is armed. clearDeadline
329 + // leaves HardDeadline armed, if any, which nothing needs to undo.
330 deadlineSet bool
331 unflushed bool
332 }
@@ -279,14 +336,23 @@ func (w *IdleTimeoutWriter) resetDeadline() {
336 return
337 }
338
282 - if err := w.Ctrl.SetWriteDeadline(w.Deadline.next()); err != nil {
339 + if w.setWriteDeadline(w.Deadline.next(), "could not set write deadline") {
340 + w.deadlineSet = true
341 + }
342 +}
343 +
344 +// setWriteDeadline always sets the requested deadline: another handler may
345 +// have changed it through its own response controller since our last call.
346 +// It reports whether the deadline was set successfully.
347 +func (w *IdleTimeoutWriter) setWriteDeadline(deadline time.Time, logMessage string) bool {
348 + if err := w.Ctrl.SetWriteDeadline(deadline); err != nil {
349 w.unsupported = true
284 - if c := w.Logger.Check(zapcore.DebugLevel, "could not set write deadline"); c != nil {
350 + if c := w.Logger.Check(zapcore.DebugLevel, logMessage); c != nil {
351 c.Write(zap.Error(err))
352 }
287 - return
353 + return false
354 }
289 - w.deadlineSet = true
355 + return true
356 }
357
358 // clearDeadline replaces the idle deadline set by resetDeadline with
@@ -303,11 +369,7 @@ func (w *IdleTimeoutWriter) clearDeadline() {
369 return
370 }
371
306 - if err := w.Ctrl.SetWriteDeadline(w.Deadline.HardDeadline); err != nil {
307 - w.unsupported = true
308 - if c := w.Logger.Check(zapcore.DebugLevel, "could not clear write deadline"); c != nil {
309 - c.Write(zap.Error(err))
310 - }
372 + if !w.setWriteDeadline(w.Deadline.HardDeadline, "could not clear write deadline") {
373 return
374 }
375 w.deadlineSet = false
modules/caddyhttp/idletimeout_test.go
+246 -1
@@ -22,6 +22,7 @@ import (
22 "io"
23 "net/http"
24 "net/http/httptest"
25 + "os"
26 "strings"
27 "sync"
28 "testing"
@@ -649,6 +650,7 @@ func TestIdleTimeoutWriter_ClearBetweenWrites(t *testing.T) {
650 name string
651 clear bool
652 hard bool
653 + idle time.Duration
654 ops func(w *IdleTimeoutWriter) error
655 expected []string
656 }{
@@ -717,6 +719,23 @@ func TestIdleTimeoutWriter_ClearBetweenWrites(t *testing.T) {
719 },
720 expected: []string{idleSet, zeroSet, idleSet, zeroSet},
721 },
722 + {
723 + name: "hard deadline is restored for each operation",
724 + clear: true,
725 + hard: true,
726 + idle: 3 * time.Hour,
727 + ops: func(w *IdleTimeoutWriter) error {
728 + if _, err := w.Write([]byte("x")); err != nil {
729 + return err
730 + }
731 + if _, err := w.Write([]byte("x")); err != nil {
732 + return err
733 + }
734 + w.HandlerDone()
735 + return nil
736 + },
737 + expected: []string{hardSet, hardSet, hardSet, hardSet, hardSet},
738 + },
739 {
740 name: "handler done does nothing when not clearing",
741 ops: func(w *IdleTimeoutWriter) error {
@@ -727,11 +746,15 @@ func TestIdleTimeoutWriter_ClearBetweenWrites(t *testing.T) {
746 expected: []string{idleSet},
747 },
748 } {
749 + timeout := idle
750 + if tc.idle > 0 {
751 + timeout = tc.idle
752 + }
753 rec := &writeDeadlineRecorder{ResponseRecorder: httptest.NewRecorder()}
754 w := &IdleTimeoutWriter{
755 ResponseWriterWrapper: &ResponseWriterWrapper{ResponseWriter: rec},
756 Ctrl: http.NewResponseController(rec),
734 - Deadline: IdleDeadline{Timeout: idle},
757 + Deadline: IdleDeadline{Timeout: timeout},
758 Logger: zap.NewNop(),
759 ClearBetweenWrites: tc.clear,
760 }
@@ -922,3 +945,225 @@ func TestIdleTimeoutReaderOverrideSuspendsMinRate(t *testing.T) {
945 assert.True(t, deadlines[len(deadlines)-1].After(time.Now()),
946 "the restored server-wide deadline should not have expired while overridden")
947 }
948 +
949 +// TestIdleTimeoutReaderPausedHandlerHTTP2 checks that a handler that
950 +// stops reading the body for longer than the idle timeout doesn't lose
951 +// it on HTTP/2, where the read deadline is a timer that fails the body
952 +// when it fires, even with no read in flight. The client isn't stalled:
953 +// it's held back by flow control while the handler isn't reading.
954 +func TestIdleTimeoutReaderPausedHandlerHTTP2(t *testing.T) {
955 + const idle = 100 * time.Millisecond
956 + const pause = 300 * time.Millisecond
957 + const size = 4 << 20 // more than the HTTP/2 flow control window
958 +
959 + srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
960 + reader := &IdleTimeoutReader{
961 + ReadCloser: r.Body,
962 + Ctrl: http.NewResponseController(w),
963 + Deadline: IdleDeadline{Start: time.Now(), Timeout: idle},
964 + Logger: zap.NewNop(),
965 + ClearBetweenReads: r.ProtoMajor == 2,
966 + }
967 + defer reader.HandlerDone()
968 +
969 + if _, err := io.ReadFull(reader, make([]byte, 8)); err != nil {
970 + http.Error(w, err.Error(), http.StatusBadRequest)
971 + return
972 + }
973 + time.Sleep(pause)
974 + n, err := io.Copy(io.Discard, reader)
975 + if err != nil {
976 + http.Error(w, err.Error(), http.StatusRequestTimeout)
977 + return
978 + }
979 + _, _ = fmt.Fprint(w, n+8)
980 + }))
981 + srv.EnableHTTP2 = true
982 + srv.StartTLS()
983 + defer srv.Close()
984 +
985 + resp, err := srv.Client().Post(srv.URL, "application/octet-stream", bytes.NewReader(make([]byte, size)))
986 + require.NoError(t, err)
987 + defer resp.Body.Close()
988 + require.Equal(t, 2, resp.ProtoMajor)
989 + body, err := io.ReadAll(resp.Body)
990 + require.NoError(t, err)
991 + assert.Equal(t, http.StatusOK, resp.StatusCode, string(body))
992 + assert.Equal(t, fmt.Sprint(size), string(body))
993 +}
994 +
995 +// TestIdleTimeoutReaderStalledClientHTTP2 checks that clearing the
996 +// deadline between reads still aborts a read from a client that stops
997 +// sending.
998 +func TestIdleTimeoutReaderStalledClientHTTP2(t *testing.T) {
999 + const idle = 100 * time.Millisecond
1000 +
1001 + readErr := make(chan error, 1)
1002 + srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1003 + reader := &IdleTimeoutReader{
1004 + ReadCloser: r.Body,
1005 + Ctrl: http.NewResponseController(w),
1006 + Deadline: IdleDeadline{Start: time.Now(), Timeout: idle},
1007 + Logger: zap.NewNop(),
1008 + ClearBetweenReads: r.ProtoMajor == 2,
1009 + }
1010 + defer reader.HandlerDone()
1011 + _, err := io.Copy(io.Discard, reader)
1012 + readErr <- err
1013 + }))
1014 + srv.EnableHTTP2 = true
1015 + srv.StartTLS()
1016 + defer srv.Close()
1017 +
1018 + pr, pw := io.Pipe()
1019 + defer pw.Close()
1020 + go func() { _, _ = pw.Write(make([]byte, 8)) }() // then stall
1021 + go func() {
1022 + resp, err := srv.Client().Post(srv.URL, "application/octet-stream", pr)
1023 + if err == nil {
1024 + resp.Body.Close()
1025 + }
1026 + }()
1027 +
1028 + select {
1029 + case err := <-readErr:
1030 + assert.ErrorIs(t, err, os.ErrDeadlineExceeded)
1031 + case <-time.After(5 * time.Second):
1032 + t.Fatal("read from a stalled client was not aborted")
1033 + }
1034 +}
1035 +
1036 +func TestIdleTimeoutReaderClearBetweenReads(t *testing.T) {
1037 + const idle = time.Hour
1038 + hard := time.Now().Add(2 * time.Hour)
1039 +
1040 + for i, tc := range []struct {
1041 + clear bool
1042 + hard bool
1043 + expected []string
1044 + }{
1045 + // a read of "ab", then one returning io.EOF
1046 + {clear: false, expected: []string{"idle", "idle", "zero"}},
1047 + {clear: true, expected: []string{"idle", "zero", "idle", "zero"}},
1048 + {clear: true, hard: true, expected: []string{"idle", "hard", "idle", "zero"}},
1049 + } {
1050 + w := &readDeadlineRecorder{ResponseRecorder: httptest.NewRecorder()}
1051 + r := &IdleTimeoutReader{
1052 + ReadCloser: io.NopCloser(strings.NewReader("ab")),
1053 + Ctrl: http.NewResponseController(w),
1054 + Deadline: IdleDeadline{Timeout: idle},
1055 + Logger: zap.NewNop(),
1056 + ClearBetweenReads: tc.clear,
1057 + }
1058 + if tc.hard {
1059 + r.Deadline.HardDeadline = hard
1060 + }
1061 +
1062 + before := time.Now()
1063 + _, err := io.Copy(io.Discard, r)
1064 + after := time.Now()
1065 + require.NoError(t, err)
1066 +
1067 + var actual []string
1068 + for _, d := range w.snapshot() {
1069 + switch {
1070 + case d.IsZero():
1071 + actual = append(actual, "zero")
1072 + case d.Equal(hard):
1073 + actual = append(actual, "hard")
1074 + case !d.Before(before.Add(idle)) && !d.After(after.Add(idle)):
1075 + actual = append(actual, "idle")
1076 + default:
1077 + actual = append(actual, "other")
1078 + }
1079 + }
1080 + assert.Equal(t, tc.expected, actual, "Test %d", i)
1081 + }
1082 +}
1083 +
1084 +// TestIdleTimeoutReaderHandlerDoneClearsReleasedHardDeadline checks
1085 +// that the hard deadline put back after a partial read doesn't outlive
1086 +// the handler when it returns without reading the rest of the body.
1087 +func TestIdleTimeoutReaderHandlerDoneClearsReleasedHardDeadline(t *testing.T) {
1088 + hard := time.Now().Add(time.Hour)
1089 + w := &readDeadlineRecorder{ResponseRecorder: httptest.NewRecorder()}
1090 + r := &IdleTimeoutReader{
1091 + ReadCloser: io.NopCloser(strings.NewReader("abcd")),
1092 + Ctrl: http.NewResponseController(w),
1093 + Deadline: IdleDeadline{Timeout: time.Minute, HardDeadline: hard},
1094 + Logger: zap.NewNop(),
1095 + ClearBetweenReads: true,
1096 + }
1097 +
1098 + _, err := r.Read(make([]byte, 2))
1099 + require.NoError(t, err)
1100 + deadlines := w.snapshot()
1101 + require.Len(t, deadlines, 2)
1102 + assert.Equal(t, hard, deadlines[1], "the idle deadline should be released to the hard one")
1103 +
1104 + r.HandlerDone()
1105 + deadlines = w.snapshot()
1106 + require.Len(t, deadlines, 3)
1107 + assert.True(t, deadlines[2].IsZero(), "the released hard deadline should be cleared")
1108 +}
1109 +
1110 +// TestIdleTimeoutReaderRestoresHardDeadline checks that another handler's
1111 +// deadline update between reads cannot bypass the wrapper's hard ceiling.
1112 +func TestIdleTimeoutReaderRestoresHardDeadline(t *testing.T) {
1113 + for _, clear := range []bool{false, true} {
1114 + t.Run(fmt.Sprintf("clear=%t", clear), func(t *testing.T) {
1115 + hard := time.Now().Add(time.Minute)
1116 + rec := &readDeadlineRecorder{ResponseRecorder: httptest.NewRecorder()}
1117 + ctrl := http.NewResponseController(rec)
1118 + r := &IdleTimeoutReader{
1119 + ReadCloser: io.NopCloser(strings.NewReader("abcd")),
1120 + Ctrl: ctrl,
1121 + Deadline: IdleDeadline{Timeout: time.Hour, HardDeadline: hard},
1122 + Logger: zap.NewNop(),
1123 + ClearBetweenReads: clear,
1124 + }
1125 + _, err := r.Read(make([]byte, 2))
1126 + require.NoError(t, err)
1127 + // A handler can use its own response controller to change the deadline.
1128 + require.NoError(t, ctrl.SetReadDeadline(time.Time{}))
1129 + _, err = r.Read(make([]byte, 2))
1130 + require.NoError(t, err)
1131 + deadlines := rec.snapshot()
1132 + assert.Equal(t, hard, deadlines[len(deadlines)-1])
1133 + })
1134 + }
1135 +}
1136 +
1137 +// TestIdleTimeoutWriterRestoresHardDeadline checks that another handler's
1138 +// deadline update cannot bypass the hard ceiling on writes or the final flush.
1139 +func TestIdleTimeoutWriterRestoresHardDeadline(t *testing.T) {
1140 + for _, op := range []string{"write", "read from", "flush", "handler done"} {
1141 + t.Run(op, func(t *testing.T) {
1142 + hard := time.Now().Add(time.Minute)
1143 + rec := &writeDeadlineRecorder{ResponseRecorder: httptest.NewRecorder()}
1144 + ctrl := http.NewResponseController(rec)
1145 + w := &IdleTimeoutWriter{
1146 + ResponseWriterWrapper: &ResponseWriterWrapper{ResponseWriter: rec},
1147 + Ctrl: ctrl,
1148 + Deadline: IdleDeadline{Timeout: time.Hour, HardDeadline: hard},
1149 + Logger: zap.NewNop(),
1150 + ClearBetweenWrites: true,
1151 + }
1152 + _, err := w.Write([]byte("x"))
1153 + require.NoError(t, err)
1154 + require.NoError(t, ctrl.SetWriteDeadline(time.Time{}))
1155 + switch op {
1156 + case "write":
1157 + _, err = w.Write([]byte("x"))
1158 + case "read from":
1159 + _, err = w.ReadFrom(strings.NewReader("x"))
1160 + case "flush":
1161 + err = w.FlushError()
1162 + case "handler done":
1163 + w.HandlerDone()
1164 + }
1165 + require.NoError(t, err)
1166 + assert.Equal(t, hard, rec.deadlines[len(rec.deadlines)-1])
1167 + })
1168 + }
1169 +}
modules/caddyhttp/server.go
+3 -2
@@ -597,8 +597,9 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
597 MinRate: s.ReadMinRate,
598 HardDeadline: readHardDeadline,
599 },
600 - Logger: s.logger,
601 - DrainDeadline: r.ProtoMajor == 1 && r.ContentLength != 0,
600 + Logger: s.logger,
601 + DrainDeadline: r.ProtoMajor == 1 && r.ContentLength != 0,
602 + ClearBetweenReads: r.ProtoMajor == 2,
603 }
604 defer idleReader.HandlerDone()
605 r.Body = idleReader
modules/caddyhttp/timeouts/timeouts.go
+3 -2
@@ -114,8 +114,9 @@ func (t Timeouts) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
114 Timeout: t.ReadTimeout,
115 MinRate: t.ReadMinRate,
116 },
117 - Logger: t.logger,
118 - DrainDeadline: r.ProtoMajor == 1 && r.ContentLength != 0,
117 + Logger: t.logger,
118 + DrainDeadline: r.ProtoMajor == 1 && r.ContentLength != 0,
119 + ClearBetweenReads: r.ProtoMajor == 2,
120 }
121 defer idleReader.HandlerDone()
122 r.Body = idleReader