368
assert.False(t, deadlines[0].IsZero())
369
}
370
371
+func TestIdleTimeoutReaderHandlerDoneArmsDrainAfterHandlerDeadline(t *testing.T) {
372
+ w := &readDeadlineRecorder{ResponseRecorder: httptest.NewRecorder()}
373
+ r := &IdleTimeoutReader{
374
+ ReadCloser: io.NopCloser(strings.NewReader("body")),
375
+ Ctrl: http.NewResponseController(w),
376
+ Deadline: IdleDeadline{Timeout: time.Second},
377
+ Logger: zap.NewNop(),
378
+ DrainDeadline: true,
379
+ }
380
+ handlerDeadline := time.Now().Add(time.Hour)
381
+ require.NoError(t, r.SetReadDeadline(handlerDeadline))
382
+
383
+ r.HandlerDone()
384
+ deadlines := w.snapshot()
385
+ require.Len(t, deadlines, 2)
386
+ assert.False(t, deadlines[1].IsZero())
387
+ assert.True(t, deadlines[1].Before(handlerDeadline))
388
+}
389
+
390
func TestIdleTimeoutReaderHandlerDoneWithoutDrainDeadline(t *testing.T) {
391
w := &readDeadlineRecorder{ResponseRecorder: httptest.NewRecorder()}
392
r := &IdleTimeoutReader{
1331
}
1332
}
1333
1334
+func TestIdleTimeoutReader_HandlerDeadline(t *testing.T) {
1335
+ const idle = time.Hour
1336
+ now := time.Now()
1337
+ earlier := now.Add(30 * time.Minute)
1338
+ later := now.Add(2 * time.Hour)
1339
+ hard := now.Add(20 * time.Minute)
1340
+
1341
+ // the values recorded for each SetReadDeadline call
1342
+ const (
1343
+ idleSet = "idle"
1344
+ zeroSet = "zero"
1345
+ earlierSet = "earlier"
1346
+ laterSet = "later"
1347
+ hardSet = "hard"
1348
+ otherSet = "other"
1349
+ )
1350
+
1351
+ for i, tc := range []struct {
1352
+ name string
1353
+ clear bool
1354
+ hard bool
1355
+ deadlines []time.Time
1356
+ expected []string
1357
+ }{
1358
+ {
1359
+ name: "earlier handler deadline caps the idle reset",
1360
+ deadlines: []time.Time{earlier},
1361
+ expected: []string{earlierSet, earlierSet, earlierSet, zeroSet},
1362
+ },
1363
+ {
1364
+ name: "clearing puts back the handler deadline",
1365
+ clear: true,
1366
+ deadlines: []time.Time{earlier},
1367
+ expected: []string{earlierSet, earlierSet, earlierSet, earlierSet, zeroSet},
1368
+ },
1369
+ {
1370
+ name: "later handler deadline doesn't loosen the idle timeout",
1371
+ deadlines: []time.Time{later},
1372
+ expected: []string{laterSet, idleSet, idleSet, zeroSet},
1373
+ },
1374
+ {
1375
+ name: "clearing puts back a later handler deadline",
1376
+ clear: true,
1377
+ deadlines: []time.Time{later},
1378
+ expected: []string{laterSet, idleSet, laterSet, idleSet, zeroSet},
1379
+ },
1380
+ {
1381
+ name: "zero handler deadline removes the ceiling",
1382
+ deadlines: []time.Time{earlier, {}},
1383
+ expected: []string{earlierSet, zeroSet, idleSet, idleSet, zeroSet},
1384
+ },
1385
+ {
1386
+ name: "earlier hard deadline stays the ceiling",
1387
+ clear: true,
1388
+ hard: true,
1389
+ deadlines: []time.Time{earlier},
1390
+ expected: []string{hardSet, hardSet, hardSet, hardSet, zeroSet},
1391
+ },
1392
+ {
1393
+ name: "later handler deadline doesn't lift the hard deadline",
1394
+ hard: true,
1395
+ deadlines: []time.Time{later},
1396
+ expected: []string{hardSet, hardSet, hardSet, zeroSet},
1397
+ },
1398
+ {
1399
+ name: "zero handler deadline keeps the hard deadline",
1400
+ hard: true,
1401
+ deadlines: []time.Time{{}},
1402
+ expected: []string{hardSet, hardSet, hardSet, zeroSet},
1403
+ },
1404
+ } {
1405
+ rec := &readDeadlineRecorder{ResponseRecorder: httptest.NewRecorder()}
1406
+ r := &IdleTimeoutReader{
1407
+ ReadCloser: io.NopCloser(strings.NewReader("ab")),
1408
+ Ctrl: http.NewResponseController(rec),
1409
+ Deadline: IdleDeadline{Timeout: idle},
1410
+ Logger: zap.NewNop(),
1411
+ ClearBetweenReads: tc.clear,
1412
+ }
1413
+ if tc.hard {
1414
+ r.Deadline.HardDeadline = hard
1415
+ }
1416
+
1417
+ // handlers usually reach the reader through other wrappers
1418
+ w := &readDeadlineWriter{ResponseWriterWrapper: ResponseWriterWrapper{ResponseWriter: rec}, reader: r}
1419
+ ctrl := http.NewResponseController(&ResponseWriterWrapper{ResponseWriter: w})
1420
+ for _, d := range tc.deadlines {
1421
+ require.NoError(t, ctrl.SetReadDeadline(d))
1422
+ }
1423
+
1424
+ before := time.Now()
1425
+ _, err := io.Copy(io.Discard, r)
1426
+ require.NoError(t, err)
1427
+ after := time.Now()
1428
+
1429
+ var actual []string
1430
+ for _, d := range rec.snapshot() {
1431
+ switch {
1432
+ case d.IsZero():
1433
+ actual = append(actual, zeroSet)
1434
+ case d.Equal(earlier):
1435
+ actual = append(actual, earlierSet)
1436
+ case d.Equal(later):
1437
+ actual = append(actual, laterSet)
1438
+ case d.Equal(hard):
1439
+ actual = append(actual, hardSet)
1440
+ case !d.Before(before.Add(idle)) && !d.After(after.Add(idle)):
1441
+ actual = append(actual, idleSet)
1442
+ default:
1443
+ actual = append(actual, otherSet)
1444
+ }
1445
+ }
1446
+ assert.Equal(t, tc.expected, actual, "Test %d (%s)", i, tc.name)
1447
+ }
1448
+}
1449
+
1450
+// TestIdleTimeoutReaderHandlerDeadlineDuringRead checks that a handler
1451
+// can bring the idle deadline of a blocked read forward but can't
1452
+// remove it or push it back.
1453
+func TestIdleTimeoutReaderHandlerDeadlineDuringRead(t *testing.T) {
1454
+ earlier := time.Now().Add(time.Minute)
1455
+ for i, tc := range []struct {
1456
+ name string
1457
+ deadline time.Time
1458
+ expectHandler bool
1459
+ }{
1460
+ {name: "zero", deadline: time.Time{}},
1461
+ {name: "later", deadline: time.Now().Add(2 * time.Hour)},
1462
+ {name: "earlier", deadline: earlier, expectHandler: true},
1463
+ } {
1464
+ body := &blockingReadCloser{started: make(chan struct{}), release: make(chan struct{})}
1465
+ w := &readDeadlineRecorder{ResponseRecorder: httptest.NewRecorder()}
1466
+ r := &IdleTimeoutReader{
1467
+ ReadCloser: body,
1468
+ Ctrl: http.NewResponseController(w),
1469
+ Deadline: IdleDeadline{Timeout: time.Hour},
1470
+ Logger: zap.NewNop(),
1471
+ }
1472
+
1473
+ readDone := make(chan struct{})
1474
+ go func() {
1475
+ defer close(readDone)
1476
+ _, _ = r.Read(make([]byte, 1))
1477
+ }()
1478
+ <-body.started
1479
+
1480
+ require.NoError(t, r.SetReadDeadline(tc.deadline))
1481
+ deadlines := w.snapshot()
1482
+ require.Len(t, deadlines, 2, "Test %d (%s)", i, tc.name)
1483
+ expected := deadlines[0]
1484
+ if tc.expectHandler {
1485
+ expected = earlier
1486
+ }
1487
+ assert.Equal(t, expected, deadlines[1], "Test %d (%s)", i, tc.name)
1488
+
1489
+ close(body.release)
1490
+ <-readDone
1491
+ deadlines = w.snapshot()
1492
+ assert.True(t, deadlines[len(deadlines)-1].IsZero(), "Test %d (%s): the deadline should be cleared at EOF", i, tc.name)
1493
+ }
1494
+}
1495
+
1496
+// TestIdleTimeoutReaderHandlerDeadlineAfterTimeout checks that a
1497
+// handler can't lift the expired deadline bounding the HTTP/1 drain of
1498
+// a body that timed out.
1499
+func TestIdleTimeoutReaderHandlerDeadlineAfterTimeout(t *testing.T) {
1500
+ w := &readDeadlineRecorder{ResponseRecorder: httptest.NewRecorder()}
1501
+ r := &IdleTimeoutReader{
1502
+ ReadCloser: io.NopCloser(iotest.ErrReader(os.ErrDeadlineExceeded)),
1503
+ Ctrl: http.NewResponseController(w),
1504
+ Deadline: IdleDeadline{Timeout: -time.Second},
1505
+ Logger: zap.NewNop(),
1506
+ DrainDeadline: true,
1507
+ }
1508
+ _, err := r.Read(make([]byte, 1))
1509
+ require.ErrorIs(t, err, os.ErrDeadlineExceeded)
1510
+ expired := w.snapshot()[0]
1511
+
1512
+ require.NoError(t, r.SetReadDeadline(time.Time{}))
1513
+ r.HandlerDone()
1514
+ deadlines := w.snapshot()
1515
+ assert.Equal(t, expired, deadlines[len(deadlines)-1])
1516
+}
1517
+
1518
// TestIdleTimeoutReaderHandlerDoneClearsReleasedHardDeadline checks
1519
// that the hard deadline put back after a partial read doesn't outlive
1520
// the handler when it returns without reading the rest of the body.