From fe9bcbcc9214b6b58014da3313b101e53b73e5b2 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Fri, 26 Sep 2025 14:53:15 -0700 Subject: [PATCH] http2: support HTTP2Config.StrictMaxConcurrentRequests When HTTP2Config.StrictMaxConcurrentRequests is set (added in Go 1.26), use it to override the value of Transport.StrictMaxConcurrentStreams. Permits configuring this parameter from net/http without importing x/net/http2. For golang/go#67813 Change-Id: Ie7fa5a8ac033b1827cf7fef4e23b5110a05dc95f Reviewed-on: https://go-review.googlesource.com/c/net/+/707315 LUCI-TryBot-Result: Go LUCI Reviewed-by: Nicholas Husin Reviewed-by: Nicholas Husin --- http2/config.go | 17 +++++++++++------ http2/config_go125.go | 15 +++++++++++++++ http2/config_go126.go | 15 +++++++++++++++ http2/transport.go | 6 ++++-- http2/transport_test.go | 24 +++++++++++++++++++----- 5 files changed, 64 insertions(+), 13 deletions(-) create mode 100644 http2/config_go125.go create mode 100644 http2/config_go126.go diff --git a/http2/config.go b/http2/config.go index 02fe0c2d..8a7a89d0 100644 --- a/http2/config.go +++ b/http2/config.go @@ -27,6 +27,7 @@ import ( // - If the resulting value is zero or out of range, use a default. type http2Config struct { MaxConcurrentStreams uint32 + StrictMaxConcurrentRequests bool MaxDecoderHeaderTableSize uint32 MaxEncoderHeaderTableSize uint32 MaxReadFrameSize uint32 @@ -64,12 +65,13 @@ func configFromServer(h1 *http.Server, h2 *Server) http2Config { // (the net/http Transport). func configFromTransport(h2 *Transport) http2Config { conf := http2Config{ - MaxEncoderHeaderTableSize: h2.MaxEncoderHeaderTableSize, - MaxDecoderHeaderTableSize: h2.MaxDecoderHeaderTableSize, - MaxReadFrameSize: h2.MaxReadFrameSize, - SendPingTimeout: h2.ReadIdleTimeout, - PingTimeout: h2.PingTimeout, - WriteByteTimeout: h2.WriteByteTimeout, + StrictMaxConcurrentRequests: h2.StrictMaxConcurrentStreams, + MaxEncoderHeaderTableSize: h2.MaxEncoderHeaderTableSize, + MaxDecoderHeaderTableSize: h2.MaxDecoderHeaderTableSize, + MaxReadFrameSize: h2.MaxReadFrameSize, + SendPingTimeout: h2.ReadIdleTimeout, + PingTimeout: h2.PingTimeout, + WriteByteTimeout: h2.WriteByteTimeout, } // Unlike most config fields, where out-of-range values revert to the default, @@ -128,6 +130,9 @@ func fillNetHTTPConfig(conf *http2Config, h2 *http.HTTP2Config) { if h2.MaxConcurrentStreams != 0 { conf.MaxConcurrentStreams = uint32(h2.MaxConcurrentStreams) } + if http2ConfigStrictMaxConcurrentRequests(h2) { + conf.StrictMaxConcurrentRequests = true + } if h2.MaxEncoderHeaderTableSize != 0 { conf.MaxEncoderHeaderTableSize = uint32(h2.MaxEncoderHeaderTableSize) } diff --git a/http2/config_go125.go b/http2/config_go125.go new file mode 100644 index 00000000..b4373fe3 --- /dev/null +++ b/http2/config_go125.go @@ -0,0 +1,15 @@ +// Copyright 2025 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 !go1.26 + +package http2 + +import ( + "net/http" +) + +func http2ConfigStrictMaxConcurrentRequests(h2 *http.HTTP2Config) bool { + return false +} diff --git a/http2/config_go126.go b/http2/config_go126.go new file mode 100644 index 00000000..6b071c14 --- /dev/null +++ b/http2/config_go126.go @@ -0,0 +1,15 @@ +// Copyright 2025 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 go1.26 + +package http2 + +import ( + "net/http" +) + +func http2ConfigStrictMaxConcurrentRequests(h2 *http.HTTP2Config) bool { + return h2.StrictMaxConcurrentRequests +} diff --git a/http2/transport.go b/http2/transport.go index 35e39025..be759b60 100644 --- a/http2/transport.go +++ b/http2/transport.go @@ -355,6 +355,7 @@ type ClientConn struct { readIdleTimeout time.Duration pingTimeout time.Duration extendedConnectAllowed bool + strictMaxConcurrentStreams bool // rstStreamPingsBlocked works around an unfortunate gRPC behavior. // gRPC strictly limits the number of PING frames that it will receive. @@ -784,7 +785,8 @@ func (t *Transport) newClientConn(c net.Conn, singleUse bool) (*ClientConn, erro initialWindowSize: 65535, // spec default initialStreamRecvWindowSize: conf.MaxUploadBufferPerStream, maxConcurrentStreams: initialMaxConcurrentStreams, // "infinite", per spec. Use a smaller value until we have received server settings. - peerMaxHeaderListSize: 0xffffffffffffffff, // "infinite", per spec. Use 2^64-1 instead. + strictMaxConcurrentStreams: conf.StrictMaxConcurrentRequests, + peerMaxHeaderListSize: 0xffffffffffffffff, // "infinite", per spec. Use 2^64-1 instead. streams: make(map[uint32]*clientStream), singleUse: singleUse, seenSettingsChan: make(chan struct{}), @@ -1018,7 +1020,7 @@ func (cc *ClientConn) idleStateLocked() (st clientConnIdleState) { return } var maxConcurrentOkay bool - if cc.t.StrictMaxConcurrentStreams { + if cc.strictMaxConcurrentStreams { // We'll tell the caller we can take a new request to // prevent the caller from dialing a new TCP // connection, but then we'll block later before diff --git a/http2/transport_test.go b/http2/transport_test.go index e918a4ed..8ccae257 100644 --- a/http2/transport_test.go +++ b/http2/transport_test.go @@ -3475,14 +3475,28 @@ func TestTransportRequestsLowServerLimit(t *testing.T) { // tests Transport.StrictMaxConcurrentStreams func TestTransportRequestsStallAtServerLimit(t *testing.T) { - synctestTest(t, testTransportRequestsStallAtServerLimit) + synctestSubtest(t, "Transport", func(t testing.TB) { + testTransportRequestsStallAtServerLimit(t, func(tr *Transport) { + tr.StrictMaxConcurrentStreams = true + }) + }) + synctestSubtest(t, "HTTP2Config", func(t testing.TB) { + // HTTP2Config.StrictMaxConcurrentRequests was added in Go 1.26. + h2 := &http.HTTP2Config{} + v := reflect.ValueOf(h2).Elem().FieldByName("StrictMaxConcurrentRequests") + if !v.IsValid() { + t.Skip("HTTP2Config does not contain StrictMaxConcurrentRequests") + } + v.SetBool(true) + testTransportRequestsStallAtServerLimit(t, func(tr *http.Transport) { + tr.HTTP2 = h2 + }) + }) } -func testTransportRequestsStallAtServerLimit(t testing.TB) { +func testTransportRequestsStallAtServerLimit(t testing.TB, opt any) { const maxConcurrent = 2 - tc := newTestClientConn(t, func(tr *Transport) { - tr.StrictMaxConcurrentStreams = true - }) + tc := newTestClientConn(t, opt) tc.greet(Setting{SettingMaxConcurrentStreams, maxConcurrent}) cancelClientRequest := make(chan struct{})