Skip to content

Commit 7191757

Browse files
committed
http2: add support for net/http HTTP2 config field
For golang/go#67813 Change-Id: I6b7f857d6ed250ba8b09649730980a91b3e8d7e9 Reviewed-on: https://go-review.googlesource.com/c/net/+/607255 Reviewed-by: Brad Fitzpatrick <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Jonathan Amsterdam <[email protected]>
1 parent 4790dc7 commit 7191757

9 files changed

+416
-164
lines changed

http2/clientconn_test.go

+11-3
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ func (tc *testClientConn) readClientPreface() {
148148
}
149149
}
150150

151-
func newTestClientConn(t *testing.T, opts ...func(*Transport)) *testClientConn {
151+
func newTestClientConn(t *testing.T, opts ...any) *testClientConn {
152152
t.Helper()
153153

154154
tt := newTestTransport(t, opts...)
@@ -486,7 +486,7 @@ type testTransport struct {
486486
ccs []*testClientConn
487487
}
488488

489-
func newTestTransport(t *testing.T, opts ...func(*Transport)) *testTransport {
489+
func newTestTransport(t *testing.T, opts ...any) *testTransport {
490490
tt := &testTransport{
491491
t: t,
492492
group: newSynctest(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)),
@@ -495,7 +495,15 @@ func newTestTransport(t *testing.T, opts ...func(*Transport)) *testTransport {
495495

496496
tr := &Transport{}
497497
for _, o := range opts {
498-
o(tr)
498+
switch o := o.(type) {
499+
case func(*http.Transport):
500+
if tr.t1 == nil {
501+
tr.t1 = &http.Transport{}
502+
}
503+
o(tr.t1)
504+
case func(*Transport):
505+
o(tr)
506+
}
499507
}
500508
tt.tr = tr
501509

http2/config.go

+122
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
// Copyright 2024 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package http2
6+
7+
import (
8+
"math"
9+
"net/http"
10+
"time"
11+
)
12+
13+
// http2Config is a package-internal version of net/http.HTTP2Config.
14+
//
15+
// http.HTTP2Config was added in Go 1.24.
16+
// When running with a version of net/http that includes HTTP2Config,
17+
// we merge the configuration with the fields in Transport or Server
18+
// to produce an http2Config.
19+
//
20+
// Zero valued fields in http2Config are interpreted as in the
21+
// net/http.HTTPConfig documentation.
22+
//
23+
// Precedence order for reconciling configurations is:
24+
//
25+
// - Use the net/http.{Server,Transport}.HTTP2Config value, when non-zero.
26+
// - Otherwise use the http2.{Server.Transport} value.
27+
// - If the resulting value is zero or out of range, use a default.
28+
type http2Config struct {
29+
MaxConcurrentStreams uint32
30+
MaxDecoderHeaderTableSize uint32
31+
MaxEncoderHeaderTableSize uint32
32+
MaxReadFrameSize uint32
33+
MaxUploadBufferPerConnection int32
34+
MaxUploadBufferPerStream int32
35+
SendPingTimeout time.Duration
36+
PingTimeout time.Duration
37+
WriteByteTimeout time.Duration
38+
PermitProhibitedCipherSuites bool
39+
CountError func(errType string)
40+
}
41+
42+
// configFromServer merges configuration settings from
43+
// net/http.Server.HTTP2Config and http2.Server.
44+
func configFromServer(h1 *http.Server, h2 *Server) http2Config {
45+
conf := http2Config{
46+
MaxConcurrentStreams: h2.MaxConcurrentStreams,
47+
MaxEncoderHeaderTableSize: h2.MaxEncoderHeaderTableSize,
48+
MaxDecoderHeaderTableSize: h2.MaxDecoderHeaderTableSize,
49+
MaxReadFrameSize: h2.MaxReadFrameSize,
50+
MaxUploadBufferPerConnection: h2.MaxUploadBufferPerConnection,
51+
MaxUploadBufferPerStream: h2.MaxUploadBufferPerStream,
52+
SendPingTimeout: h2.ReadIdleTimeout,
53+
PingTimeout: h2.PingTimeout,
54+
WriteByteTimeout: h2.WriteByteTimeout,
55+
PermitProhibitedCipherSuites: h2.PermitProhibitedCipherSuites,
56+
CountError: h2.CountError,
57+
}
58+
fillNetHTTPServerConfig(&conf, h1)
59+
setConfigDefaults(&conf, true)
60+
return conf
61+
}
62+
63+
// configFromServer merges configuration settings from h2 and h2.t1.HTTP2
64+
// (the net/http Transport).
65+
func configFromTransport(h2 *Transport) http2Config {
66+
conf := http2Config{
67+
MaxEncoderHeaderTableSize: h2.MaxEncoderHeaderTableSize,
68+
MaxDecoderHeaderTableSize: h2.MaxDecoderHeaderTableSize,
69+
MaxReadFrameSize: h2.MaxReadFrameSize,
70+
SendPingTimeout: h2.ReadIdleTimeout,
71+
PingTimeout: h2.PingTimeout,
72+
WriteByteTimeout: h2.WriteByteTimeout,
73+
}
74+
75+
// Unlike most config fields, where out-of-range values revert to the default,
76+
// Transport.MaxReadFrameSize clips.
77+
if conf.MaxReadFrameSize < minMaxFrameSize {
78+
conf.MaxReadFrameSize = minMaxFrameSize
79+
} else if conf.MaxReadFrameSize > maxFrameSize {
80+
conf.MaxReadFrameSize = maxFrameSize
81+
}
82+
83+
if h2.t1 != nil {
84+
fillNetHTTPTransportConfig(&conf, h2.t1)
85+
}
86+
setConfigDefaults(&conf, false)
87+
return conf
88+
}
89+
90+
func setDefault[T ~int | ~int32 | ~uint32 | ~int64](v *T, minval, maxval, defval T) {
91+
if *v < minval || *v > maxval {
92+
*v = defval
93+
}
94+
}
95+
96+
func setConfigDefaults(conf *http2Config, server bool) {
97+
setDefault(&conf.MaxConcurrentStreams, 1, math.MaxUint32, defaultMaxStreams)
98+
setDefault(&conf.MaxEncoderHeaderTableSize, 1, math.MaxUint32, initialHeaderTableSize)
99+
setDefault(&conf.MaxDecoderHeaderTableSize, 1, math.MaxUint32, initialHeaderTableSize)
100+
if server {
101+
setDefault(&conf.MaxUploadBufferPerConnection, initialWindowSize, math.MaxInt32, 1<<20)
102+
} else {
103+
setDefault(&conf.MaxUploadBufferPerConnection, initialWindowSize, math.MaxInt32, transportDefaultConnFlow)
104+
}
105+
if server {
106+
setDefault(&conf.MaxUploadBufferPerStream, 1, math.MaxInt32, 1<<20)
107+
} else {
108+
setDefault(&conf.MaxUploadBufferPerStream, 1, math.MaxInt32, transportDefaultStreamFlow)
109+
}
110+
setDefault(&conf.MaxReadFrameSize, minMaxFrameSize, maxFrameSize, defaultMaxReadFrameSize)
111+
setDefault(&conf.PingTimeout, 1, math.MaxInt64, 15*time.Second)
112+
}
113+
114+
// adjustHTTP1MaxHeaderSize converts a limit in bytes on the size of an HTTP/1 header
115+
// to an HTTP/2 MAX_HEADER_LIST_SIZE value.
116+
func adjustHTTP1MaxHeaderSize(n int64) int64 {
117+
// http2's count is in a slightly different unit and includes 32 bytes per pair.
118+
// So, take the net/http.Server value and pad it up a bit, assuming 10 headers.
119+
const perFieldOverhead = 32 // per http2 spec
120+
const typicalHeaders = 10 // conservative
121+
return n + typicalHeaders*perFieldOverhead
122+
}

http2/config_go124.go

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright 2024 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build go1.24
6+
7+
package http2
8+
9+
import "net/http"
10+
11+
// fillNetHTTPServerConfig sets fields in conf from srv.HTTP2.
12+
func fillNetHTTPServerConfig(conf *http2Config, srv *http.Server) {
13+
fillNetHTTPConfig(conf, srv.HTTP2)
14+
}
15+
16+
// fillNetHTTPServerConfig sets fields in conf from tr.HTTP2.
17+
func fillNetHTTPTransportConfig(conf *http2Config, tr *http.Transport) {
18+
fillNetHTTPConfig(conf, tr.HTTP2)
19+
}
20+
21+
func fillNetHTTPConfig(conf *http2Config, h2 *http.HTTP2Config) {
22+
if h2 == nil {
23+
return
24+
}
25+
if h2.MaxConcurrentStreams != 0 {
26+
conf.MaxConcurrentStreams = uint32(h2.MaxConcurrentStreams)
27+
}
28+
if h2.MaxEncoderHeaderTableSize != 0 {
29+
conf.MaxEncoderHeaderTableSize = uint32(h2.MaxEncoderHeaderTableSize)
30+
}
31+
if h2.MaxDecoderHeaderTableSize != 0 {
32+
conf.MaxDecoderHeaderTableSize = uint32(h2.MaxDecoderHeaderTableSize)
33+
}
34+
if h2.MaxConcurrentStreams != 0 {
35+
conf.MaxConcurrentStreams = uint32(h2.MaxConcurrentStreams)
36+
}
37+
if h2.MaxReadFrameSize != 0 {
38+
conf.MaxReadFrameSize = uint32(h2.MaxReadFrameSize)
39+
}
40+
if h2.MaxReceiveBufferPerConnection != 0 {
41+
conf.MaxUploadBufferPerConnection = int32(h2.MaxReceiveBufferPerConnection)
42+
}
43+
if h2.MaxReceiveBufferPerStream != 0 {
44+
conf.MaxUploadBufferPerStream = int32(h2.MaxReceiveBufferPerStream)
45+
}
46+
if h2.SendPingTimeout != 0 {
47+
conf.SendPingTimeout = h2.SendPingTimeout
48+
}
49+
if h2.PingTimeout != 0 {
50+
conf.PingTimeout = h2.PingTimeout
51+
}
52+
if h2.WriteByteTimeout != 0 {
53+
conf.WriteByteTimeout = h2.WriteByteTimeout
54+
}
55+
if h2.PermitProhibitedCipherSuites {
56+
conf.PermitProhibitedCipherSuites = true
57+
}
58+
if h2.CountError != nil {
59+
conf.CountError = h2.CountError
60+
}
61+
}

http2/config_pre_go124.go

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright 2024 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build !go1.24
6+
7+
package http2
8+
9+
import "net/http"
10+
11+
// Pre-Go 1.24 fallback.
12+
// The Server.HTTP2 and Transport.HTTP2 config fields were added in Go 1.24.
13+
14+
func fillNetHTTPServerConfig(conf *http2Config, srv *http.Server) {}
15+
16+
func fillNetHTTPTransportConfig(conf *http2Config, tr *http.Transport) {}

http2/config_test.go

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// Copyright 2024 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build go1.24
6+
7+
package http2
8+
9+
import (
10+
"net/http"
11+
"testing"
12+
"time"
13+
)
14+
15+
func TestConfigServerSettings(t *testing.T) {
16+
config := &http.HTTP2Config{
17+
MaxConcurrentStreams: 1,
18+
MaxDecoderHeaderTableSize: 1<<20 + 2,
19+
MaxEncoderHeaderTableSize: 1<<20 + 3,
20+
MaxReadFrameSize: 1<<20 + 4,
21+
MaxReceiveBufferPerConnection: 64<<10 + 5,
22+
MaxReceiveBufferPerStream: 64<<10 + 6,
23+
}
24+
const maxHeaderBytes = 4096 + 7
25+
st := newServerTester(t, nil, func(s *http.Server) {
26+
s.MaxHeaderBytes = maxHeaderBytes
27+
s.HTTP2 = config
28+
})
29+
st.writePreface()
30+
st.writeSettings()
31+
st.wantSettings(map[SettingID]uint32{
32+
SettingMaxConcurrentStreams: uint32(config.MaxConcurrentStreams),
33+
SettingHeaderTableSize: uint32(config.MaxDecoderHeaderTableSize),
34+
SettingInitialWindowSize: uint32(config.MaxReceiveBufferPerStream),
35+
SettingMaxFrameSize: uint32(config.MaxReadFrameSize),
36+
SettingMaxHeaderListSize: maxHeaderBytes + (32 * 10),
37+
})
38+
}
39+
40+
func TestConfigTransportSettings(t *testing.T) {
41+
config := &http.HTTP2Config{
42+
MaxConcurrentStreams: 1, // ignored by Transport
43+
MaxDecoderHeaderTableSize: 1<<20 + 2,
44+
MaxEncoderHeaderTableSize: 1<<20 + 3,
45+
MaxReadFrameSize: 1<<20 + 4,
46+
MaxReceiveBufferPerConnection: 64<<10 + 5,
47+
MaxReceiveBufferPerStream: 64<<10 + 6,
48+
}
49+
const maxHeaderBytes = 4096 + 7
50+
tc := newTestClientConn(t, func(tr *http.Transport) {
51+
tr.HTTP2 = config
52+
tr.MaxResponseHeaderBytes = maxHeaderBytes
53+
})
54+
tc.wantSettings(map[SettingID]uint32{
55+
SettingHeaderTableSize: uint32(config.MaxDecoderHeaderTableSize),
56+
SettingInitialWindowSize: uint32(config.MaxReceiveBufferPerStream),
57+
SettingMaxFrameSize: uint32(config.MaxReadFrameSize),
58+
SettingMaxHeaderListSize: maxHeaderBytes + (32 * 10),
59+
})
60+
tc.wantWindowUpdate(0, uint32(config.MaxReceiveBufferPerConnection))
61+
}
62+
63+
func TestConfigPingTimeoutServer(t *testing.T) {
64+
st := newServerTester(t, func(w http.ResponseWriter, r *http.Request) {
65+
}, func(s *Server) {
66+
s.ReadIdleTimeout = 2 * time.Second
67+
s.PingTimeout = 3 * time.Second
68+
})
69+
st.greet()
70+
71+
st.advance(2 * time.Second)
72+
_ = readFrame[*PingFrame](t, st)
73+
st.advance(3 * time.Second)
74+
st.wantClosed()
75+
}
76+
77+
func TestConfigPingTimeoutTransport(t *testing.T) {
78+
tc := newTestClientConn(t, func(tr *Transport) {
79+
tr.ReadIdleTimeout = 2 * time.Second
80+
tr.PingTimeout = 3 * time.Second
81+
})
82+
tc.greet()
83+
84+
req, _ := http.NewRequest("GET", "https://dummy.tld/", nil)
85+
rt := tc.roundTrip(req)
86+
tc.wantFrameType(FrameHeaders)
87+
88+
tc.advance(2 * time.Second)
89+
tc.wantFrameType(FramePing)
90+
tc.advance(3 * time.Second)
91+
err := rt.err()
92+
if err == nil {
93+
t.Fatalf("expected connection to close")
94+
}
95+
}

http2/connframes_test.go

+18
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,24 @@ func (tf *testConnFramer) wantRSTStream(streamID uint32, code ErrCode) {
261261
}
262262
}
263263

264+
func (tf *testConnFramer) wantSettings(want map[SettingID]uint32) {
265+
fr := readFrame[*SettingsFrame](tf.t, tf)
266+
if fr.Header().Flags.Has(FlagSettingsAck) {
267+
tf.t.Errorf("got SETTINGS frame with ACK set, want no ACK")
268+
}
269+
for wantID, wantVal := range want {
270+
gotVal, ok := fr.Value(wantID)
271+
if !ok {
272+
tf.t.Errorf("SETTINGS: %v is not set, want %v", wantID, wantVal)
273+
} else if gotVal != wantVal {
274+
tf.t.Errorf("SETTINGS: %v is %v, want %v", wantID, gotVal, wantVal)
275+
}
276+
}
277+
if tf.t.Failed() {
278+
tf.t.Fatalf("%v", fr)
279+
}
280+
}
281+
264282
func (tf *testConnFramer) wantSettingsAck() {
265283
tf.t.Helper()
266284
fr := readFrame[*SettingsFrame](tf.t, tf)

0 commit comments

Comments
 (0)