@marcel91200 / caddy Mirror · read-only / commits / a28f0614
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, 02:15 UTC

caddyhttp: let handler read deadlines cap the idle read timeout (#8284)

* caddyhttp: let handler read deadlines cap the idle read timeout IdleTimeoutReader sets the read deadline before every body read, so a deadline set by a handler with http.ResponseController.SetReadDeadline() was replaced before it could take effect. Since #7913, handlers can't bound a stalled request body more tightly than read_body. A handler's read deadline is now routed to the IdleTimeoutReader, which applies it right away and keeps it as a ceiling for every idle reset and for the deadline put back between HTTP/2 reads. A handler can make the idle timeout stricter but can't loosen it, and a zero deadline removes the ceiling, as #8145 does for write deadlines. * caddyhttp: keep the hard read deadline and the drain deadline with handler deadlines SetReadDeadline installed the handler's deadline as is, so a later or zero deadline lifted read_timeout. It now installs the earlier of the two. HandlerDone cleared a deadline set by the handler before arming the HTTP/1 drain deadline, leaving the post-handler drain unbounded. * caddyhttp: keep the idle deadline of a read in flight on handler deadlines SetReadDeadline replaced the idle deadline armed for a blocked read with the handler's ceiling, so a zero or later deadline lifted the idle timeout for that read. It now installs the earlier of the two until the read returns. * caddyhttp: keep the expired drain deadline of a timed-out body on handler deadlines After a body read timed out on HTTP/1, a zero or later deadline set by the handler replaced the expired one kept to bound net/http's post-handler drain.

Kévin Dunglas committed Oct 29, 2026 at 14:22 UTC a28f061462ae394b2b488af2692c76f7fe7e9159
4 files changed +353 -28
modules/caddyhttp/idletimeout.go
+110 -28
@@ -113,6 +113,10 @@ func (d *IdleDeadline) restore(o idleOverride) {
113 // requests. An expired deadline is kept when DrainDeadline is set to bound
114 // net/http's post-handler drain.
115 // see: https://github.com/caddyserver/caddy/issues/8103
116 +//
117 +// A read deadline set by a handler through http.ResponseController
118 +// reaches SetReadDeadline and caps every idle reset, so a handler can
119 +// tighten the idle timeout but can't loosen it.
120 type IdleTimeoutReader struct {
121 io.ReadCloser
122 Ctrl *http.ResponseController
@@ -126,23 +130,27 @@ type IdleTimeoutReader struct {
130 // HTTP/2) for other protocols or requests without a body.
131 DrainDeadline bool
132
129 - // ClearBetweenReads makes the reader put back HardDeadline (no
130 - // deadline unless a hard ceiling is configured) after every
131 - // successful read, so the idle deadline only bounds reads in
132 - // flight. It is required for HTTP/2, where the read deadline is a
133 - // timer that fails the body when it fires, even with no read in
134 - // flight: otherwise a handler that stops reading for longer than the
135 - // timeout (e.g. while an upstream is slow to accept the body) loses
136 - // the body, although the client is only held back by flow control.
137 - // Other protocols only check the deadline during a read.
133 + // ClearBetweenReads makes the reader put back the earliest of
134 + // HardDeadline and the deadline set by the handler (no deadline if
135 + // there is neither) after every successful read, so the idle
136 + // deadline only bounds reads in flight. It is required for HTTP/2,
137 + // where the read deadline is a timer that fails the body when it
138 + // fires, even with no read in flight: otherwise a handler that stops
139 + // reading for longer than the timeout (e.g. while an upstream is
140 + // slow to accept the body) loses the body, although the client is
141 + // only held back by flow control. Other protocols only check the
142 + // deadline during a read.
143 ClearBetweenReads bool
144
145 mu sync.Mutex
146 unsupported bool
147 armed armedDeadline
148 installed time.Time // Last deadline successfully installed through Ctrl.
149 + reading bool
150 finished bool
151 terminalErr error
152 +
153 + handlerDeadline time.Time
154 }
155
156 // armedDeadline is which read deadline IdleTimeoutReader has armed.
@@ -154,11 +162,54 @@ const (
162 // deadlineIdle means the idle deadline from IdleDeadline.next is
163 // armed.
164 deadlineIdle
157 - // deadlineHard means only HardDeadline is armed, as put back
158 - // between reads with ClearBetweenReads.
165 + // deadlineHard means only the ceiling (HardDeadline or the
166 + // deadline set by the handler) is armed, as put back between reads
167 + // with ClearBetweenReads or set by the handler.
168 deadlineHard
169 )
170
171 +// SetReadDeadline sets the read deadline on behalf of a handler and
172 +// keeps it as a ceiling for the idle deadline, until the handler sets
173 +// another one. A zero deadline removes the ceiling. HardDeadline still
174 +// applies, and so does the idle deadline of a read in flight or the
175 +// expired one bounding the drain of a timed-out body.
176 +func (r *IdleTimeoutReader) SetReadDeadline(deadline time.Time) error {
177 + r.mu.Lock()
178 + defer r.mu.Unlock()
179 +
180 + prev := r.handlerDeadline
181 + r.handlerDeadline = deadline
182 + install := r.ceilingLocked()
183 + pinned := (r.reading && r.armed == deadlineIdle) || r.keepExpiredLocked()
184 + if pinned && (install.IsZero() || r.installed.Before(install)) {
185 + install = r.installed
186 + }
187 + if err := r.Ctrl.SetReadDeadline(install); err != nil {
188 + r.handlerDeadline = prev
189 + return err
190 + }
191 + r.installed = install
192 + if pinned {
193 + return nil
194 + }
195 + if install.IsZero() {
196 + r.armed = deadlineNone
197 + } else {
198 + r.armed = deadlineHard
199 + }
200 + return nil
201 +}
202 +
203 +// ceilingLocked returns the earliest of HardDeadline and the deadline
204 +// set by the handler, or zero if there is neither.
205 +func (r *IdleTimeoutReader) ceilingLocked() time.Time {
206 + hard := r.Deadline.HardDeadline
207 + if r.handlerDeadline.IsZero() || (!hard.IsZero() && hard.Before(r.handlerDeadline)) {
208 + return hard
209 + }
210 + return r.handlerDeadline
211 +}
212 +
213 func (r *IdleTimeoutReader) Read(p []byte) (int, error) {
214 r.mu.Lock()
215 if r.terminalErr != nil {
@@ -169,11 +220,13 @@ func (r *IdleTimeoutReader) Read(p []byte) (int, error) {
220 if !r.finished && !r.unsupported {
221 r.setDeadlineLocked("could not set read deadline")
222 }
223 + r.reading = true
224 r.mu.Unlock()
225
226 n, err := r.ReadCloser.Read(p)
227
228 r.mu.Lock()
229 + r.reading = false
230 r.Deadline.transferred += int64(n)
231 if err != nil {
232 r.terminalErr = err
@@ -191,9 +244,9 @@ func (r *IdleTimeoutReader) Read(p []byte) (int, error) {
244 // HandlerDone prevents later body reads from using the response controller.
245 // If the body is not finished and DrainDeadline is set, it leaves an idle
246 // deadline armed for net/http's post-handler drain. Otherwise, it clears a
194 -// deadline left from a finished body or put back between reads, unless the
195 -// body timed out with an expired drain deadline. It must be called before the
196 -// installing handler returns.
247 +// deadline left from a finished body, put back between reads or set by the
248 +// handler, unless the body timed out with an expired drain deadline. It must
249 +// be called before the installing handler returns.
250 func (r *IdleTimeoutReader) HandlerDone() {
251 r.mu.Lock()
252 defer r.mu.Unlock()
@@ -201,10 +254,13 @@ func (r *IdleTimeoutReader) HandlerDone() {
254 if r.finished {
255 return
256 }
204 - if r.terminalErr != nil || r.armed == deadlineHard {
257 + switch {
258 + case r.terminalErr != nil:
259 r.clearDeadlineLocked()
206 - } else if r.DrainDeadline && r.armed != deadlineIdle && !r.unsupported {
260 + case r.DrainDeadline && r.armed != deadlineIdle && !r.unsupported:
261 r.setDeadlineLocked("could not set final read deadline")
262 + case r.armed == deadlineHard:
263 + r.clearDeadlineLocked()
264 }
265 r.finished = true
266 }
@@ -239,7 +295,12 @@ func (r *IdleTimeoutReader) rearmLocked() {
295 }
296
297 func (r *IdleTimeoutReader) setDeadlineLocked(logMessage string) {
242 - if r.setReadDeadlineLocked(r.Deadline.next(), logMessage) {
298 + deadline := r.Deadline.next()
299 + if !r.handlerDeadline.IsZero() && deadline.After(r.handlerDeadline) {
300 + deadline = r.handlerDeadline
301 + }
302 +
303 + if r.setReadDeadlineLocked(deadline, logMessage) {
304 r.armed = deadlineIdle
305 }
306 }
@@ -259,22 +320,24 @@ func (r *IdleTimeoutReader) setReadDeadlineLocked(deadline time.Time, logMessage
320 return true
321 }
322
262 -// releaseDeadlineLocked replaces the idle deadline with HardDeadline.
323 +// releaseDeadlineLocked replaces the idle deadline with the earliest of
324 +// HardDeadline and the deadline set by the handler.
325 func (r *IdleTimeoutReader) releaseDeadlineLocked() {
326 if r.armed != deadlineIdle || r.unsupported {
327 return
328 }
329
268 - // once the hard deadline has passed, the deadline set before the
269 - // read (capped to it) has already expired; keep it
270 - if !r.Deadline.HardDeadline.IsZero() && !time.Now().Before(r.Deadline.HardDeadline) {
330 + // once the ceiling has passed, the deadline set before the read
331 + // (capped to it) has already expired; keep it
332 + ceiling := r.ceilingLocked()
333 + if !ceiling.IsZero() && !time.Now().Before(ceiling) {
334 return
335 }
336
274 - if !r.setReadDeadlineLocked(r.Deadline.HardDeadline, "could not release read deadline") {
337 + if !r.setReadDeadlineLocked(ceiling, "could not release read deadline") {
338 return
339 }
277 - if r.Deadline.HardDeadline.IsZero() {
340 + if ceiling.IsZero() {
341 r.armed = deadlineNone
342 } else {
343 r.armed = deadlineHard
@@ -285,11 +348,7 @@ func (r *IdleTimeoutReader) clearDeadlineLocked() {
348 if r.armed == deadlineNone {
349 return
350 }
288 - if r.DrainDeadline && !r.installed.IsZero() && !time.Now().Before(r.installed) &&
289 - errors.Is(r.terminalErr, os.ErrDeadlineExceeded) {
290 - // HTTP/1 drains a small unread body before writing the response.
291 - // Clearing its expired deadline would let a stalled client block
292 - // that drain, and therefore the error response, indefinitely.
351 + if r.keepExpiredLocked() {
352 return
353 }
354 if !r.setReadDeadlineLocked(time.Time{}, "could not clear read deadline") {
@@ -298,6 +357,16 @@ func (r *IdleTimeoutReader) clearDeadlineLocked() {
357 r.armed = deadlineNone
358 }
359
360 +// keepExpiredLocked reports whether the installed deadline expired with
361 +// the body and must be kept. HTTP/1 drains a small unread body before
362 +// writing the response. Clearing its expired deadline would let a
363 +// stalled client block that drain, and therefore the error response,
364 +// indefinitely.
365 +func (r *IdleTimeoutReader) keepExpiredLocked() bool {
366 + return r.DrainDeadline && !r.installed.IsZero() && !time.Now().Before(r.installed) &&
367 + errors.Is(r.terminalErr, os.ErrDeadlineExceeded)
368 +}
369 +
370 // IdleTimeoutWriter wraps a ResponseWriter with IdleDeadline, resetting
371 // the write deadline before every Write, ReadFrom and Flush call, the same
372 // way IdleTimeoutReader does for reads. With MinRate == 0, the deadline
@@ -534,6 +603,18 @@ func (w *IdleTimeoutWriter) FlushError() error {
603 return nil
604 }
605
606 +// readDeadlineWriter routes read deadlines set by handlers through
607 +// http.ResponseController to the IdleTimeoutReader of the request,
608 +// which would otherwise replace them before the next read.
609 +type readDeadlineWriter struct {
610 + ResponseWriterWrapper
611 + reader *IdleTimeoutReader
612 +}
613 +
614 +func (w *readDeadlineWriter) SetReadDeadline(deadline time.Time) error {
615 + return w.reader.SetReadDeadline(deadline)
616 +}
617 +
618 // idleTimeouts holds the wrappers applying a request's idle timeouts.
619 type idleTimeouts struct {
620 reader *IdleTimeoutReader
@@ -563,6 +644,7 @@ func ContextWithIdleTimeouts(ctx context.Context, reader *IdleTimeoutReader, wri
644 // Interface guards
645 var (
646 _ io.ReadCloser = (*IdleTimeoutReader)(nil)
647 + _ http.ResponseWriter = (*readDeadlineWriter)(nil)
648 _ http.ResponseWriter = (*IdleTimeoutWriter)(nil)
649 _ io.ReaderFrom = (*IdleTimeoutWriter)(nil)
650 _ interface{ FlushError() error } = (*IdleTimeoutWriter)(nil)
modules/caddyhttp/idletimeout_test.go
+203
@@ -368,6 +368,25 @@ func TestIdleTimeoutReaderHandlerDoneArmsDrain(t *testing.T) {
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{
@@ -1312,6 +1331,190 @@ func TestIdleTimeoutReaderClearBetweenReads(t *testing.T) {
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.
modules/caddyhttp/server.go
+1
@@ -657,6 +657,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
657 }
658 defer idleReader.HandlerDone()
659 r.Body = idleReader
660 + w = &readDeadlineWriter{ResponseWriterWrapper: ResponseWriterWrapper{ResponseWriter: w}, reader: idleReader}
661 }
662 if s.WriteIdleTimeout > 0 {
663 idleWriter = &IdleTimeoutWriter{
modules/caddyhttp/server_test.go
+39
@@ -12,6 +12,7 @@ import (
12 "net/http"
13 "net/http/httptest"
14 "net/netip"
15 + "os"
16 "strings"
17 "sync"
18 "testing"
@@ -1574,6 +1575,44 @@ func TestServer_ServeHTTP_IdleTimeouts(t *testing.T) {
1575 }
1576 }
1577
1578 +// TestServer_ServeHTTP_HandlerReadDeadline checks that a read deadline
1579 +// set by a handler through http.ResponseController still applies to a
1580 +// stalled request body, instead of being replaced by the idle reader.
1581 +func TestServer_ServeHTTP_HandlerReadDeadline(t *testing.T) {
1582 + readErr := make(chan error, 1)
1583 + s := &Server{
1584 + ReadIdleTimeout: caddy.Duration(time.Minute),
1585 + logger: zap.NewNop(),
1586 + errorLogger: zap.NewNop(),
1587 + primaryHandlerChain: HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
1588 + if err := http.NewResponseController(w).SetReadDeadline(time.Now().Add(100 * time.Millisecond)); err != nil {
1589 + readErr <- err
1590 + return nil
1591 + }
1592 + _, err := io.ReadAll(r.Body)
1593 + readErr <- err
1594 + return nil
1595 + }),
1596 + }
1597 + ts := httptest.NewServer(http.HandlerFunc(s.ServeHTTP))
1598 + t.Cleanup(ts.Close)
1599 +
1600 + conn, err := net.Dial("tcp", ts.Listener.Addr().String())
1601 + require.NoError(t, err)
1602 + t.Cleanup(func() { conn.Close() })
1603 +
1604 + // announce a body, send one byte of it and stall
1605 + _, err = io.WriteString(conn, "POST / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 10\r\n\r\nx")
1606 + require.NoError(t, err)
1607 +
1608 + select {
1609 + case err := <-readErr:
1610 + require.ErrorIs(t, err, os.ErrDeadlineExceeded)
1611 + case <-time.After(5 * time.Second):
1612 + t.Fatal("the handler's read deadline was replaced by the idle timeout")
1613 + }
1614 +}
1615 +
1616 // TestServer_BuildHTTP3ServerEnablesWebTransport asserts that with
1617 // WebTransport set the http3.Server advertises WebTransport in
1618 // its SETTINGS and wires the prerequisites webtransport.Server.Upgrade