| // 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. | |
| // This file tests the exported, user-facing parts of Transport. | |
| // | |
| // These tests verify that Transport behavior is consistent when using either the | |
| // HTTP/2 implementation in this package (x/net/http2), or when using the | |
| // implementation in net/http/internal/http2. | |
| package http2_test | |
| import ( | |
| "context" | |
| "crypto/tls" | |
| "errors" | |
| "fmt" | |
| "io" | |
| "net" | |
| "net/http" | |
| "slices" | |
| "strings" | |
| "testing" | |
| "testing/synctest" | |
| "time" | |
| "golang.org/x/net/http2" | |
| ) | |
| func synctestTestRoundTrip(t *testing.T, f func(t *testing.T, mode roundTripTestMode)) { | |
| t.Run("netHTTP", func(t *testing.T) { | |
| synctest.Test(t, func(t *testing.T) { | |
| f(t, roundTripNetHTTP) | |
| }) | |
| }) | |
| t.Run("xNetHTTP2", func(t *testing.T) { | |
| synctest.Test(t, func(t *testing.T) { | |
| f(t, roundTripXNetHTTP2) | |
| }) | |
| }) | |
| } | |
| // TestAPITransportDial tests the Transport.Dial, Transport.DialTLS, | |
| // and Transport.TLSClientConfig fields. | |
| func TestAPITransportDial(t *testing.T) { | |
| t.Run("DialTLS/http", func(t *testing.T) { | |
| testAPITransportDial(t, "DialTLS", "http") | |
| }) | |
| t.Run("DialTLS/https", func(t *testing.T) { | |
| testAPITransportDial(t, "DialTLS", "https") | |
| }) | |
| t.Run("DialTLSContext/http", func(t *testing.T) { | |
| testAPITransportDial(t, "DialTLSContext", "http") | |
| }) | |
| t.Run("DialTLSContext/https", func(t *testing.T) { | |
| testAPITransportDial(t, "DialTLSContext", "https") | |
| }) | |
| } | |
| func testAPITransportDial(t *testing.T, name, proto string) { | |
| synctestTestRoundTrip(t, func(t *testing.T, mode roundTripTestMode) { | |
| const serverName = "server.tld" | |
| var ( | |
| dialCalled = false | |
| dialServerName = "" | |
| verifyConnectionCalled = false | |
| tt *testTransport | |
| ) | |
| dialFunc := func(network, address string, tlsConf *tls.Config) (net.Conn, error) { | |
| dialCalled = true | |
| dialServerName = tlsConf.ServerName | |
| if got, want := tlsConf.NextProtos, []string{"h2"}; !slices.Equal(got, want) { | |
| t.Errorf("tls.Config.NextProtos = %q, want %q", got, want) | |
| } | |
| conn := tt.li.newConn() | |
| switch proto { | |
| case "http": | |
| return conn, nil | |
| case "https": | |
| tlsConn := tls.Client(conn, tlsConf) | |
| if err := tlsConn.Handshake(); err != nil { | |
| t.Errorf("client TLS handshake: %v", err) | |
| } | |
| return tlsConn, nil | |
| default: | |
| panic("unknown proto: " + proto) | |
| } | |
| } | |
| tt = newTestTransport(t, mode, func(tr2 *http2.Transport) { | |
| tr2.TLSClientConfig = &tls.Config{ | |
| InsecureSkipVerify: true, | |
| VerifyConnection: func(tls.ConnectionState) error { | |
| // We check to see if the Transport's TLSClientConfig | |
| // was used by observing if VerifyConnection was called. | |
| verifyConnectionCalled = true | |
| return nil | |
| }, | |
| } | |
| switch name { | |
| case "DialTLS": | |
| tr2.DialTLS = dialFunc | |
| tr2.DialTLSContext = nil | |
| case "DialTLSContext": | |
| tr2.DialTLS = nil | |
| tr2.DialTLSContext = func(ctx context.Context, network, address string, tlsConf *tls.Config) (net.Conn, error) { | |
| return dialFunc(network, address, tlsConf) | |
| } | |
| default: | |
| panic("unknown func: " + name) | |
| } | |
| if proto == "http" { | |
| tr2.AllowHTTP = true | |
| } | |
| }) | |
| if proto == "https" { | |
| tt.useTLS = true | |
| } | |
| req, _ := http.NewRequest("GET", proto+"://"+serverName+"/", nil) | |
| _ = tt.roundTrip(req) | |
| tc := tt.getConn() | |
| tc.wantFrameType(http2.FrameSettings) | |
| // When we use http.Transport.RoundTrip, it handles the dial and ignores | |
| // the http2.Transport's dialer. | |
| wantDialCalled := mode == roundTripXNetHTTP2 | |
| if dialCalled != wantDialCalled { | |
| t.Errorf("Transport.%v called: %v, want %v", name, dialCalled, wantDialCalled) | |
| } | |
| // If the VerifyConnection hook is called, this indicates that we | |
| // correctly used the http2.Transport.TLSClientConfig. | |
| if got, want := verifyConnectionCalled, (proto == "https" && wantDialCalled); got != want { | |
| t.Errorf("TLSConfig.VerifyConnection called: %v, want %v", got, want) | |
| } | |
| // If the dial function is called, it should be provided with a *tls.Config | |
| // with the ServerName filled in correctly. | |
| if dialCalled && dialServerName != serverName { | |
| t.Errorf("TLSConfig.ServerName = %q, want %q", dialServerName, serverName) | |
| } | |
| }) | |
| } | |
| // TestAPITransportDisableCompression tests the Transport.DisableCompression field. | |
| func TestAPITransportDisableCompression(t *testing.T) { | |
| for _, disable := range []bool{true, false} { | |
| t.Run(fmt.Sprint(disable), func(t *testing.T) { | |
| synctestTestRoundTrip(t, func(t *testing.T, mode roundTripTestMode) { | |
| tc := newTestClientConn(t, mode, func(tr2 *http2.Transport) { | |
| tr2.DisableCompression = disable | |
| }) | |
| tc.greet() | |
| req, _ := http.NewRequest("PUT", "https://dummy.tld/", nil) | |
| _ = tc.roundTrip(req) | |
| var want []string | |
| if !disable { | |
| want = []string{"gzip"} | |
| } | |
| tc.wantHeaders(wantHeader{ | |
| streamID: 1, | |
| endStream: true, | |
| header: http.Header{ | |
| "accept-encoding": want, | |
| }, | |
| }) | |
| }) | |
| }) | |
| } | |
| } | |
| // TestAPITransportAllowHTTPOff tests the Transport.AllowHTTP field. | |
| // (It only tests AllowHTTP = false, since most other tests use AllowHTTP = true.) | |
| func TestAPITransportAllowHTTPOff(t *testing.T) { | |
| synctestTestRoundTrip(t, func(t *testing.T, mode roundTripTestMode) { | |
| tt := newTestTransport(t, mode, func(tr2 *http2.Transport) { | |
| tr2.AllowHTTP = false | |
| }) | |
| req, _ := http.NewRequest("GET", "http://dummy.tld/", nil) | |
| rt := tt.roundTrip(req) | |
| switch mode { | |
| case roundTripNetHTTP: | |
| // net/http.Transport doesn't respect http2.Transport.AllowHTTP. | |
| // When using a net/http Transport, unencrypted HTTP/2 is allowed when | |
| // the transport Protocols contains UnencryptedHTTP2. | |
| tc := tt.getConn() | |
| tc.wantFrameType(http2.FrameSettings) | |
| tc.wantFrameType(http2.FrameWindowUpdate) | |
| tc.wantFrameType(http2.FrameHeaders) | |
| case roundTripXNetHTTP2: | |
| // x/net/http.Transport only permits http URLs when AllowHTTP is false. | |
| err := rt.err() | |
| want := "unencrypted HTTP/2 not enabled" | |
| if err == nil || !strings.Contains(err.Error(), want) { | |
| t.Errorf("RoundTrip = %v; want %q", err, want) | |
| } | |
| } | |
| }) | |
| } | |
| // TestAPITransportMaxHeaderListSize tests the Transport.MaxHeaderListSize field. | |
| func TestAPITransportMaxHeaderListSize(t *testing.T) { | |
| synctestTestRoundTrip(t, func(t *testing.T, mode roundTripTestMode) { | |
| const size = 20000 | |
| tc := newTestClientConn(t, mode, func(tr2 *http2.Transport) { | |
| tr2.MaxHeaderListSize = size | |
| }) | |
| tc.wantSettings(map[http2.SettingID]uint32{ | |
| http2.SettingMaxHeaderListSize: size, | |
| }) | |
| }) | |
| } | |
| // TestAPITransportMaxDecoderHeaderTableSize tests the Transport.MaxDecoderHeaderTableSize field. | |
| func TestAPITransportMaxDecoderHeaderTableSize(t *testing.T) { | |
| synctestTestRoundTrip(t, func(t *testing.T, mode roundTripTestMode) { | |
| const size = 10000 | |
| tc := newTestClientConn(t, mode, func(tr2 *http2.Transport) { | |
| tr2.MaxDecoderHeaderTableSize = size | |
| }) | |
| tc.wantSettings(map[http2.SettingID]uint32{ | |
| http2.SettingHeaderTableSize: size, | |
| }) | |
| }) | |
| } | |
| // TestAPITransportMaxEncoderHeaderTableSize should go here, | |
| // but it's difficult to verify the effects of the encoder table. | |
| // TestAPITransportMaxReadFrameSize tests the Transport.MaxReadFrameSize field. | |
| func TestAPITransportMaxReadFrameSize(t *testing.T) { | |
| synctestTestRoundTrip(t, func(t *testing.T, mode roundTripTestMode) { | |
| const size = 20000 | |
| tc := newTestClientConn(t, mode, func(tr2 *http2.Transport) { | |
| tr2.MaxReadFrameSize = size | |
| }) | |
| tc.wantSettings(map[http2.SettingID]uint32{ | |
| http2.SettingMaxFrameSize: size, | |
| }) | |
| }) | |
| } | |
| // TestAPITransportStrictMaxConcurrentStreamsEnabled tests the | |
| // Transport.StrictMaxConcurrentStreams field. | |
| func TestAPITransportStrictMaxConcurrentStreamsEnabled(t *testing.T) { | |
| synctestTestRoundTrip(t, func(t *testing.T, mode roundTripTestMode) { | |
| tt := newTestTransport(t, mode, func(tr2 *http2.Transport) { | |
| tr2.StrictMaxConcurrentStreams = true | |
| }) | |
| // Request 1: Sent on a new connection. | |
| // We observe MaxConcurrentStreams = 1. | |
| req1, _ := http.NewRequest("GET", "http://dummy.tld/1", nil) | |
| rt1 := tt.roundTrip(req1) | |
| tc1 := tt.getConn() | |
| tc1.wantFrameType(http2.FrameSettings) | |
| tc1.wantFrameType(http2.FrameWindowUpdate) | |
| tc1.wantHeaders(wantHeader{ | |
| streamID: 1, | |
| endStream: true, | |
| header: http.Header{ | |
| ":authority": []string{"dummy.tld"}, | |
| ":method": []string{"GET"}, | |
| ":path": []string{"/1"}, | |
| }, | |
| }) | |
| tc1.writeSettings(http2.Setting{ | |
| ID: http2.SettingMaxConcurrentStreams, | |
| Val: 1, | |
| }) | |
| tc1.wantFrameType(http2.FrameSettings) | |
| // Request 2: Blocks, because request 1 is consuming the stream | |
| // concurrency slot. | |
| req2, _ := http.NewRequest("GET", "http://dummy.tld/2", nil) | |
| _ = tt.roundTrip(req2) | |
| tc1.wantIdle() | |
| // Send a response to request 1. | |
| // Request 2 can now be sent. | |
| tc1.writeHeaders(http2.HeadersFrameParam{ | |
| StreamID: 1, | |
| EndHeaders: true, | |
| EndStream: true, | |
| BlockFragment: tc1.makeHeaderBlockFragment( | |
| ":status", "200", | |
| ), | |
| }) | |
| rt1.wantStatus(200) | |
| tc1.wantHeaders(wantHeader{ | |
| streamID: 3, | |
| endStream: true, | |
| header: http.Header{ | |
| ":authority": []string{"dummy.tld"}, | |
| ":method": []string{"GET"}, | |
| ":path": []string{"/2"}, | |
| }, | |
| }) | |
| }) | |
| } | |
| // TestAPITransportStrictMaxConcurrentStreamsDisabled tests the | |
| // Transport.StrictMaxConcurrentStreams field. | |
| func TestAPITransportStrictMaxConcurrentStreamsDisabled(t *testing.T) { | |
| synctestTestRoundTrip(t, func(t *testing.T, mode roundTripTestMode) { | |
| tt := newTestTransport(t, mode, func(tr2 *http2.Transport) { | |
| tr2.StrictMaxConcurrentStreams = false | |
| }) | |
| // Request 1: Sent on a new connection. | |
| // We observe MaxConcurrentStreams = 1. | |
| req1, _ := http.NewRequest("GET", "http://dummy.tld/1", nil) | |
| _ = tt.roundTrip(req1) | |
| tc1 := tt.getConn() | |
| tc1.wantFrameType(http2.FrameSettings) | |
| tc1.wantFrameType(http2.FrameWindowUpdate) | |
| tc1.wantHeaders(wantHeader{ | |
| streamID: 1, | |
| endStream: true, | |
| header: http.Header{ | |
| ":authority": []string{"dummy.tld"}, | |
| ":method": []string{"GET"}, | |
| ":path": []string{"/1"}, | |
| }, | |
| }) | |
| tc1.writeSettings(http2.Setting{ | |
| ID: http2.SettingMaxConcurrentStreams, | |
| Val: 1, | |
| }) | |
| tc1.wantFrameType(http2.FrameSettings) | |
| // Request 2: Sent on a new connection, because request 1 is consuming the | |
| // first connection's stream concurrency slot. | |
| req2, _ := http.NewRequest("GET", "http://dummy.tld/2", nil) | |
| _ = tt.roundTrip(req2) | |
| tc2 := tt.getConn() | |
| tc2.wantFrameType(http2.FrameSettings) | |
| tc2.wantFrameType(http2.FrameWindowUpdate) | |
| tc2.wantHeaders(wantHeader{ | |
| streamID: 1, | |
| endStream: true, | |
| header: http.Header{ | |
| ":authority": []string{"dummy.tld"}, | |
| ":method": []string{"GET"}, | |
| ":path": []string{"/2"}, | |
| }, | |
| }) | |
| }) | |
| } | |
| // TestAPITransportIdleConnTimeout tests the Transport.IdleConnTimeout field. | |
| func TestAPITransportIdleConnTimeout(t *testing.T) { | |
| synctestTestRoundTrip(t, func(t *testing.T, mode roundTripTestMode) { | |
| const idleConnTimeout = 3 * time.Second | |
| tc := newTestClientConn(t, mode, func(tr2 *http2.Transport) { | |
| tr2.IdleConnTimeout = idleConnTimeout | |
| }) | |
| tc.greet() | |
| tc.wantIdle() | |
| closeDelay := tc.connReader.waitForData(t) | |
| tc.wantClosed() | |
| if got, want := closeDelay, idleConnTimeout; got != want { | |
| t.Errorf("time until close: %v, want %v", got, want) | |
| } | |
| }) | |
| } | |
| // TestAPITransportPingTimeout tests the | |
| // Transport.ReadIdleTimeout and Transport.PingTimeout fields. | |
| func TestAPITransportPingTimeout(t *testing.T) { | |
| synctestTestRoundTrip(t, func(t *testing.T, mode roundTripTestMode) { | |
| const readIdleTimeout = 3 * time.Second | |
| const pingTimeout = 5 * time.Second | |
| tc := newTestClientConn(t, mode, func(tr2 *http2.Transport) { | |
| tr2.ReadIdleTimeout = readIdleTimeout | |
| tr2.PingTimeout = pingTimeout | |
| }) | |
| tc.greet() | |
| tc.wantIdle() | |
| // PING is sent after ReadIdleTimeout. | |
| pingDelay := tc.connReader.waitForData(t) | |
| tc.wantFrameType(http2.FramePing) | |
| if got, want := pingDelay, readIdleTimeout; got != want { | |
| t.Errorf("time until PING: %v, want %v", got, want) | |
| } | |
| // Connection is closed after PingTimeout. | |
| closeDelay := tc.connReader.waitForData(t) | |
| tc.wantClosed() | |
| if got, want := closeDelay, pingTimeout; got != want { | |
| t.Errorf("time after PING until close: %v, want %v", got, want) | |
| } | |
| }) | |
| } | |
| // TestAPITransportWriteByteTimeout tests the Transport.WriteByteTimeout field. | |
| func TestAPITransportWriteByteTimeout(t *testing.T) { | |
| synctestTestRoundTrip(t, func(t *testing.T, mode roundTripTestMode) { | |
| const writeByteTimeout = 3 * time.Second | |
| tt := newTestTransport(t, mode, func(tr2 *http2.Transport) { | |
| tr2.WriteByteTimeout = writeByteTimeout | |
| }) | |
| req1, _ := http.NewRequest("GET", "http://dummy.tld/1", nil) | |
| _ = tt.roundTrip(req1) | |
| tc := tt.getConn() | |
| tc.wantFrameType(http2.FrameSettings) | |
| tc.wantFrameType(http2.FrameWindowUpdate) | |
| tc.wantFrameType(http2.FrameHeaders) | |
| // Block writes (past the first byte). | |
| // Just sleeping for WriteByteTimeout shouldn't do anything. | |
| tc.connReader.stop() | |
| tc.netconn.SetReadBufferSize(1) | |
| time.Sleep(2 * writeByteTimeout) | |
| tc.wantIdle() | |
| // Sending a new request will fail after the write timeout. | |
| // | |
| // We need to sleep for 2*writeByteTimeout, since we'll read | |
| // 2 bytes during the first timeout period. (We set a 1-byte buffer, | |
| // the smallest the test conn permits, which still allows for writing a byte.) | |
| req2, _ := http.NewRequest("GET", "http://dummy.tld/", nil) | |
| _ = tt.roundTrip(req2) | |
| time.Sleep(2 * writeByteTimeout) | |
| synctest.Wait() | |
| // Drain the partial request from the conn, | |
| // after which we can observe that it has been closed. | |
| tc.connReader.start() | |
| io.Copy(io.Discard, tc.connReader) | |
| tc.wantClosed() | |
| }) | |
| } | |
| // TestAPITransportCountError tests the Transport.CountError field. | |
| func TestAPITransportCountError(t *testing.T) { | |
| synctestTestRoundTrip(t, func(t *testing.T, mode roundTripTestMode) { | |
| countError := 0 | |
| tc := newTestClientConn(t, mode, func(tr2 *http2.Transport) { | |
| tr2.CountError = func(errType string) { | |
| countError++ | |
| } | |
| }) | |
| tc.greet() | |
| tc.netconn.Close() | |
| synctest.Wait() | |
| if countError != 1 { | |
| t.Errorf("after connection error: CountError called %v times, want 1", countError) | |
| } | |
| }) | |
| } | |
| func TestAPITransportIDNA(t *testing.T) { | |
| type dialErr struct { | |
| error | |
| address string | |
| } | |
| tr := &http2.Transport{ | |
| DialTLSContext: func(ctx context.Context, network, address string, conf *tls.Config) (net.Conn, error) { | |
| host, _, _ := net.SplitHostPort(address) | |
| return nil, dialErr{ | |
| error: errors.New("dial error"), | |
| address: host, | |
| } | |
| }, | |
| } | |
| for _, test := range []struct { | |
| url string | |
| wantDialAddr string | |
| skip bool | |
| }{{ | |
| url: "https://example.tld/", | |
| wantDialAddr: "example.tld", | |
| }, { | |
| // Invalid host, but permitted because all-ASCII. | |
| url: "https://xn-example-.tld/", | |
| wantDialAddr: "xn-example-.tld", | |
| }, { | |
| url: "https://гофер.го/", | |
| wantDialAddr: "xn--c1ae0ajs.xn--c1aw", | |
| }, { | |
| // IDNA translation returns an error, we try using the untranslated string. | |
| url: "https://aâ’ˆcom/", | |
| wantDialAddr: "aâ’ˆcom", | |
| }, { | |
| // Unicode name converted to the empty string. | |
| url: "https://\u00ad/", | |
| wantDialAddr: "\u00ad", | |
| // When !http2legacy, this relies on a fix added in Go 1.28. | |
| // Just skip the test here when wrapping net/http. | |
| skip: wrappedAPI, | |
| }} { | |
| if test.skip { | |
| continue | |
| } | |
| req, _ := http.NewRequest("GET", test.url, nil) | |
| _, err := tr.RoundTrip(req) | |
| e, ok := err.(dialErr) | |
| if !ok { | |
| t.Fatalf("RoundTrip error %v; want dialErr", err) | |
| } | |
| if got, want := e.address, test.wantDialAddr; got != want { | |
| t.Errorf("RoundTrip for %q dialed %q, want %q", test.url, got, want) | |
| } | |
| } | |
| } | |
| type testClientConnPool struct { | |
| t *testing.T | |
| li *synctestNetListener | |
| tr2 *http2.Transport | |
| wantReq *http.Request | |
| wantDead *http2.ClientConn | |
| conns []*http2.ClientConn | |
| tlsConf *tls.Config | |
| } | |
| func (p *testClientConnPool) GetClientConn(req *http.Request, addr string) (cc *http2.ClientConn, err error) { | |
| if p.wantReq == nil { | |
| p.t.Errorf("unexpected call to ClientConnPool.GetClientConn") | |
| } | |
| conn := net.Conn(p.li.newConn()) | |
| if req.URL.Scheme == "https" { | |
| conn = tls.Client(conn, p.tlsConf) | |
| } | |
| cc, err = p.tr2.NewClientConn(conn) | |
| if cc != nil { | |
| p.conns = append(p.conns, cc) | |
| } | |
| p.wantReq = nil | |
| return cc, err | |
| } | |
| func (p *testClientConnPool) MarkDead(cc *http2.ClientConn) { | |
| if p.wantDead == nil { | |
| p.t.Errorf("unexpected call to ClientConnPool.MarkDead") | |
| } | |
| p.wantDead = nil | |
| } | |
| func (p *testClientConnPool) check() { | |
| p.t.Helper() | |
| synctest.Wait() | |
| if p.wantReq != nil { | |
| p.t.Errorf("wanted call to ClientConnPool.GetClientConn, got none") | |
| } | |
| if p.wantDead != nil { | |
| p.t.Errorf("wanted call to ClientConnPool.MarkDead, got none") | |
| } | |
| } | |
| // TestAPITransportConnPool tests the Transport.ConnPool field. | |
| func TestAPITransportConnPool(t *testing.T) { | |
| synctestTestRoundTrip(t, func(t *testing.T, mode roundTripTestMode) { | |
| const idleConnTimeout = 3 * time.Second | |
| pool := &testClientConnPool{ | |
| t: t, | |
| } | |
| tt := newTestTransport(t, mode, func(tr2 *http2.Transport) { | |
| tr2.ConnPool = pool | |
| }) | |
| tt.useTLS = true | |
| pool.tr2 = tt.tr | |
| pool.li = tt.li | |
| pool.tlsConf = testTLSClientConfig.Clone() | |
| pool.tlsConf.NextProtos = []string{"h2"} | |
| // Send a request. The pool creates a new connection. | |
| req1, _ := http.NewRequest("GET", "https://dummy.tld/", nil) | |
| pool.wantReq = req1 | |
| rt := tt.roundTrip(req1) | |
| pool.check() | |
| if len(pool.conns) != 1 { | |
| t.Fatalf("expected pool to create 1 conn, got %v", len(pool.conns)) | |
| } | |
| // This is the connection created by the ClientConnPool. | |
| tc1 := tt.getConn() | |
| tc1.wantFrameType(http2.FrameSettings) | |
| tc1.wantFrameType(http2.FrameWindowUpdate) | |
| tc1.wantFrameType(http2.FrameHeaders) | |
| tc1.writeSettings() | |
| tc1.wantFrameType(http2.FrameSettings) // ACK | |
| tc1.writeHeaders(http2.HeadersFrameParam{ | |
| StreamID: 1, | |
| EndHeaders: true, | |
| EndStream: true, | |
| BlockFragment: tc1.makeHeaderBlockFragment( | |
| ":status", "200", | |
| ), | |
| }) | |
| rt.wantStatus(200) | |
| // ClientConnPool.MarkDead is called when the connection closes. | |
| pool.wantDead = pool.conns[0] | |
| tc1.netconn.Close() | |
| pool.check() | |
| }) | |
| } | |
| // TestAPITransportNewClientConn tests the Transport.NewClientConn method. | |
| func TestAPITransportNewClientConn(t *testing.T) { | |
| synctest.Test(t, func(t *testing.T) { | |
| tt := newTestTransport(t, roundTripXNetHTTP2, func(tr2 *http2.Transport) { | |
| // ClientConnState.LastIdle is only set when there is an idle timer. | |
| tr2.IdleConnTimeout = 10 * time.Second | |
| }) | |
| nc := tt.li.newConn() | |
| cc, err := tt.tr.NewClientConn(nc) | |
| if err != nil { | |
| t.Fatalf("NewClientConn: %v", err) | |
| } | |
| tc1 := tt.getConn() | |
| tc1.wantFrameType(http2.FrameSettings) | |
| tc1.wantFrameType(http2.FrameWindowUpdate) | |
| tc1.writeSettings(http2.Setting{ | |
| ID: http2.SettingMaxConcurrentStreams, | |
| Val: 1, | |
| }) | |
| tc1.wantFrameType(http2.FrameSettings) // ACK | |
| synctest.Wait() | |
| wantClientConnState(t, cc.State(), http2.ClientConnState{ | |
| MaxConcurrentStreams: 1, | |
| }) | |
| if got, want := cc.CanTakeNewRequest(), true; got != want { | |
| t.Errorf("cc.CanTakeNewRequest() = %v, want %v", got, want) | |
| } | |
| if got, want := cc.ReserveNewRequest(), true; got != want { | |
| t.Errorf("cc.ReserveNewRequest() = %v, want %v", got, want) | |
| } | |
| // Reservation has consumed the one concurrency slot. | |
| if got, want := cc.CanTakeNewRequest(), false; got != want { | |
| t.Errorf("cc.CanTakeNewRequest() = %v, want %v (sole request slot reserved)", got, want) | |
| } | |
| wantClientConnState(t, cc.State(), http2.ClientConnState{ | |
| StreamsReserved: 1, | |
| MaxConcurrentStreams: 1, | |
| }) | |
| // Consume the reservation by sending a request. | |
| req1, _ := http.NewRequest("GET", "https://dummy.tld/", nil) | |
| rt1 := newTestRoundTrip(t, req1, cc.RoundTrip) | |
| tc1.wantFrameType(http2.FrameHeaders) | |
| wantClientConnState(t, cc.State(), http2.ClientConnState{ | |
| StreamsActive: 1, | |
| MaxConcurrentStreams: 1, | |
| }) | |
| tc1.writeHeaders(http2.HeadersFrameParam{ | |
| StreamID: 1, | |
| EndHeaders: true, | |
| EndStream: true, | |
| BlockFragment: tc1.makeHeaderBlockFragment( | |
| ":status", "200", | |
| ), | |
| }) | |
| rt1.wantStatus(200) | |
| if got, want := cc.CanTakeNewRequest(), true; got != want { | |
| t.Errorf("cc.CanTakeNewRequest() = %v, want %v", got, want) | |
| } | |
| wantClientConnState(t, cc.State(), http2.ClientConnState{ | |
| MaxConcurrentStreams: 1, | |
| LastIdle: time.Now(), | |
| }) | |
| cc.SetDoNotReuse() | |
| wantClientConnState(t, cc.State(), http2.ClientConnState{ | |
| Closing: true, | |
| MaxConcurrentStreams: 1, | |
| LastIdle: time.Now(), | |
| }) | |
| req2, _ := http.NewRequest("GET", "https://dummy.tld/", nil) | |
| rt2 := newTestRoundTrip(t, req2, cc.RoundTrip) | |
| if rt2.err() == nil { | |
| t.Fatalf("RoundTrip after SetDoNotReuse: succeeded, want error") | |
| } | |
| if err := cc.Close(); err != nil { | |
| t.Errorf("cc.Close() = %v, want nil", err) | |
| } | |
| wantClientConnState(t, cc.State(), http2.ClientConnState{ | |
| Closing: true, | |
| Closed: true, | |
| MaxConcurrentStreams: 1, | |
| LastIdle: time.Now(), | |
| }) | |
| }) | |
| } | |
| // TestAPITransportClientConnPending tests the ClientConnState.Pending state. | |
| func TestAPITransportClientConnPending(t *testing.T) { | |
| synctest.Test(t, func(t *testing.T) { | |
| tt := newTestTransport(t, roundTripXNetHTTP2, func(tr2 *http2.Transport) { | |
| tr2.StrictMaxConcurrentStreams = true | |
| }) | |
| nc := tt.li.newConn() | |
| cc, err := tt.tr.NewClientConn(nc) | |
| if err != nil { | |
| t.Fatalf("NewClientConn: %v", err) | |
| } | |
| tc1 := tt.getConn() | |
| tc1.wantFrameType(http2.FrameSettings) | |
| tc1.wantFrameType(http2.FrameWindowUpdate) | |
| tc1.writeSettings(http2.Setting{ | |
| ID: http2.SettingMaxConcurrentStreams, | |
| Val: 1, | |
| }) | |
| tc1.wantFrameType(http2.FrameSettings) // ACK | |
| synctest.Wait() | |
| wantClientConnState(t, cc.State(), http2.ClientConnState{ | |
| MaxConcurrentStreams: 1, | |
| }) | |
| // Send a request, consuming the concurrency slot. | |
| req1, _ := http.NewRequest("GET", "https://dummy.tld/", nil) | |
| rt1 := newTestRoundTrip(t, req1, cc.RoundTrip) | |
| tc1.wantFrameType(http2.FrameHeaders) | |
| wantClientConnState(t, cc.State(), http2.ClientConnState{ | |
| MaxConcurrentStreams: 1, | |
| StreamsActive: 1, | |
| }) | |
| // Send another request, which enters the Pending state. | |
| req2, _ := http.NewRequest("GET", "https://dummy.tld/", nil) | |
| _ = newTestRoundTrip(t, req2, cc.RoundTrip) | |
| tc1.wantIdle() | |
| wantClientConnState(t, cc.State(), http2.ClientConnState{ | |
| MaxConcurrentStreams: 1, | |
| StreamsActive: 1, | |
| StreamsPending: 1, | |
| }) | |
| // First request completes, second starts. | |
| tc1.writeHeaders(http2.HeadersFrameParam{ | |
| StreamID: 1, | |
| EndHeaders: true, | |
| EndStream: true, | |
| BlockFragment: tc1.makeHeaderBlockFragment( | |
| ":status", "200", | |
| ), | |
| }) | |
| rt1.wantStatus(200) | |
| tc1.wantFrameType(http2.FrameHeaders) | |
| wantClientConnState(t, cc.State(), http2.ClientConnState{ | |
| MaxConcurrentStreams: 1, | |
| StreamsActive: 1, | |
| }) | |
| }) | |
| } | |
| // TestAPIClientConnPing tests the ClientConn.Ping method. | |
| func TestAPIClientConnPing(t *testing.T) { | |
| synctest.Test(t, func(t *testing.T) { | |
| tt := newTestTransport(t, roundTripXNetHTTP2) | |
| nc := tt.li.newConn() | |
| cc, err := tt.tr.NewClientConn(nc) | |
| if err != nil { | |
| t.Fatalf("NewClientConn: %v", err) | |
| } | |
| tc1 := tt.getConn() | |
| tc1.wantFrameType(http2.FrameSettings) | |
| tc1.wantFrameType(http2.FrameWindowUpdate) | |
| tc1.writeSettings(http2.Setting{ | |
| ID: http2.SettingMaxConcurrentStreams, | |
| Val: 1, | |
| }) | |
| tc1.wantFrameType(http2.FrameSettings) // ACK | |
| tc1.wantIdle() | |
| // Ping with successful response. | |
| pingDone := false | |
| var pingErr error | |
| go func() { | |
| pingErr = cc.Ping(context.Background()) | |
| pingDone = true | |
| }() | |
| synctest.Wait() | |
| fr := readFrame[*http2.PingFrame](t, tc1) | |
| if pingDone { | |
| t.Fatalf("cc.Ping() = %v; want to still be running", pingErr) | |
| } | |
| tc1.writePing(true, fr.Data) | |
| synctest.Wait() | |
| if !pingDone { | |
| t.Fatalf("cc.Ping() still running; want to be done") | |
| } | |
| if pingErr != nil { | |
| t.Fatalf("cc.Ping() = %v; want nil", pingErr) | |
| } | |
| // Ping with no response. | |
| const timeout = 1 * time.Second | |
| ctx, cancel := context.WithTimeout(context.Background(), timeout) | |
| defer cancel() | |
| start := time.Now() | |
| if err := cc.Ping(ctx); err == nil { | |
| t.Fatalf("cc.Ping() = nil; want error") | |
| } | |
| if got, want := time.Since(start), timeout; got != want { | |
| t.Fatalf("cc.Ping() returned after %v, want %v", got, want) | |
| } | |
| }) | |
| } | |
| // TestAPITransportClientConnReserveNewRequest tests the ClientConn ReserveNewRequest method. | |
| func TestAPITransportClientConnReserveNewRequest(t *testing.T) { | |
| synctest.Test(t, func(t *testing.T) { | |
| tt := newTestTransport(t, roundTripXNetHTTP2) | |
| nc := tt.li.newConn() | |
| cc, err := tt.tr.NewClientConn(nc) | |
| if err != nil { | |
| t.Fatalf("NewClientConn: %v", err) | |
| } | |
| tc1 := tt.getConn() | |
| tc1.wantFrameType(http2.FrameSettings) | |
| tc1.wantFrameType(http2.FrameWindowUpdate) | |
| tc1.writeSettings(http2.Setting{ | |
| ID: http2.SettingMaxConcurrentStreams, | |
| Val: 1, | |
| }) | |
| tc1.wantFrameType(http2.FrameSettings) // ACK | |
| synctest.Wait() | |
| wantClientConnState(t, cc.State(), http2.ClientConnState{ | |
| MaxConcurrentStreams: 1, | |
| }) | |
| if got, want := cc.ReserveNewRequest(), true; got != want { | |
| t.Fatalf("cc.ReserveNewRequest() = %v, want %v", got, want) | |
| } | |
| wantClientConnState(t, cc.State(), http2.ClientConnState{ | |
| MaxConcurrentStreams: 1, | |
| StreamsReserved: 1, | |
| }) | |
| // Issue #80680: Deadlock when ReserveNewRequest invokes the state hook. | |
| go func() { | |
| for i := range 100 { | |
| tc1.writeSettings(http2.Setting{ | |
| ID: http2.SettingMaxConcurrentStreams, | |
| Val: uint32(i * 2), | |
| }) | |
| } | |
| }() | |
| go func() { | |
| for range 100 { | |
| cc.ReserveNewRequest() | |
| } | |
| }() | |
| synctest.Wait() | |
| }) | |
| } | |
| func TestAPITransportSettingsRoundTripRace(t *testing.T) { | |
| synctest.Test(t, func(t *testing.T) { | |
| tt := newTestTransport(t, roundTripXNetHTTP2) | |
| nc := tt.li.newConn() | |
| cc, err := tt.tr.NewClientConn(nc) | |
| if err != nil { | |
| t.Fatalf("NewClientConn: %v", err) | |
| } | |
| tc1 := tt.getConn() | |
| tc1.wantFrameType(http2.FrameSettings) | |
| tc1.wantFrameType(http2.FrameWindowUpdate) | |
| tc1.writeSettings(http2.Setting{ | |
| ID: http2.SettingMaxConcurrentStreams, | |
| Val: 1, | |
| }) | |
| tc1.wantFrameType(http2.FrameSettings) // ACK | |
| synctest.Wait() | |
| go func() { | |
| for i := range 100 { | |
| tc1.writeSettings(http2.Setting{ | |
| ID: http2.SettingMaxConcurrentStreams, | |
| Val: uint32(i * 2), | |
| }) | |
| } | |
| }() | |
| go func() { | |
| for range 100 { | |
| req, _ := http.NewRequest("GET", "http://example.tld/", nil) | |
| resp, err := cc.RoundTrip(req) | |
| if err == nil { | |
| resp.Body.Close() | |
| } | |
| } | |
| }() | |
| synctest.Wait() | |
| }) | |
| } | |
| // TestAPIClientConnShutdown tests the ClientConn.Shutdown method. | |
| // Shutdown returns after the last request on the connection completes. | |
| func TestAPIClientConnShutdownSuccess(t *testing.T) { | |
| synctest.Test(t, func(t *testing.T) { | |
| tt := newTestTransport(t, roundTripXNetHTTP2) | |
| nc := tt.li.newConn() | |
| cc, err := tt.tr.NewClientConn(nc) | |
| if err != nil { | |
| t.Fatalf("NewClientConn: %v", err) | |
| } | |
| tc1 := tt.getConn() | |
| tc1.greet() | |
| // Start a request. | |
| req1, _ := http.NewRequest("GET", "https://dummy.tld/", nil) | |
| rt1 := newTestRoundTrip(t, req1, cc.RoundTrip) | |
| tc1.wantFrameType(http2.FrameHeaders) | |
| // Start shutdown. | |
| var shutdownErr error | |
| shutdownDone := false | |
| go func() { | |
| shutdownErr = cc.Shutdown(context.Background()) | |
| shutdownDone = true | |
| }() | |
| synctest.Wait() | |
| if shutdownDone { | |
| t.Fatalf("cc.Shutdown() = %v; want still running with req in flight", err) | |
| } | |
| if rt1.done() { | |
| t.Fatalf("RoundTrip finished; want still running") | |
| } | |
| // Server terminates the outstanding request, connection shuts down. | |
| tc1.writeRSTStream(1, http2.ErrCodeCancel) | |
| synctest.Wait() | |
| if !shutdownDone { | |
| t.Fatalf("cc.Shutdown() still running; want to have returned") | |
| } | |
| if shutdownErr != nil { | |
| t.Fatalf("cc.Shutdown() = %v; want nil", err) | |
| } | |
| if err := rt1.err(); err == nil { | |
| t.Fatalf("RoundTrip succeeded; want error") | |
| } | |
| // We might send a GOAWAY frame before closing. | |
| if fr := tc1.readFrame(); fr != nil { | |
| if _, ok := fr.(*http2.GoAwayFrame); !ok { | |
| t.Fatalf("read frame %v; want GOAWAY or nothing", fr) | |
| } | |
| } | |
| tc1.wantClosed() | |
| }) | |
| } | |
| // TestAPIClientConnShutdown tests the ClientConn.Shutdown method. | |
| // Shutdown's context expires before the last request on the connection completes. | |
| func TestAPIClientConnShutdownFailure(t *testing.T) { | |
| synctest.Test(t, func(t *testing.T) { | |
| tt := newTestTransport(t, roundTripXNetHTTP2) | |
| nc := tt.li.newConn() | |
| cc, err := tt.tr.NewClientConn(nc) | |
| if err != nil { | |
| t.Fatalf("NewClientConn: %v", err) | |
| } | |
| tc1 := tt.getConn() | |
| tc1.greet() | |
| // Start a request. | |
| req1, _ := http.NewRequest("GET", "https://dummy.tld/", nil) | |
| _ = newTestRoundTrip(t, req1, cc.RoundTrip) | |
| tc1.wantFrameType(http2.FrameHeaders) | |
| // Shutdown's context expires before the request completes. | |
| const timeout = 1 * time.Second | |
| ctx, cancel := context.WithTimeout(context.Background(), timeout) | |
| defer cancel() | |
| start := time.Now() | |
| if err := cc.Shutdown(ctx); !errors.Is(err, context.DeadlineExceeded) { | |
| t.Errorf("cc.Shutdown() = %v; want DeadlineExceeded", err) | |
| } | |
| if got, want := time.Since(start), timeout; got != want { | |
| t.Fatalf("cc.Shutdown() returned after %v, want %v", got, want) | |
| } | |
| // We might send a GOAWAY frame before closing. | |
| if fr := tc1.readFrame(); fr != nil { | |
| if _, ok := fr.(*http2.GoAwayFrame); !ok { | |
| t.Fatalf("read frame %v; want GOAWAY or nothing", fr) | |
| } | |
| } | |
| tc1.wantIdle() | |
| }) | |
| } | |
| func wantClientConnState(t *testing.T, a, b http2.ClientConnState) { | |
| t.Helper() | |
| if got, want := a.Closed, b.Closed; got != want { | |
| t.Errorf("ClientConnState.Closed = %v, want %v", got, want) | |
| } | |
| if got, want := a.Closing, b.Closing; got != want { | |
| t.Errorf("ClientConnState.Closing = %v, want %v", got, want) | |
| } | |
| if got, want := a.StreamsActive, b.StreamsActive; got != want { | |
| t.Errorf("ClientConnState.StreamsActive = %v, want %v", got, want) | |
| } | |
| if got, want := a.StreamsReserved, b.StreamsReserved; got != want { | |
| t.Errorf("ClientConnState.StreamsReserved = %v, want %v", got, want) | |
| } | |
| if got, want := a.StreamsPending, b.StreamsPending; got != want { | |
| t.Errorf("ClientConnState.StreamsPending = %v, want %v", got, want) | |
| } | |
| if got, want := a.MaxConcurrentStreams, b.MaxConcurrentStreams; got != want { | |
| t.Errorf("ClientConnState.MaxConcurrentStreams = %v, want %v", got, want) | |
| } | |
| if got, want := a.LastIdle, b.LastIdle; !got.Equal(want) { | |
| t.Errorf("ClientConnState.LastIdle = %v, want %v", got, want) | |
| } | |
| } |