Skip to content

Commit

Permalink
http2: add support for net/http HTTP2 config field
Browse files Browse the repository at this point in the history
For golang/go#67813

Change-Id: I6b7f857d6ed250ba8b09649730980a91b3e8d7e9
Reviewed-on: https://go-review.googlesource.com/c/net/+/607255
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
  • Loading branch information
neild committed Sep 25, 2024
1 parent 4790dc7 commit 7191757
Show file tree
Hide file tree
Showing 9 changed files with 416 additions and 164 deletions.
14 changes: 11 additions & 3 deletions http2/clientconn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ func (tc *testClientConn) readClientPreface() {
}
}

func newTestClientConn(t *testing.T, opts ...func(*Transport)) *testClientConn {
func newTestClientConn(t *testing.T, opts ...any) *testClientConn {
t.Helper()

tt := newTestTransport(t, opts...)
Expand Down Expand Up @@ -486,7 +486,7 @@ type testTransport struct {
ccs []*testClientConn
}

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

tr := &Transport{}
for _, o := range opts {
o(tr)
switch o := o.(type) {
case func(*http.Transport):
if tr.t1 == nil {
tr.t1 = &http.Transport{}
}
o(tr.t1)
case func(*Transport):
o(tr)
}
}
tt.tr = tr

Expand Down
122 changes: 122 additions & 0 deletions http2/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// Copyright 2024 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.

package http2

import (
"math"
"net/http"
"time"
)

// http2Config is a package-internal version of net/http.HTTP2Config.
//
// http.HTTP2Config was added in Go 1.24.
// When running with a version of net/http that includes HTTP2Config,
// we merge the configuration with the fields in Transport or Server
// to produce an http2Config.
//
// Zero valued fields in http2Config are interpreted as in the
// net/http.HTTPConfig documentation.
//
// Precedence order for reconciling configurations is:
//
// - Use the net/http.{Server,Transport}.HTTP2Config value, when non-zero.
// - Otherwise use the http2.{Server.Transport} value.
// - If the resulting value is zero or out of range, use a default.
type http2Config struct {
MaxConcurrentStreams uint32
MaxDecoderHeaderTableSize uint32
MaxEncoderHeaderTableSize uint32
MaxReadFrameSize uint32
MaxUploadBufferPerConnection int32
MaxUploadBufferPerStream int32
SendPingTimeout time.Duration
PingTimeout time.Duration
WriteByteTimeout time.Duration
PermitProhibitedCipherSuites bool
CountError func(errType string)
}

// configFromServer merges configuration settings from
// net/http.Server.HTTP2Config and http2.Server.
func configFromServer(h1 *http.Server, h2 *Server) http2Config {
conf := http2Config{
MaxConcurrentStreams: h2.MaxConcurrentStreams,
MaxEncoderHeaderTableSize: h2.MaxEncoderHeaderTableSize,
MaxDecoderHeaderTableSize: h2.MaxDecoderHeaderTableSize,
MaxReadFrameSize: h2.MaxReadFrameSize,
MaxUploadBufferPerConnection: h2.MaxUploadBufferPerConnection,
MaxUploadBufferPerStream: h2.MaxUploadBufferPerStream,
SendPingTimeout: h2.ReadIdleTimeout,
PingTimeout: h2.PingTimeout,
WriteByteTimeout: h2.WriteByteTimeout,
PermitProhibitedCipherSuites: h2.PermitProhibitedCipherSuites,
CountError: h2.CountError,
}
fillNetHTTPServerConfig(&conf, h1)
setConfigDefaults(&conf, true)
return conf
}

// configFromServer merges configuration settings from h2 and h2.t1.HTTP2
// (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,
}

// Unlike most config fields, where out-of-range values revert to the default,
// Transport.MaxReadFrameSize clips.
if conf.MaxReadFrameSize < minMaxFrameSize {
conf.MaxReadFrameSize = minMaxFrameSize
} else if conf.MaxReadFrameSize > maxFrameSize {
conf.MaxReadFrameSize = maxFrameSize
}

if h2.t1 != nil {
fillNetHTTPTransportConfig(&conf, h2.t1)
}
setConfigDefaults(&conf, false)
return conf
}

func setDefault[T ~int | ~int32 | ~uint32 | ~int64](v *T, minval, maxval, defval T) {
if *v < minval || *v > maxval {
*v = defval
}
}

