Skip to content

Commit bfc8f28

Browse files
committed
net/http: add Protocols field to Server and Transport
Support configuring which HTTP version(s) a server or client use via an explicit set of protocols. The Protocols field takes precedence over TLSNextProto and ForceAttemptHTTP2. Fixes #67814 Change-Id: I09ece88f78ad4d98ca1f213157b5f62ae11e063f Reviewed-on: https://go-review.googlesource.com/c/go/+/607496 LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Jonathan Amsterdam <[email protected]> Reviewed-by: Brad Fitzpatrick <[email protected]>
1 parent 635c2dc commit bfc8f28

File tree

8 files changed

+429
-22
lines changed

8 files changed

+429
-22
lines changed

api/next/67814.txt

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
pkg net/http, method (*Protocols) SetHTTP1(bool) #67814
2+
pkg net/http, method (*Protocols) SetHTTP2(bool) #67814
3+
pkg net/http, method (Protocols) String() string #67814
4+
pkg net/http, method (Protocols) HTTP1() bool #67814
5+
pkg net/http, method (Protocols) HTTP2() bool #67814
6+
pkg net/http, type Protocols struct #67814
7+
pkg net/http, type Server struct, Protocols *Protocols #67814
8+
pkg net/http, type Transport struct, Protocols *Protocols #67814
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
The new [Server.Protocols] and [Transport.Protocols] fields provide
2+
a simple way to configure what HTTP protocols a server or client use.

src/net/http/example_test.go

+28
Original file line numberDiff line numberDiff line change
@@ -193,3 +193,31 @@ func ExampleNotFoundHandler() {
193193

194194
log.Fatal(http.ListenAndServe(":8080", mux))
195195
}
196+
197+
func ExampleProtocols_http1() {
198+
srv := http.Server{
199+
Addr: ":8443",
200+
}
201+
202+
// Serve only HTTP/1.
203+
srv.Protocols = new(http.Protocols)
204+
srv.Protocols.SetHTTP1(true)
205+
206+
log.Fatal(srv.ListenAndServeTLS("cert.pem", "key.pem"))
207+
}
208+
209+
func ExampleProtocols_http1or2() {
210+
t := http.DefaultTransport.(*http.Transport).Clone()
211+
212+
// Use either HTTP/1 and HTTP/2.
213+
t.Protocols = new(http.Protocols)
214+
t.Protocols.SetHTTP1(true)
215+
t.Protocols.SetHTTP2(true)
216+
217+
cli := &http.Client{Transport: t}
218+
res, err := cli.Get("http://www.google.com/robots.txt")
219+
if err != nil {
220+
log.Fatal(err)
221+
}
222+
res.Body.Close()
223+
}

src/net/http/http.go

+48
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,54 @@ import (
1616
"golang.org/x/net/http/httpguts"
1717
)
1818

19+
// Protocols is a set of HTTP protocols.
20+
//
21+
// The supported protocols are:
22+
//
23+
// - HTTP1 is the HTTP/1.0 and HTTP/1.1 protocols.
24+
// HTTP1 is supported on both unsecured TCP and secured TLS connections.
25+
//
26+
// - HTTP2 is the HTTP/2 protcol over a TLS connection.
27+
type Protocols struct {
28+
bits uint8
29+
}
30+
31+
const (
32+
protoHTTP1 = 1 << iota
33+
protoHTTP2
34+
)
35+
36+
// HTTP1 reports whether p includes HTTP/1.
37+
func (p Protocols) HTTP1() bool { return p.bits&protoHTTP1 != 0 }
38+
39+
// SetHTTP1 adds or removes HTTP/1 from p.
40+
func (p *Protocols) SetHTTP1(ok bool) { p.setBit(protoHTTP1, ok) }
41+
42+
// HTTP2 reports whether p includes HTTP/2.
43+
func (p Protocols) HTTP2() bool { return p.bits&protoHTTP2 != 0 }
44+
45+
// SetHTTP2 adds or removes HTTP/2 from p.
46+
func (p *Protocols) SetHTTP2(ok bool) { p.setBit(protoHTTP2, ok) }
47+
48+
func (p *Protocols) setBit(bit uint8, ok bool) {
49+
if ok {
50+
p.bits |= bit
51+
} else {
52+
p.bits &^= bit
53+
}
54+
}
55+
56+
func (p Protocols) String() string {
57+
var s []string
58+
if p.HTTP1() {
59+
s = append(s, "HTTP1")
60+
}
61+
if p.HTTP2() {
62+
s = append(s, "HTTP2")
63+
}
64+
return "{" + strings.Join(s, ",") + "}"
65+
}
66+
1967
// incomparable is a zero-width, non-comparable type. Adding it to a struct
2068
// makes that struct also non-comparable, and generally doesn't add
2169
// any size (as long as it's first).

