src/net/http/clientserver_test.go | 21 ++++++++++++++++++++- src/net/http/http2_test.go | 18 ++++++++++++++++++ src/net/http/omithttp2_test.go | 13 +++++++++++++ src/net/http/serve_test.go | 29 +++++++++++++++++++++++++++++ src/net/http/server.go | 2 ++ diff --git a/src/net/http/clientserver_test.go b/src/net/http/clientserver_test.go index c3cf3984ef1f6a8e5dfc0d624a327048a69f12ac..9094de2b38eac718e31cb0f4362a717cc8518520 100644 --- a/src/net/http/clientserver_test.go +++ b/src/net/http/clientserver_test.go @@ -46,6 +46,11 @@ http2Mode = testMode("h2") // HTTP/2 http2UnencryptedMode = testMode("h2unencrypted") // HTTP/2 ) +type ( + testAddMode []testMode // default, plus these + testSkipMode []testMode // default, minus these +) + type testNotParallelOpt struct{} var ( @@ -70,6 +75,16 @@ modes := []testMode{http1Mode, http2Mode} parallel := true for _, opt := range opts { switch opt := opt.(type) { + case testAddMode: + for _, m := range opt { + if !slices.Contains(modes, m) { + modes = append(modes, m) + } + } + case testSkipMode: + modes = slices.DeleteFunc(modes, func(m testMode) bool { + return slices.Contains(opt, m) + }) case []testMode: modes = opt case testNotParallelOpt: @@ -99,8 +114,12 @@ // runSynctest is run combined with synctest.Run. // // The TB passed to f arranges for cleanup functions to be run in the synctest bubble. func runSynctest(t *testing.T, f func(t *testing.T, mode testMode), opts ...any) { + // HTTP/2 pool reset is not parallelizable. + opts = append(opts, testNotParallel) run(t, func(t *testing.T, mode testMode) { synctest.Test(t, func(t *testing.T) { + ResetPools() + defer ResetPools() f(t, mode) }) }, opts...) @@ -168,7 +187,7 @@ // // The optFakeNet option configures the server and client to use a fake network implementation, // suitable for use in testing/synctest tests. func newClientServerTest(t testing.TB, mode testMode, h Handler, opts ...any) *clientServerTest { - if mode == http2Mode { + if mode == http2Mode || mode == http2UnencryptedMode { CondSkipHTTP2(t) } cst := &clientServerTest{ diff --git a/src/net/http/http2_test.go b/src/net/http/http2_test.go new file mode 100644 index 0000000000000000000000000000000000000000..b94452595ddbddb0aa7f43cde250880c1fcb40b0 --- /dev/null +++ b/src/net/http/http2_test.go @@ -0,0 +1,18 @@ +// Copyright 2026 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !nethttpomithttp2 + +package http + +import "sync" + +// ResetPools reinitializes pools containing channels. +// Call this at the start and end of any test using synctest, +// to avoid leaking bubbled channels into/out of bubbles. +func ResetPools() { + http2errChanPool = sync.Pool{ + New: func() any { return make(chan error, 1) }, + } +} diff --git a/src/net/http/omithttp2_test.go b/src/net/http/omithttp2_test.go new file mode 100644 index 0000000000000000000000000000000000000000..c5c2a55d1d22c84ae248248d815c07dabdbda35d --- /dev/null +++ b/src/net/http/omithttp2_test.go @@ -0,0 +1,13 @@ +// Copyright 2026 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build nethttpomithttp2 + +package http + +// ResetPools reinitializes pools containing channels. +// Call this at the start and end of any test using synctest, +// to avoid leaking bubbled channels into/out of bubbles. +func ResetPools() { +} diff --git a/src/net/http/serve_test.go b/src/net/http/serve_test.go index 7d1e56da3cdc8c15f2aecae462d62396c59753a7..b0f8f36065b9ba3a788063ca6466e69a612c1a62 100644 --- a/src/net/http/serve_test.go +++ b/src/net/http/serve_test.go @@ -840,6 +840,35 @@ }) } } +func TestServerReadHeaderTimeoutIsCleared(t *testing.T) { + runSynctest(t, testServerReadHeaderTimeoutIsCleared, + testAddMode{http2UnencryptedMode}) +} +func testServerReadHeaderTimeoutIsCleared(t *testing.T, mode testMode) { + const timeout = time.Second + cst := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) { + w.WriteHeader(200) + NewResponseController(w).Flush() + time.Sleep(2 * timeout) + io.WriteString(w, "ok") + }), func(s *httptest.Server) { + s.Config.ReadHeaderTimeout = timeout + }, optFakeNet) + + res, err := cst.c.Get(cst.ts.URL) + if err != nil { + t.Fatal(err) + } + got, err := io.ReadAll(res.Body) + res.Body.Close() + if err != nil { + t.Fatalf("reading response body after ReadHeaderTimeout: %v", err) + } + if want := "ok"; string(got) != want { + t.Fatalf("response body = %q, want %q", got, want) + } +} + func TestServerReadTimeout(t *testing.T) { run(t, testServerReadTimeout) } func testServerReadTimeout(t *testing.T, mode testMode) { respBody := "response body" diff --git a/src/net/http/server.go b/src/net/http/server.go index 4ba86844f238b1693b29fef1fcff6372e37fea1d..975f4289736e85adcb008930cd7df819a9045c41 100644 --- a/src/net/http/server.go +++ b/src/net/http/server.go @@ -2207,6 +2207,8 @@ if !hasPreface(c, []byte("PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n")) { return false } c.setState(c.rwc, StateActive, skipHooks) + c.rwc.SetReadDeadline(time.Time{}) + c.rwc.SetWriteDeadline(time.Time{}) h := unencryptedHTTP2Request{ctx, c.rwc, serverHandler{c.server}} fn(c.server, unencryptedTLSConn(c.rwc), h) return true