func setConfigDefaults(conf *http2Config, server bool) {
setDefault(&conf.MaxConcurrentStreams, 1, math.MaxUint32, defaultMaxStreams)
setDefault(&conf.MaxEncoderHeaderTableSize, 1, math.MaxUint32, initialHeaderTableSize)
setDefault(&conf.MaxDecoderHeaderTableSize, 1, math.MaxUint32, initialHeaderTableSize)
if server {
setDefault(&conf.MaxUploadBufferPerConnection, initialWindowSize, math.MaxInt32, 1<<20)
} else {
setDefault(&conf.MaxUploadBufferPerConnection, initialWindowSize, math.MaxInt32, transportDefaultConnFlow)
}
if server {
setDefault(&conf.MaxUploadBufferPerStream, 1, math.MaxInt32, 1<<20)
} else {
setDefault(&conf.MaxUploadBufferPerStream, 1, math.MaxInt32, transportDefaultStreamFlow)
}
setDefault(&conf.MaxReadFrameSize, minMaxFrameSize, maxFrameSize, defaultMaxReadFrameSize)
setDefault(&conf.PingTimeout, 1, math.MaxInt64, 15*time.Second)
}

// adjustHTTP1MaxHeaderSize converts a limit in bytes on the size of an HTTP/1 header
// to an HTTP/2 MAX_HEADER_LIST_SIZE value.
func adjustHTTP1MaxHeaderSize(n int64) int64 {
// http2's count is in a slightly different unit and includes 32 bytes per pair.
// So, take the net/http.Server value and pad it up a bit, assuming 10 headers.
const perFieldOverhead = 32 // per http2 spec
const typicalHeaders = 10 // conservative
return n + typicalHeaders*perFieldOverhead
}
61 changes: 61 additions & 0 deletions http2/config_go124.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright 2024 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.24

package http2

import "net/http"

// fillNetHTTPServerConfig sets fields in conf from srv.HTTP2.
func fillNetHTTPServerConfig(conf *http2Config, srv *http.Server) {
fillNetHTTPConfig(conf, srv.HTTP2)
}

// fillNetHTTPServerConfig sets fields in conf from tr.HTTP2.
func fillNetHTTPTransportConfig(conf *http2Config, tr *http.Transport) {
fillNetHTTPConfig(conf, tr.HTTP2)
}

func fillNetHTTPConfig(conf *http2Config, h2 *http.HTTP2Config) {
if h2 == nil {
return
}
if h2.MaxConcurrentStreams != 0 {
conf.MaxConcurrentStreams = uint32(h2.MaxConcurrentStreams)
}
if h2.MaxEncoderHeaderTableSize != 0 {
conf.MaxEncoderHeaderTableSize = uint32(h2.MaxEncoderHeaderTableSize)
}
if h2.MaxDecoderHeaderTableSize != 0 {
conf.MaxDecoderHeaderTableSize = uint32(h2.MaxDecoderHeaderTableSize)
}
if h2.MaxConcurrentStreams != 0 {
conf.MaxConcurrentStreams = uint32(h2.MaxConcurrentStreams)
}
if h2.MaxReadFrameSize != 0 {
conf.MaxReadFrameSize = uint32(h2.MaxReadFrameSize)
}
if h2.MaxReceiveBufferPerConnection != 0 {
conf.MaxUploadBufferPerConnection = int32(h2.MaxReceiveBufferPerConnection)
}
if h2.MaxReceiveBufferPerStream != 0 {
conf.MaxUploadBufferPerStream = int32(h2.MaxReceiveBufferPerStream)
}
if h2.SendPingTimeout != 0 {
conf.SendPingTimeout = h2.SendPingTimeout
}
if h2.PingTimeout != 0 {
conf.PingTimeout = h2.PingTimeout
}
if h2.WriteByteTimeout != 0 {
conf.WriteByteTimeout = h2.WriteByteTimeout
}
if h2.PermitProhibitedCipherSuites {
conf.PermitProhibitedCipherSuites = true
}
if h2.CountError != nil {
conf.CountError = h2.CountError
}
}
16 changes: 16 additions & 0 deletions http2/config_pre_go124.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright 2024 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.24

package http2

import "net/http"

// Pre-Go 1.24 fallback.
// The Server.HTTP2 and Transport.HTTP2 config fields were added in Go 1.24.

func fillNetHTTPServerConfig(conf *http2Config, srv *http.Server) {}

func fillNetHTTPTransportConfig(conf *http2Config, tr *http.Transport) {}
95 changes: 95 additions & 0 deletions http2/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Copyright 2024 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.24

package http2

import (
"net/http"
"testing"
"time"
)