src/net/http/http_test.go

+22
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,28 @@ func TestNoUnicodeStrings(t *testing.T) {
187187
}
188188
}
189189

190+
func TestProtocols(t *testing.T) {
191+
var p Protocols
192+
if p.HTTP1() {
193+
t.Errorf("zero-value protocols: p.HTTP1() = true, want false")
194+
}
195+
p.SetHTTP1(true)
196+
p.SetHTTP2(true)
197+
if !p.HTTP1() {
198+
t.Errorf("initialized protocols: p.HTTP1() = false, want true")
199+
}
200+
if !p.HTTP2() {
201+
t.Errorf("initialized protocols: p.HTTP2() = false, want true")
202+
}
203+
p.SetHTTP1(false)
204+
if p.HTTP1() {
205+
t.Errorf("after unsetting HTTP1: p.HTTP1() = true, want false")
206+
}
207+
if !p.HTTP2() {
208+
t.Errorf("after unsetting HTTP1: p.HTTP2() = false, want true")
209+
}
210+
}
211+
190212
const redirectURL = "/thisaredirect细雪withasciilettersのけぶabcdefghijk.html"
191213

192214
func BenchmarkHexEscapeNonASCII(b *testing.B) {

src/net/http/server.go

+71-8
Original file line numberDiff line numberDiff line change
@@ -2979,6 +2979,13 @@ type Server struct {
29792979
// See https://go.dev/issue/67813.
29802980
HTTP2 *HTTP2Config
29812981

2982+
// Protocols is the set of protocols accepted by the server.
2983+
//
2984+
// If Protocols is nil, the default is usually HTTP/1 and HTTP/2.
2985+
// If TLSNextProto is non-nil and does not contain an "h2" entry,
2986+
// the default is HTTP/1 only.
2987+
Protocols *Protocols
2988+
29822989
inShutdown atomic.Bool // true when server is in shutdown
29832990

29842991
disableKeepAlives atomic.Bool
@@ -3389,9 +3396,7 @@ func (s *Server) ServeTLS(l net.Listener, certFile, keyFile string) error {
33893396
}
33903397

33913398
config := cloneTLSConfig(s.TLSConfig)
3392-
if !slices.Contains(config.NextProtos, "http/1.1") {
3393-
config.NextProtos = append(config.NextProtos, "http/1.1")
3394-
}
3399+
config.NextProtos = adjustNextProtos(config.NextProtos, s.protocols())
33953400

33963401
configHasCert := len(config.Certificates) > 0 || config.GetCertificate != nil || config.GetConfigForClient != nil
33973402
if !configHasCert || certFile != "" || keyFile != "" {
@@ -3407,6 +3412,59 @@ func (s *Server) ServeTLS(l net.Listener, certFile, keyFile string) error {
34073412
return s.Serve(tlsListener)
34083413
}
34093414

3415+
func (s *Server) protocols() Protocols {
3416+
if s.Protocols != nil {
3417+
return *s.Protocols // user-configured set
3418+
}
3419+
3420+
// The historic way of disabling HTTP/2 is to set TLSNextProto to
3421+
// a non-nil map with no "h2" entry.
3422+
_, hasH2 := s.TLSNextProto["h2"]
3423+
http2Disabled := s.TLSNextProto != nil && !hasH2
3424+
3425+
// If GODEBUG=http2server=0, then HTTP/2 is disabled unless
3426+
// the user has manually added an "h2" entry to TLSNextProto
3427+
// (probably by using x/net/http2 directly).
3428+
if http2server.Value() == "0" && !hasH2 {
3429+
http2Disabled = true
3430+
}
3431+
3432+
var p Protocols
3433+
p.SetHTTP1(true) // default always includes HTTP/1
3434+
if !http2Disabled {
3435+
p.SetHTTP2(true)
3436+
}
3437+
return p
3438+
}
3439+
3440+
// adjustNextProtos adds or removes "http/1.1" and "h2" entries from
3441+
// a tls.Config.NextProtos list, according to the set of protocols in protos.
3442+
func adjustNextProtos(nextProtos []string, protos Protocols) []string {
3443+
var have Protocols
3444+
nextProtos = slices.DeleteFunc(nextProtos, func(s string) bool {
3445+
switch s {
3446+
case "http/1.1":
3447+
if !protos.HTTP1() {
3448+
return true
3449+
}
3450+
have.SetHTTP1(true)
3451+
case "h2":
3452+
if !protos.HTTP2() {
3453+
return true
3454+
}
3455+
have.SetHTTP2(true)
3456+
}
3457+
return false
3458+
})
3459+
if protos.HTTP2() && !have.HTTP2() {
3460+
nextProtos = append(nextProtos, "h2")
3461+
}
3462+
if protos.HTTP1() && !have.HTTP1() {
3463+
nextProtos = append(nextProtos, "http/1.1")
3464+
}
3465+
return nextProtos
3466+
}
3467+
34103468
// trackListener adds or removes a net.Listener to the set of tracked
34113469
// listeners.
34123470
//
@@ -3600,16 +3658,21 @@ func (s *Server) onceSetNextProtoDefaults() {
36003658
if omitBundledHTTP2 {
36013659
return
36023660
}
3661+
if !s.protocols().HTTP2() {
3662+
return
3663+
}
36033664
if http2server.Value() == "0" {
36043665
http2server.IncNonDefault()
36053666
return
36063667
}
3607-
// Enable HTTP/2 by default if the user hasn't otherwise
3608-
// configured their TLSNextProto map.
3609-
if s.TLSNextProto == nil {
3610-
conf := &http2Server{}
3611-
s.nextProtoErr = http2ConfigureServer(s, conf)
3668+
if _, ok := s.TLSNextProto["h2"]; ok {
3669+
// TLSNextProto already contains an HTTP/2 implementation.
3670+
// The user probably called golang.org/x/net/http2.ConfigureServer
3671+
// to add it.
3672+
return
36123673
}
3674+
conf := &http2Server{}
3675+
s.nextProtoErr = http2ConfigureServer(s, conf)
36133676
}
36143677

36153678
// TimeoutHandler returns a [Handler] that runs h with the given time limit.

src/net/http/transport.go

+48-14
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,7 @@ const DefaultMaxIdleConnsPerHost = 2
7676
// Transport uses HTTP/1.1 for HTTP URLs and either HTTP/1.1 or HTTP/2
7777
// for HTTPS URLs, depending on whether the server supports HTTP/2,
7878
// and how the Transport is configured. The [DefaultTransport] supports HTTP/2.
79-
// To explicitly enable HTTP/2 on a transport, use golang.org/x/net/http2
80-
// and call ConfigureTransport. See the package docs for more about HTTP/2.
79+
// To explicitly enable HTTP/2 on a transport, set [Transport.Protocols].
8180
//
8281
// Responses with status codes in the 1xx range are either handled
8382
// automatically (100 expect-continue) or ignored. The one
@@ -300,6 +299,13 @@ type Transport struct {
300299
// This field does not yet have any effect.
301300
// See https://go.dev/issue/67813.
302301
HTTP2 *HTTP2Config
302+
303+
// Protocols is the set of protocols supported by the transport.
304+
//
305+
// If Protocols is nil, the default is usually HTTP/1 only.
306+
// If ForceAttemptHTTP2 is true, or if TLSNextProto contains an "h2" entry,
307+
// the default is HTTP/1 and HTTP/2.
308+
Protocols *Protocols
303309
}
304310

305311
func (t *Transport) writeBufferSize() int {
@@ -349,6 +355,10 @@ func (t *Transport) Clone() *Transport {
349355
t2.HTTP2 = &HTTP2Config{}
350356
*t2.HTTP2 = *t.HTTP2
351357
}
358+
if t.Protocols != nil {
359+
t2.Protocols = &Protocols{}
360+
*t2.Protocols = *t.Protocols
361+
}
352362
if !t.tlsNextProtoWasNil {
353363
npm := maps.Clone(t.TLSNextProto)
354364
if npm == nil {
@@ -399,18 +409,8 @@ func (t *Transport) onceSetNextProtoDefaults() {
399409
}
400410
}
401411

402-
if t.TLSNextProto != nil {
403-
// This is the documented way to disable http2 on a
404-
// Transport.
405-
return
406-
}
407-
if !t.ForceAttemptHTTP2 && (t.TLSClientConfig != nil || t.Dial != nil || t.DialContext != nil || t.hasCustomTLSDialer()) {
408-
// Be conservative and don't automatically enable
409-
// http2 if they've specified a custom TLS config or
410-
// custom dialers. Let them opt-in themselves via
411-
// http2.ConfigureTransport so we don't surprise them
412-
// by modifying their tls.Config. Issue 14275.
413-
// However, if ForceAttemptHTTP2 is true, it overrides the above checks.
412+
protocols := t.protocols()
413+
if !protocols.HTTP2() {
414414
return
415415
}
416416
if omitBundledHTTP2 {
@@ -437,6 +437,40 @@ func (t *Transport) onceSetNextProtoDefaults() {
437437
t2.MaxHeaderListSize = uint32(limit1)
438438
}
439439
}
440+
441+
// Server.ServeTLS clones the tls.Config before modifying it.
442+
// Transport doesn't. We may want to make the two consistent some day.
443+
//
444+
// http2configureTransport will have already set NextProtos, but adjust it again
445+
// here to remove HTTP/1.1 if the user has disabled it.
446+
t.TLSClientConfig.NextProtos = adjustNextProtos(t.TLSClientConfig.NextProtos, protocols)
447+
}
448+
449+
func (t *Transport) protocols() Protocols {
450+
if t.Protocols != nil {
451+
return *t.Protocols // user-configured set
452+
}
453+
var p Protocols
454+
p.SetHTTP1(true) // default always includes HTTP/1
455+
switch {
456+
case t.TLSNextProto != nil:
457+
// Setting TLSNextProto to an empty map is is a documented way
458+
// to disable HTTP/2 on a Transport.
459+
if t.TLSNextProto["h2"] != nil {
460+
p.SetHTTP2(true)
461+
}
462+
case !t.ForceAttemptHTTP2 && (t.TLSClientConfig != nil || t.Dial != nil || t.DialContext != nil || t.hasCustomTLSDialer()):
463+
// Be conservative and don't automatically enable
464+
// http2 if they've specified a custom TLS config or
465+
// custom dialers. Let them opt-in themselves via
466+
// Transport.Protocols.SetHTTP2(true) so we don't surprise them
467+
// by modifying their tls.Config. Issue 14275.
468+
// However, if ForceAttemptHTTP2 is true, it overrides the above checks.
469+
case http2client.Value() == "0":
470+
default:
471+
p.SetHTTP2(true)
472+
}
473+
return p
440474
}
441475

442476
// ProxyFromEnvironment returns the URL of the proxy to use for a

0 commit comments

Comments
 (0)