github.com/apernet/quic-go@v0.43.1-0.20240515053213-5e9e635fd9f0/http3/roundtrip_test.go (about)

     1  package http3
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"crypto/tls"
     7  	"errors"
     8  	"io"
     9  	"net/http"
    10  	"time"
    11  
    12  	"github.com/apernet/quic-go"
    13  	mockquic "github.com/apernet/quic-go/internal/mocks/quic"
    14  	"github.com/apernet/quic-go/internal/protocol"
    15  	"github.com/apernet/quic-go/internal/qerr"
    16  
    17  	. "github.com/onsi/ginkgo/v2"
    18  	. "github.com/onsi/gomega"
    19  	"go.uber.org/mock/gomock"
    20  )
    21  
    22  type mockBody struct {
    23  	reader   bytes.Reader
    24  	readErr  error
    25  	closeErr error
    26  	closed   bool
    27  }
    28  
    29  // make sure the mockBody can be used as a http.Request.Body
    30  var _ io.ReadCloser = &mockBody{}
    31  
    32  func (m *mockBody) Read(p []byte) (int, error) {
    33  	if m.readErr != nil {
    34  		return 0, m.readErr
    35  	}
    36  	return m.reader.Read(p)
    37  }
    38  
    39  func (m *mockBody) SetData(data []byte) {
    40  	m.reader = *bytes.NewReader(data)
    41  }
    42  
    43  func (m *mockBody) Close() error {
    44  	m.closed = true
    45  	return m.closeErr
    46  }
    47  
    48  var _ = Describe("RoundTripper", func() {
    49  	var req *http.Request
    50  
    51  	BeforeEach(func() {
    52  		var err error
    53  		req, err = http.NewRequest("GET", "https://www.example.org/file1.html", nil)
    54  		Expect(err).ToNot(HaveOccurred())
    55  	})
    56  
    57  	It("rejects quic.Configs that allow multiple QUIC versions", func() {
    58  		qconf := &quic.Config{
    59  			Versions: []quic.Version{protocol.Version2, protocol.Version1},
    60  		}
    61  		rt := &RoundTripper{QUICConfig: qconf}
    62  		_, err := rt.RoundTrip(req)
    63  		Expect(err).To(MatchError("can only use a single QUIC version for dialing a HTTP/3 connection"))
    64  	})
    65  
    66  	It("uses the default QUIC and TLS config if none is give", func() {
    67  		var dialAddrCalled bool
    68  		rt := &RoundTripper{
    69  			Dial: func(_ context.Context, _ string, tlsConf *tls.Config, quicConf *quic.Config) (quic.EarlyConnection, error) {
    70  				defer GinkgoRecover()
    71  				Expect(quicConf.MaxIncomingStreams).To(Equal(defaultQuicConfig.MaxIncomingStreams))
    72  				Expect(tlsConf.NextProtos).To(Equal([]string{NextProtoH3}))
    73  				Expect(quicConf.Versions).To(Equal([]protocol.Version{protocol.Version1}))
    74  				dialAddrCalled = true
    75  				return nil, errors.New("test done")
    76  			},
    77  		}
    78  		_, err := rt.RoundTripOpt(req, RoundTripOpt{})
    79  		Expect(err).To(MatchError("test done"))
    80  		Expect(dialAddrCalled).To(BeTrue())
    81  	})
    82  
    83  	It("adds the port to the hostname, if none is given", func() {
    84  		var dialAddrCalled bool
    85  		rt := &RoundTripper{
    86  			Dial: func(_ context.Context, hostname string, _ *tls.Config, _ *quic.Config) (quic.EarlyConnection, error) {
    87  				defer GinkgoRecover()
    88  				Expect(hostname).To(Equal("quic.clemente.io:443"))
    89  				dialAddrCalled = true
    90  				return nil, errors.New("test done")
    91  			},
    92  		}
    93  		req, err := http.NewRequest("GET", "https://quic.clemente.io:443", nil)
    94  		Expect(err).ToNot(HaveOccurred())
    95  		_, err = rt.RoundTripOpt(req, RoundTripOpt{})
    96  		Expect(err).To(MatchError("test done"))
    97  		Expect(dialAddrCalled).To(BeTrue())
    98  	})
    99  
   100  	It("sets the ServerName in the tls.Config, if not set", func() {
   101  		const host = "foo.bar"
   102  		var dialCalled bool
   103  		rt := &RoundTripper{
   104  			Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
   105  				defer GinkgoRecover()
   106  				Expect(tlsCfg.ServerName).To(Equal(host))
   107  				dialCalled = true
   108  				return nil, errors.New("test done")
   109  			},
   110  		}
   111  		req, err := http.NewRequest("GET", "https://foo.bar", nil)
   112  		Expect(err).ToNot(HaveOccurred())
   113  		_, err = rt.RoundTripOpt(req, RoundTripOpt{})
   114  		Expect(err).To(MatchError("test done"))
   115  		Expect(dialCalled).To(BeTrue())
   116  	})
   117  
   118  	It("uses the TLS config and QUIC config", func() {
   119  		tlsConf := &tls.Config{
   120  			ServerName: "foo.bar",
   121  			NextProtos: []string{"proto foo", "proto bar"},
   122  		}
   123  		quicConf := &quic.Config{MaxIdleTimeout: 3 * time.Nanosecond}
   124  		var dialAddrCalled bool
   125  		rt := &RoundTripper{
   126  			Dial: func(_ context.Context, host string, tlsConfP *tls.Config, quicConfP *quic.Config) (quic.EarlyConnection, error) {
   127  				defer GinkgoRecover()
   128  				Expect(host).To(Equal("www.example.org:443"))
   129  				Expect(tlsConfP.ServerName).To(Equal(tlsConf.ServerName))
   130  				Expect(tlsConfP.NextProtos).To(Equal([]string{NextProtoH3}))
   131  				Expect(quicConfP.MaxIdleTimeout).To(Equal(quicConf.MaxIdleTimeout))
   132  				dialAddrCalled = true
   133  				return nil, errors.New("test done")
   134  			},
   135  			QUICConfig:      quicConf,
   136  			TLSClientConfig: tlsConf,
   137  		}
   138  		_, err := rt.RoundTripOpt(req, RoundTripOpt{})
   139  		Expect(err).To(MatchError("test done"))
   140  		Expect(dialAddrCalled).To(BeTrue())
   141  		// make sure the original tls.Config was not modified
   142  		Expect(tlsConf.NextProtos).To(Equal([]string{"proto foo", "proto bar"}))
   143  	})
   144  
   145  	It("uses the custom dialer, if provided", func() {
   146  		testErr := errors.New("test done")
   147  		tlsConf := &tls.Config{ServerName: "foo.bar"}
   148  		quicConf := &quic.Config{MaxIdleTimeout: 1337 * time.Second}
   149  		// nolint:staticcheck // This is a test.
   150  		ctx := context.WithValue(context.Background(), "foo", "bar")
   151  		var dialerCalled bool
   152  		rt := &RoundTripper{
   153  			Dial: func(ctxP context.Context, address string, tlsConfP *tls.Config, quicConfP *quic.Config) (quic.EarlyConnection, error) {
   154  				defer GinkgoRecover()
   155  				Expect(ctx.Value("foo").(string)).To(Equal("bar"))
   156  				Expect(address).To(Equal("www.example.org:443"))
   157  				Expect(tlsConfP.ServerName).To(Equal("foo.bar"))
   158  				Expect(quicConfP.MaxIdleTimeout).To(Equal(quicConf.MaxIdleTimeout))
   159  				dialerCalled = true
   160  				return nil, testErr
   161  			},
   162  			TLSClientConfig: tlsConf,
   163  			QUICConfig:      quicConf,
   164  		}
   165  		_, err := rt.RoundTripOpt(req.WithContext(ctx), RoundTripOpt{})
   166  		Expect(err).To(MatchError(testErr))
   167  		Expect(dialerCalled).To(BeTrue())
   168  	})
   169  
   170  	It("enables HTTP/3 Datagrams", func() {
   171  		testErr := errors.New("handshake error")
   172  		rt := &RoundTripper{
   173  			EnableDatagrams: true,
   174  			Dial: func(_ context.Context, _ string, _ *tls.Config, quicConf *quic.Config) (quic.EarlyConnection, error) {
   175  				defer GinkgoRecover()
   176  				Expect(quicConf.EnableDatagrams).To(BeTrue())
   177  				return nil, testErr
   178  			},
   179  		}
   180  		_, err := rt.RoundTripOpt(req, RoundTripOpt{})
   181  		Expect(err).To(MatchError(testErr))
   182  	})
   183  
   184  	It("requires quic.Config.EnableDatagrams if HTTP/3 datagrams are enabled", func() {
   185  		rt := &RoundTripper{
   186  			QUICConfig:      &quic.Config{EnableDatagrams: false},
   187  			EnableDatagrams: true,
   188  			Dial: func(_ context.Context, _ string, _ *tls.Config, config *quic.Config) (quic.EarlyConnection, error) {
   189  				return nil, errors.New("handshake error")
   190  			},
   191  		}
   192  		_, err := rt.RoundTrip(req)
   193  		Expect(err).To(MatchError("HTTP Datagrams enabled, but QUIC Datagrams disabled"))
   194  	})
   195  
   196  	It("creates new clients", func() {
   197  		testErr := errors.New("test err")
   198  		req1, err := http.NewRequest("GET", "https://quic-go.net/foobar.html", nil)
   199  		Expect(err).ToNot(HaveOccurred())
   200  		req2, err := http.NewRequest("GET", "https://example.com/foobar.html", nil)
   201  		Expect(err).ToNot(HaveOccurred())
   202  		var hostsDialed []string
   203  		rt := &RoundTripper{
   204  			Dial: func(_ context.Context, host string, _ *tls.Config, quicConf *quic.Config) (quic.EarlyConnection, error) {
   205  				hostsDialed = append(hostsDialed, host)
   206  				return nil, testErr
   207  			},
   208  		}
   209  		_, err = rt.RoundTrip(req1)
   210  		Expect(err).To(MatchError(testErr))
   211  		_, err = rt.RoundTrip(req2)
   212  		Expect(err).To(MatchError(testErr))
   213  		Expect(hostsDialed).To(Equal([]string{"quic-go.net:443", "example.com:443"}))
   214  	})
   215  
   216  	Context("reusing clients", func() {
   217  		var (
   218  			rt         *RoundTripper
   219  			req1, req2 *http.Request
   220  			clientChan chan *MockSingleRoundTripper
   221  		)
   222  
   223  		BeforeEach(func() {
   224  			clientChan = make(chan *MockSingleRoundTripper, 16)
   225  			rt = &RoundTripper{
   226  				newClient: func(quic.EarlyConnection) singleRoundTripper {
   227  					select {
   228  					case c := <-clientChan:
   229  						return c
   230  					default:
   231  						Fail("no client")
   232  						return nil
   233  					}
   234  				},
   235  			}
   236  			var err error
   237  			req1, err = http.NewRequest("GET", "https://quic-go.net/file1.html", nil)
   238  			Expect(err).ToNot(HaveOccurred())
   239  			req2, err = http.NewRequest("GET", "https://quic-go.net/file2.html", nil)
   240  			Expect(err).ToNot(HaveOccurred())
   241  			Expect(req1.URL).ToNot(Equal(req2.URL))
   242  		})
   243  
   244  		It("reuses existing clients", func() {
   245  			cl := NewMockSingleRoundTripper(mockCtrl)
   246  			clientChan <- cl
   247  			conn := mockquic.NewMockEarlyConnection(mockCtrl)
   248  			handshakeChan := make(chan struct{})
   249  			close(handshakeChan)
   250  			conn.EXPECT().HandshakeComplete().Return(handshakeChan).MaxTimes(2)
   251  
   252  			cl.EXPECT().RoundTrip(req1).Return(&http.Response{Request: req1}, nil)
   253  			cl.EXPECT().RoundTrip(req2).Return(&http.Response{Request: req2}, nil)
   254  			var count int
   255  			rt.Dial = func(context.Context, string, *tls.Config, *quic.Config) (quic.EarlyConnection, error) {
   256  				count++
   257  				return conn, nil
   258  			}
   259  			rsp, err := rt.RoundTrip(req1)
   260  			Expect(err).ToNot(HaveOccurred())
   261  			Expect(rsp.Request).To(Equal(req1))
   262  			rsp, err = rt.RoundTrip(req2)
   263  			Expect(err).ToNot(HaveOccurred())
   264  			Expect(rsp.Request).To(Equal(req2))
   265  			Expect(count).To(Equal(1))
   266  		})
   267  
   268  		It("immediately removes a clients when a request errored", func() {
   269  			cl1 := NewMockSingleRoundTripper(mockCtrl)
   270  			clientChan <- cl1
   271  			cl2 := NewMockSingleRoundTripper(mockCtrl)
   272  			clientChan <- cl2
   273  
   274  			req1, err := http.NewRequest("GET", "https://quic-go.net/foobar.html", nil)
   275  			Expect(err).ToNot(HaveOccurred())
   276  			req2, err := http.NewRequest("GET", "https://quic-go.net/bar.html", nil)
   277  			Expect(err).ToNot(HaveOccurred())
   278  
   279  			conn := mockquic.NewMockEarlyConnection(mockCtrl)
   280  			var count int
   281  			rt.Dial = func(context.Context, string, *tls.Config, *quic.Config) (quic.EarlyConnection, error) {
   282  				count++
   283  				return conn, nil
   284  			}
   285  			testErr := errors.New("test err")
   286  			handshakeChan := make(chan struct{})
   287  			close(handshakeChan)
   288  			conn.EXPECT().HandshakeComplete().Return(handshakeChan).MaxTimes(2)
   289  			cl1.EXPECT().RoundTrip(req1).Return(nil, testErr)
   290  			cl2.EXPECT().RoundTrip(req2).Return(&http.Response{Request: req2}, nil)
   291  			_, err = rt.RoundTrip(req1)
   292  			Expect(err).To(MatchError(testErr))
   293  			rsp, err := rt.RoundTrip(req2)
   294  			Expect(err).ToNot(HaveOccurred())
   295  			Expect(rsp.Request).To(Equal(req2))
   296  			Expect(count).To(Equal(2))
   297  		})
   298  
   299  		It("does not remove a client when a request returns context canceled error", func() {
   300  			cl1 := NewMockSingleRoundTripper(mockCtrl)
   301  			clientChan <- cl1
   302  			cl2 := NewMockSingleRoundTripper(mockCtrl)
   303  			clientChan <- cl2
   304  
   305  			req1, err := http.NewRequest("GET", "https://quic-go.net/foobar.html", nil)
   306  			Expect(err).ToNot(HaveOccurred())
   307  			req2, err := http.NewRequest("GET", "https://quic-go.net/bar.html", nil)
   308  			Expect(err).ToNot(HaveOccurred())
   309  
   310  			conn := mockquic.NewMockEarlyConnection(mockCtrl)
   311  			var count int
   312  			rt.Dial = func(context.Context, string, *tls.Config, *quic.Config) (quic.EarlyConnection, error) {
   313  				count++
   314  				return conn, nil
   315  			}
   316  			testErr := context.Canceled
   317  			handshakeChan := make(chan struct{})
   318  			close(handshakeChan)
   319  			conn.EXPECT().HandshakeComplete().Return(handshakeChan).MaxTimes(2)
   320  			cl1.EXPECT().RoundTrip(req1).Return(nil, testErr)
   321  			cl1.EXPECT().RoundTrip(req2).Return(&http.Response{Request: req2}, nil)
   322  			_, err = rt.RoundTrip(req1)
   323  			Expect(err).To(MatchError(testErr))
   324  			rsp, err := rt.RoundTrip(req2)
   325  			Expect(err).ToNot(HaveOccurred())
   326  			Expect(rsp.Request).To(Equal(req2))
   327  			Expect(count).To(Equal(1))
   328  		})
   329  
   330  		It("recreates a client when a request times out", func() {
   331  			var reqCount int
   332  			cl1 := NewMockSingleRoundTripper(mockCtrl)
   333  			cl1.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(func(req *http.Request) (*http.Response, error) {
   334  				reqCount++
   335  				if reqCount == 1 { // the first request is successful...
   336  					Expect(req.URL).To(Equal(req1.URL))
   337  					return &http.Response{Request: req}, nil
   338  				}
   339  				// ... after that, the connection timed out in the background
   340  				Expect(req.URL).To(Equal(req2.URL))
   341  				return nil, &qerr.IdleTimeoutError{}
   342  			}).Times(2)
   343  			cl2 := NewMockSingleRoundTripper(mockCtrl)
   344  			cl2.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(func(req *http.Request) (*http.Response, error) {
   345  				return &http.Response{Request: req}, nil
   346  			})
   347  			clientChan <- cl1
   348  			clientChan <- cl2
   349  
   350  			conn := mockquic.NewMockEarlyConnection(mockCtrl)
   351  			handshakeChan := make(chan struct{})
   352  			close(handshakeChan)
   353  			conn.EXPECT().HandshakeComplete().Return(handshakeChan).MaxTimes(2)
   354  			var count int
   355  			rt.Dial = func(context.Context, string, *tls.Config, *quic.Config) (quic.EarlyConnection, error) {
   356  				count++
   357  				return conn, nil
   358  			}
   359  			rsp1, err := rt.RoundTrip(req1)
   360  			Expect(err).ToNot(HaveOccurred())
   361  			Expect(rsp1.Request.RemoteAddr).To(Equal(req1.RemoteAddr))
   362  			rsp2, err := rt.RoundTrip(req2)
   363  			Expect(err).ToNot(HaveOccurred())
   364  			Expect(rsp2.Request.RemoteAddr).To(Equal(req2.RemoteAddr))
   365  		})
   366  
   367  		It("only issues a request once, even if a timeout error occurs", func() {
   368  			var count int
   369  			rt.Dial = func(context.Context, string, *tls.Config, *quic.Config) (quic.EarlyConnection, error) {
   370  				count++
   371  				return mockquic.NewMockEarlyConnection(mockCtrl), nil
   372  			}
   373  			rt.newClient = func(quic.EarlyConnection) singleRoundTripper {
   374  				cl := NewMockSingleRoundTripper(mockCtrl)
   375  				cl.EXPECT().RoundTrip(gomock.Any()).Return(nil, &qerr.IdleTimeoutError{})
   376  				return cl
   377  			}
   378  			_, err := rt.RoundTrip(req1)
   379  			Expect(err).To(MatchError(&qerr.IdleTimeoutError{}))
   380  			Expect(count).To(Equal(1))
   381  		})
   382  
   383  		It("handles a burst of requests", func() {
   384  			wait := make(chan struct{})
   385  			reqs := make(chan struct{}, 2)
   386  
   387  			cl := NewMockSingleRoundTripper(mockCtrl)
   388  			cl.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(func(req *http.Request) (*http.Response, error) {
   389  				reqs <- struct{}{}
   390  				<-wait
   391  				return nil, &qerr.IdleTimeoutError{}
   392  			}).Times(2)
   393  			clientChan <- cl
   394  
   395  			conn := mockquic.NewMockEarlyConnection(mockCtrl)
   396  			conn.EXPECT().HandshakeComplete().Return(wait).AnyTimes()
   397  			var count int
   398  			rt.Dial = func(context.Context, string, *tls.Config, *quic.Config) (quic.EarlyConnection, error) {
   399  				count++
   400  				return conn, nil
   401  			}
   402  
   403  			done := make(chan struct{}, 2)
   404  			go func() {
   405  				defer GinkgoRecover()
   406  				defer func() { done <- struct{}{} }()
   407  				_, err := rt.RoundTrip(req1)
   408  				Expect(err).To(MatchError(&qerr.IdleTimeoutError{}))
   409  			}()
   410  			// wait for the first requests to be issued
   411  			Eventually(reqs).Should(Receive())
   412  			go func() {
   413  				defer GinkgoRecover()
   414  				defer func() { done <- struct{}{} }()
   415  				_, err := rt.RoundTrip(req2)
   416  				Expect(err).To(MatchError(&qerr.IdleTimeoutError{}))
   417  			}()
   418  			Eventually(reqs).Should(Receive())
   419  			close(wait) // now return the requests
   420  			Eventually(done).Should(Receive())
   421  			Eventually(done).Should(Receive())
   422  			Expect(count).To(Equal(1))
   423  		})
   424  
   425  		It("doesn't create new clients if RoundTripOpt.OnlyCachedConn is set", func() {
   426  			req, err := http.NewRequest("GET", "https://quic-go.net/foobar.html", nil)
   427  			Expect(err).ToNot(HaveOccurred())
   428  			_, err = rt.RoundTripOpt(req, RoundTripOpt{OnlyCachedConn: true})
   429  			Expect(err).To(MatchError(ErrNoCachedConn))
   430  		})
   431  	})
   432  
   433  	Context("validating request", func() {
   434  		var rt RoundTripper
   435  
   436  		It("rejects plain HTTP requests", func() {
   437  			req, err := http.NewRequest("GET", "http://www.example.org/", nil)
   438  			req.Body = &mockBody{}
   439  			Expect(err).ToNot(HaveOccurred())
   440  			_, err = rt.RoundTrip(req)
   441  			Expect(err).To(MatchError("http3: unsupported protocol scheme: http"))
   442  			Expect(req.Body.(*mockBody).closed).To(BeTrue())
   443  		})
   444  
   445  		It("rejects requests without a URL", func() {
   446  			req.URL = nil
   447  			req.Body = &mockBody{}
   448  			_, err := rt.RoundTrip(req)
   449  			Expect(err).To(MatchError("http3: nil Request.URL"))
   450  			Expect(req.Body.(*mockBody).closed).To(BeTrue())
   451  		})
   452  
   453  		It("rejects request without a URL Host", func() {
   454  			req.URL.Host = ""
   455  			req.Body = &mockBody{}
   456  			_, err := rt.RoundTrip(req)
   457  			Expect(err).To(MatchError("http3: no Host in request URL"))
   458  			Expect(req.Body.(*mockBody).closed).To(BeTrue())
   459  		})
   460  
   461  		It("doesn't try to close the body if the request doesn't have one", func() {
   462  			req.URL = nil
   463  			Expect(req.Body).To(BeNil())
   464  			_, err := rt.RoundTrip(req)
   465  			Expect(err).To(MatchError("http3: nil Request.URL"))
   466  		})
   467  
   468  		It("rejects requests without a header", func() {
   469  			req.Header = nil
   470  			req.Body = &mockBody{}
   471  			_, err := rt.RoundTrip(req)
   472  			Expect(err).To(MatchError("http3: nil Request.Header"))
   473  			Expect(req.Body.(*mockBody).closed).To(BeTrue())
   474  		})
   475  
   476  		It("rejects requests with invalid header name fields", func() {
   477  			req.Header.Add("foobär", "value")
   478  			_, err := rt.RoundTrip(req)
   479  			Expect(err).To(MatchError("http3: invalid http header field name \"foobär\""))
   480  		})
   481  
   482  		It("rejects requests with invalid header name values", func() {
   483  			req.Header.Add("foo", string([]byte{0x7}))
   484  			_, err := rt.RoundTrip(req)
   485  			Expect(err.Error()).To(ContainSubstring("http3: invalid http header field value"))
   486  		})
   487  
   488  		It("rejects requests with an invalid request method", func() {
   489  			req.Method = "foobär"
   490  			req.Body = &mockBody{}
   491  			_, err := rt.RoundTrip(req)
   492  			Expect(err).To(MatchError("http3: invalid method \"foobär\""))
   493  			Expect(req.Body.(*mockBody).closed).To(BeTrue())
   494  		})
   495  	})
   496  
   497  	Context("closing", func() {
   498  		It("closes", func() {
   499  			conn := mockquic.NewMockEarlyConnection(mockCtrl)
   500  			rt := &RoundTripper{
   501  				Dial: func(context.Context, string, *tls.Config, *quic.Config) (quic.EarlyConnection, error) {
   502  					return conn, nil
   503  				},
   504  				newClient: func(quic.EarlyConnection) singleRoundTripper {
   505  					cl := NewMockSingleRoundTripper(mockCtrl)
   506  					cl.EXPECT().RoundTrip(gomock.Any()).Return(&http.Response{}, nil)
   507  					return cl
   508  				},
   509  			}
   510  			req, err := http.NewRequest("GET", "https://quic-go.net/foobar.html", nil)
   511  			Expect(err).ToNot(HaveOccurred())
   512  			_, err = rt.RoundTrip(req)
   513  			Expect(err).ToNot(HaveOccurred())
   514  			conn.EXPECT().CloseWithError(quic.ApplicationErrorCode(0), "")
   515  			Expect(rt.Close()).To(Succeed())
   516  		})
   517  
   518  		It("closes while dialing", func() {
   519  			rt := &RoundTripper{
   520  				Dial: func(ctx context.Context, _ string, _ *tls.Config, _ *quic.Config) (quic.EarlyConnection, error) {
   521  					defer GinkgoRecover()
   522  					Eventually(ctx.Done()).Should(BeClosed())
   523  					return nil, errors.New("cancelled")
   524  				},
   525  			}
   526  			req, err := http.NewRequest("GET", "https://quic-go.net/foobar.html", nil)
   527  			Expect(err).ToNot(HaveOccurred())
   528  
   529  			errChan := make(chan error, 1)
   530  			go func() {
   531  				defer GinkgoRecover()
   532  				_, err := rt.RoundTrip(req)
   533  				errChan <- err
   534  			}()
   535  
   536  			Consistently(errChan, scaleDuration(30*time.Millisecond)).ShouldNot(Receive())
   537  			Expect(rt.Close()).To(Succeed())
   538  			var rtErr error
   539  			Eventually(errChan).Should(Receive(&rtErr))
   540  			Expect(rtErr).To(MatchError("cancelled"))
   541  		})
   542  
   543  		It("closes idle connections", func() {
   544  			conn1 := mockquic.NewMockEarlyConnection(mockCtrl)
   545  			conn2 := mockquic.NewMockEarlyConnection(mockCtrl)
   546  			rt := &RoundTripper{
   547  				Dial: func(_ context.Context, hostname string, _ *tls.Config, _ *quic.Config) (quic.EarlyConnection, error) {
   548  					switch hostname {
   549  					case "site1.com:443":
   550  						return conn1, nil
   551  					case "site2.com:443":
   552  						return conn2, nil
   553  					default:
   554  						Fail("unexpected hostname")
   555  						return nil, errors.New("unexpected hostname")
   556  					}
   557  				},
   558  			}
   559  			req1, err := http.NewRequest("GET", "https://site1.com", nil)
   560  			Expect(err).ToNot(HaveOccurred())
   561  			req2, err := http.NewRequest("GET", "https://site2.com", nil)
   562  			Expect(err).ToNot(HaveOccurred())
   563  			Expect(req1.Host).ToNot(Equal(req2.Host))
   564  			ctx1, cancel1 := context.WithCancel(context.Background())
   565  			ctx2, cancel2 := context.WithCancel(context.Background())
   566  			req1 = req1.WithContext(ctx1)
   567  			req2 = req2.WithContext(ctx2)
   568  			roundTripCalled := make(chan struct{})
   569  			reqFinished := make(chan struct{})
   570  			rt.newClient = func(quic.EarlyConnection) singleRoundTripper {
   571  				cl := NewMockSingleRoundTripper(mockCtrl)
   572  				cl.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(func(r *http.Request) (*http.Response, error) {
   573  					roundTripCalled <- struct{}{}
   574  					<-r.Context().Done()
   575  					return nil, nil
   576  				})
   577  				return cl
   578  			}
   579  			go func() {
   580  				rt.RoundTrip(req1)
   581  				reqFinished <- struct{}{}
   582  			}()
   583  			go func() {
   584  				rt.RoundTrip(req2)
   585  				reqFinished <- struct{}{}
   586  			}()
   587  			<-roundTripCalled
   588  			<-roundTripCalled
   589  			// Both two requests are started.
   590  			cancel1()
   591  			<-reqFinished
   592  			// req1 is finished
   593  			conn1.EXPECT().CloseWithError(gomock.Any(), gomock.Any())
   594  			rt.CloseIdleConnections()
   595  			cancel2()
   596  			<-reqFinished
   597  			// all requests are finished
   598  			conn2.EXPECT().CloseWithError(gomock.Any(), gomock.Any())
   599  			rt.CloseIdleConnections()
   600  		})
   601  	})
   602  })