22
"io"
23
"net/http"
24
"net/http/httptest"
25
+ "os"
26
"strings"
27
"sync"
28
"testing"
650
name string
651
clear bool
652
hard bool
653
+ idle time.Duration
654
ops func(w *IdleTimeoutWriter) error
655
expected []string
656
}{
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 {
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
}
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
+}