148
require.NoError(t, err)
149
assert.EqualValues(t, size, n)
150
}
151
+
152
+// TestTimeoutsTakePrecedence checks that the read timeout of the most
153
+// specific scope applies: a timeouts handler over the server-wide idle
154
+// timeout, and an inner timeouts handler over an outer one.
155
+func TestTimeoutsTakePrecedence(t *testing.T) {
156
+ const short = 100 * time.Millisecond
157
+ const long = 2 * time.Second
158
+ const pause = 300 * time.Millisecond
159
+
160
+ for i, tc := range []struct {
161
+ name string
162
+ server time.Duration
163
+ outer time.Duration
164
+ inner time.Duration
165
+ expected int
166
+ }{
167
+ {name: "route extends server-wide", server: short, inner: long, expected: http.StatusOK},
168
+ {name: "route shortens server-wide", server: long, inner: short, expected: http.StatusRequestTimeout},
169
+ {name: "inner route extends outer", outer: short, inner: long, expected: http.StatusOK},
170
+ {name: "inner route shortens outer", outer: long, inner: short, expected: http.StatusRequestTimeout},
171
+ {name: "inner route extends outer and server-wide", server: short, outer: short, inner: long, expected: http.StatusOK},
172
+ } {
173
+ var handler caddyhttp.Handler = caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
174
+ if _, err := io.Copy(io.Discard, r.Body); err != nil {
175
+ w.WriteHeader(http.StatusRequestTimeout)
176
+ return nil
177
+ }
178
+ w.WriteHeader(http.StatusOK)
179
+ return nil
180
+ })
181
+ for _, timeout := range []time.Duration{tc.inner, tc.outer} {
182
+ if timeout == 0 {
183
+ continue
184
+ }
185
+ tm := Timeouts{ReadTimeout: timeout, logger: zap.NewNop()}
186
+ next := handler
187
+ handler = caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
188
+ return tm.ServeHTTP(w, r, next)
189
+ })
190
+ }
191
+
192
+ srv := httptest.NewServer(noError(caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
193
+ // stand in for the server-wide idle timeout
194
+ if tc.server > 0 {
195
+ reader := &caddyhttp.IdleTimeoutReader{
196
+ ReadCloser: r.Body,
197
+ Ctrl: http.NewResponseController(w),
198
+ Deadline: caddyhttp.IdleDeadline{Start: time.Now(), Timeout: tc.server},
199
+ Logger: zap.NewNop(),
200
+ }
201
+ defer reader.HandlerDone()
202
+ r.Body = reader
203
+ r = r.WithContext(caddyhttp.ContextWithIdleTimeouts(r.Context(), reader, nil))
204
+ }
205
+ return handler.ServeHTTP(w, r)
206
+ })))
207
+
208
+ body := &pacedReader{delay: pause, chunkSize: 8, chunkCount: 2}
209
+ resp, err := http.Post(srv.URL, "application/octet-stream", body)
210
+ if err != nil {
211
+ // a timed out read may close the connection before a response
212
+ if tc.expected != http.StatusRequestTimeout {
213
+ t.Errorf("Test %d (%s): unexpected error: %v", i, tc.name, err)
214
+ }
215
+ } else {
216
+ resp.Body.Close()
217
+ assert.Equal(t, tc.expected, resp.StatusCode, "Test %d (%s)", i, tc.name)
218
+ }
219
+ srv.Close()
220
+ }
221
+}
222
+
223
+func TestTimeoutsOverrideServerWide(t *testing.T) {
224
+ const serverTimeout = time.Hour
225
+ const routeTimeout = time.Minute
226
+
227
+ rec := &readDeadlineRecorder{ResponseRecorder: httptest.NewRecorder()}
228
+ req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("body"))
229
+ reader := &caddyhttp.IdleTimeoutReader{
230
+ ReadCloser: req.Body,
231
+ Ctrl: http.NewResponseController(rec),
232
+ Deadline: caddyhttp.IdleDeadline{Start: time.Now(), Timeout: serverTimeout},
233
+ Logger: zap.NewNop(),
234
+ }
235
+ req.Body = reader
236
+ writer := &caddyhttp.IdleTimeoutWriter{
237
+ ResponseWriterWrapper: &caddyhttp.ResponseWriterWrapper{ResponseWriter: rec},
238
+ Ctrl: http.NewResponseController(rec),
239
+ Deadline: caddyhttp.IdleDeadline{Start: time.Now(), Timeout: serverTimeout},
240
+ MaxChunk: 1000,
241
+ Logger: zap.NewNop(),
242
+ }
243
+ req = req.WithContext(caddyhttp.ContextWithIdleTimeouts(req.Context(), reader, writer))
244
+
245
+ tm := Timeouts{ReadTimeout: routeTimeout, WriteTimeout: routeTimeout, MaxWriteChunk: 100, logger: zap.NewNop()}
246
+ err := tm.ServeHTTP(writer, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
247
+ // overridden in place rather than wrapped again
248
+ assert.Same(t, writer, w)
249
+ assert.Same(t, reader, r.Body)
250
+ assert.Equal(t, routeTimeout, writer.Deadline.Timeout)
251
+ assert.Equal(t, 100, writer.MaxChunk)
252
+
253
+ before := time.Now()
254
+ _, err := r.Body.Read(make([]byte, 1))
255
+ require.NoError(t, err)
256
+ require.Len(t, rec.deadlines, 1)
257
+ assert.WithinRange(t, rec.deadlines[0], before.Add(routeTimeout), time.Now().Add(routeTimeout))
258
+ return nil
259
+ }))
260
+ require.NoError(t, err)
261
+
262
+ // the server-wide settings are back once the handler returns, and
263
+ // the armed read deadline was moved to them
264
+ assert.Equal(t, serverTimeout, writer.Deadline.Timeout)
265
+ assert.Equal(t, 1000, writer.MaxChunk)
266
+ require.Len(t, rec.deadlines, 2)
267
+ assert.WithinRange(t, rec.deadlines[1], time.Now().Add(serverTimeout-time.Second), time.Now().Add(serverTimeout))
268
+
269
+ before := time.Now()
270
+ _, err = reader.Read(make([]byte, 1))
271
+ require.NoError(t, err)
272
+ require.Len(t, rec.deadlines, 3)
273
+ assert.WithinRange(t, rec.deadlines[2], before.Add(serverTimeout), time.Now().Add(serverTimeout))
274
+}
275
+
276
+func TestTimeoutsKeepServerWideMaxWriteChunk(t *testing.T) {
277
+ rec := httptest.NewRecorder()
278
+ writer := &caddyhttp.IdleTimeoutWriter{
279
+ ResponseWriterWrapper: &caddyhttp.ResponseWriterWrapper{ResponseWriter: rec},
280
+ Ctrl: http.NewResponseController(rec),
281
+ Deadline: caddyhttp.IdleDeadline{Timeout: time.Hour},
282
+ MaxChunk: 1000,
283
+ Logger: zap.NewNop(),
284
+ }
285
+ req := httptest.NewRequest(http.MethodGet, "/", nil)
286
+ req = req.WithContext(caddyhttp.ContextWithIdleTimeouts(req.Context(), nil, writer))
287
+
288
+ tm := Timeouts{WriteTimeout: time.Minute, logger: zap.NewNop()}
289
+ err := tm.ServeHTTP(writer, req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
290
+ assert.Equal(t, 1000, writer.MaxChunk)
291
+ return nil
292
+ }))
293
+ require.NoError(t, err)
294
+}
295
+
296
+// TestTimeoutsRearmReadDeadlineHTTP2 checks that a read deadline armed
297
+// before a timeouts handler starts is moved to its timeout. On HTTP/2
298
+// the deadline is a timer that fails the body when it fires, even with
299
+// no read in flight, so it can't wait for the next read to be reset.
300
+func TestTimeoutsRearmReadDeadlineHTTP2(t *testing.T) {
301
+ const serverTimeout = 100 * time.Millisecond
302
+ const routeTimeout = 2 * time.Second
303
+ const pause = 300 * time.Millisecond
304
+
305
+ tm := Timeouts{ReadTimeout: routeTimeout, logger: zap.NewNop()}
306
+ srv := httptest.NewUnstartedServer(noError(caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
307
+ // stand in for the server-wide idle timeout
308
+ reader := &caddyhttp.IdleTimeoutReader{
309
+ ReadCloser: r.Body,
310
+ Ctrl: http.NewResponseController(w),
311
+ Deadline: caddyhttp.IdleDeadline{Start: time.Now(), Timeout: serverTimeout},
312
+ Logger: zap.NewNop(),
313
+ }
314
+ defer reader.HandlerDone()
315
+ r.Body = reader
316
+ r = r.WithContext(caddyhttp.ContextWithIdleTimeouts(r.Context(), reader, nil))
317
+
318
+ // read before the timeouts handler, arming the server-wide deadline
319
+ if _, err := io.ReadFull(r.Body, make([]byte, 8)); err != nil {
320
+ w.WriteHeader(http.StatusBadRequest)
321
+ return nil
322
+ }
323
+ return tm.ServeHTTP(w, r, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
324
+ // pause longer than the server-wide timeout, but not the route's
325
+ time.Sleep(pause)
326
+ if _, err := io.Copy(io.Discard, r.Body); err != nil {
327
+ w.WriteHeader(http.StatusRequestTimeout)
328
+ return nil
329
+ }
330
+ w.WriteHeader(http.StatusOK)
331
+ return nil
332
+ }))
333
+ })))
334
+ srv.EnableHTTP2 = true
335
+ srv.StartTLS()
336
+ defer srv.Close()
337
+
338
+ pr, pw := io.Pipe()
339
+ go func() {
340
+ _, _ = pw.Write(make([]byte, 8))
341
+ time.Sleep(pause + 100*time.Millisecond)
342
+ _, _ = pw.Write(make([]byte, 8))
343
+ _ = pw.Close()
344
+ }()
345
+ resp, err := srv.Client().Post(srv.URL, "application/octet-stream", pr)
346
+ require.NoError(t, err)
347
+ defer resp.Body.Close()
348
+ require.Equal(t, 2, resp.ProtoMajor)
349
+ assert.Equal(t, http.StatusOK, resp.StatusCode)
350
+}