func TestConfigServerSettings(t *testing.T) {
config := &http.HTTP2Config{
MaxConcurrentStreams: 1,
MaxDecoderHeaderTableSize: 1<<20 + 2,
MaxEncoderHeaderTableSize: 1<<20 + 3,
MaxReadFrameSize: 1<<20 + 4,
MaxReceiveBufferPerConnection: 64<<10 + 5,
MaxReceiveBufferPerStream: 64<<10 + 6,
}
const maxHeaderBytes = 4096 + 7
st := newServerTester(t, nil, func(s *http.Server) {
s.MaxHeaderBytes = maxHeaderBytes
s.HTTP2 = config
})
st.writePreface()
st.writeSettings()
st.wantSettings(map[SettingID]uint32{
SettingMaxConcurrentStreams: uint32(config.MaxConcurrentStreams),
SettingHeaderTableSize: uint32(config.MaxDecoderHeaderTableSize),
SettingInitialWindowSize: uint32(config.MaxReceiveBufferPerStream),
SettingMaxFrameSize: uint32(config.MaxReadFrameSize),
SettingMaxHeaderListSize: maxHeaderBytes + (32 * 10),
})
}

func TestConfigTransportSettings(t *testing.T) {
config := &http.HTTP2Config{
MaxConcurrentStreams: 1, // ignored by Transport
MaxDecoderHeaderTableSize: 1<<20 + 2,
MaxEncoderHeaderTableSize: 1<<20 + 3,
MaxReadFrameSize: 1<<20 + 4,
MaxReceiveBufferPerConnection: 64<<10 + 5,
MaxReceiveBufferPerStream: 64<<10 + 6,
}
const maxHeaderBytes = 4096 + 7
tc := newTestClientConn(t, func(tr *http.Transport) {
tr.HTTP2 = config
tr.MaxResponseHeaderBytes = maxHeaderBytes
})
tc.wantSettings(map[SettingID]uint32{
SettingHeaderTableSize: uint32(config.MaxDecoderHeaderTableSize),
SettingInitialWindowSize: uint32(config.MaxReceiveBufferPerStream),
SettingMaxFrameSize: uint32(config.MaxReadFrameSize),
SettingMaxHeaderListSize: maxHeaderBytes + (32 * 10),
})
tc.wantWindowUpdate(0, uint32(config.MaxReceiveBufferPerConnection))
}

func TestConfigPingTimeoutServer(t *testing.T) {
st := newServerTester(t, func(w http.ResponseWriter, r *http.Request) {
}, func(s *Server) {
s.ReadIdleTimeout = 2 * time.Second
s.PingTimeout = 3 * time.Second
})
st.greet()

st.advance(2 * time.Second)
_ = readFrame[*PingFrame](t, st)
st.advance(3 * time.Second)
st.wantClosed()
}

func TestConfigPingTimeoutTransport(t *testing.T) {
tc := newTestClientConn(t, func(tr *Transport) {
tr.ReadIdleTimeout = 2 * time.Second
tr.PingTimeout = 3 * time.Second
})
tc.greet()

req, _ := http.NewRequest("GET", "https://dummy.tld/", nil)
rt := tc.roundTrip(req)
tc.wantFrameType(FrameHeaders)

tc.advance(2 * time.Second)
tc.wantFrameType(FramePing)
tc.advance(3 * time.Second)
err := rt.err()
if err == nil {
t.Fatalf("expected connection to close")
}
}
18 changes: 18 additions & 0 deletions http2/connframes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,24 @@ func (tf *testConnFramer) wantRSTStream(streamID uint32, code ErrCode) {
}
}

func (tf *testConnFramer) wantSettings(want map[SettingID]uint32) {
fr := readFrame[*SettingsFrame](tf.t, tf)
if fr.Header().Flags.Has(FlagSettingsAck) {
tf.t.Errorf("got SETTINGS frame with ACK set, want no ACK")
}
for wantID, wantVal := range want {
gotVal, ok := fr.Value(wantID)
if !ok {
tf.t.Errorf("SETTINGS: %v is not set, want %v", wantID, wantVal)
} else if gotVal != wantVal {
tf.t.Errorf("SETTINGS: %v is %v, want %v", wantID, gotVal, wantVal)
}
}
if tf.t.Failed() {
tf.t.Fatalf("%v", fr)
}
}

func (tf *testConnFramer) wantSettingsAck() {
tf.t.Helper()
fr := readFrame[*SettingsFrame](tf.t, tf)
Expand Down
Loading

0 comments on commit 7191757

Please sign in to comment.