diff options
| author | Nicholas S. Husin <nsh@golang.org> | 2026-03-30 19:17:03 -0400 |
|---|---|---|
| committer | Nicholas Husin <nsh@golang.org> | 2026-04-10 08:24:28 -0700 |
| commit | 2f3c778b232dd53c41e1b623d25cd9f4ab28aaa5 (patch) | |
| tree | 3959d70ffde2c7c385b0cc5c62cb3ac56dbe1516 /src/net/http | |
| parent | ce4459cf0ee339b3bcf0ed10427079a234aade36 (diff) | |
| download | go-2f3c778b232dd53c41e1b623d25cd9f4ab28aaa5.tar.xz | |
net/http: add support for running HTTP tests against HTTP/3
Add support within clientserver_test.go to bring up a test HTTP/3 server
and client when http3Mode testMode option is passed.
To be able to reuse net/http/httptest, net/http/httptest.Server.StartTLS
(and Start) have been modified so they can be called with a nil
Listener. In such cases, both methods will behave identically as usual,
but will not actually make its server serve or set its transport dialer,
both of which requires having a listener. This should be a no-op for
regular users of the package, whose entrypoint via functions such as
NewServer will automatically set a local listener.
Actually enabling HTTP/3 for our tests will be done in a separate CL.
For #70914
Change-Id: Ibc5fc83287b6a04b46e668a54924761a92b620a4
Reviewed-on: https://go-review.googlesource.com/c/go/+/740122
Reviewed-by: Damien Neil <dneil@google.com>
Reviewed-by: Nicholas Husin <husin@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Diffstat (limited to 'src/net/http')
| -rw-r--r-- | src/net/http/clientserver_test.go | 44 | ||||
| -rw-r--r-- | src/net/http/export_test.go | 1 | ||||
| -rw-r--r-- | src/net/http/http.go | 3 | ||||
| -rw-r--r-- | src/net/http/httptest/server.go | 63 |
4 files changed, 82 insertions, 29 deletions
diff --git a/src/net/http/clientserver_test.go b/src/net/http/clientserver_test.go index c25db82fe4..aadd8e3dc0 100644 --- a/src/net/http/clientserver_test.go +++ b/src/net/http/clientserver_test.go @@ -19,6 +19,7 @@ import ( "log" "maps" "net" + "net/http" . "net/http" "net/http/httptest" "net/http/httptrace" @@ -35,8 +36,17 @@ import ( "testing" "testing/synctest" "time" + _ "unsafe" // for linkname + + _ "golang.org/x/net/http3" ) +//go:linkname registerHTTP3Transport +func registerHTTP3Transport(*http.Transport) + +//go:linkname registerHTTP3Server +func registerHTTP3Server(*http.Server) <-chan string + type testMode string const ( @@ -44,13 +54,14 @@ const ( https1Mode = testMode("https1") // HTTPS/1.1 http2Mode = testMode("h2") // HTTP/2 http2UnencryptedMode = testMode("h2unencrypted") // HTTP/2 + http3Mode = testMode("h3") // HTTP/3 ) func (m testMode) Scheme() string { switch m { case http1Mode, http2UnencryptedMode: return "http" - case https1Mode, http2Mode: + case https1Mode, http2Mode, http3Mode: return "https" } panic("unknown testMode") @@ -189,7 +200,8 @@ func newClientServerTest(t testing.TB, mode testMode, h Handler, opts ...any) *c var transportFuncs []func(*Transport) - if idx := slices.Index(opts, any(optFakeNet)); idx >= 0 { + switch idx := slices.Index(opts, any(optFakeNet)); { + case idx >= 0: opts = slices.Delete(opts, idx, idx+1) cst.li = fakeNetListen() cst.ts = &httptest.Server{ @@ -201,7 +213,12 @@ func newClientServerTest(t testing.TB, mode testMode, h Handler, opts ...any) *c return cst.li.connect(), nil } }) - } else { + case mode == http3Mode: + // TODO: support testing HTTP/3 using fakenet. + cst.ts = &httptest.Server{ + Config: &Server{Handler: h}, + } + default: cst.ts = httptest.NewUnstartedServer(h) } @@ -241,6 +258,24 @@ func newClientServerTest(t testing.TB, mode testMode, h Handler, opts ...any) *c cst.ts.EnableHTTP2 = true cst.ts.TLS = cst.ts.Config.TLSConfig cst.ts.StartTLS() + case http3Mode: + http.ProtocolSetHTTP3(p) + cst.ts.TLS = cst.ts.Config.TLSConfig + cst.ts.StartTLS() + listenAddrCh := registerHTTP3Server(cst.ts.Config) + + cst.ts.Config.TLSConfig = cst.ts.TLS + cst.ts.Config.Addr = "localhost:0" + go cst.ts.Config.ListenAndServeTLS("", "") + + listenAddr := <-listenAddrCh + cst.ts.URL = "https://" + listenAddr + t.Cleanup(func() { + // Same timeout as in HTTP/2 goAwayTimeout when shutting down in tests. + ctx, cancel := context.WithTimeout(t.Context(), 25*time.Millisecond) + defer cancel() + cst.ts.Config.Shutdown(ctx) + }) default: t.Fatalf("unknown test mode %v", mode) } @@ -252,6 +287,9 @@ func newClientServerTest(t testing.TB, mode testMode, h Handler, opts ...any) *c if cst.tr.Protocols == nil { cst.tr.Protocols = p } + if mode == http3Mode { + registerHTTP3Transport(cst.tr) + } t.Cleanup(func() { cst.close() diff --git a/src/net/http/export_test.go b/src/net/http/export_test.go index b499769c4f..300785d20d 100644 --- a/src/net/http/export_test.go +++ b/src/net/http/export_test.go @@ -33,6 +33,7 @@ var ( Export_writeStatusLine = writeStatusLine Export_is408Message = is408Message MaxPostCloseReadTime = maxPostCloseReadTime + ProtocolSetHTTP3 = protocolSetHTTP3 ) var MaxWriteWaitBeforeConnReuse = &maxWriteWaitBeforeConnReuse diff --git a/src/net/http/http.go b/src/net/http/http.go index 407e15a1c4..c46b656581 100644 --- a/src/net/http/http.go +++ b/src/net/http/http.go @@ -88,6 +88,9 @@ func (p Protocols) String() string { if p.UnencryptedHTTP2() { s = append(s, "UnencryptedHTTP2") } + if p.http3() { + s = append(s, "HTTP3") + } return "{" + strings.Join(s, ",") + "}" } diff --git a/src/net/http/httptest/server.go b/src/net/http/httptest/server.go index 7ae2561b71..fd65e5797a 100644 --- a/src/net/http/httptest/server.go +++ b/src/net/http/httptest/server.go @@ -20,6 +20,7 @@ import ( "strings" "sync" "time" + _ "unsafe" // for linkname ) // A Server is an HTTP server listening on a system-chosen port on the @@ -45,6 +46,9 @@ type Server struct { // certificate is a parsed version of the TLS config certificate, if present. certificate *x509.Certificate + // started indicates whether the server has been started. + started bool + // wg counts the number of outstanding HTTP requests on this server. // Close blocks until all requests are finished. wg sync.WaitGroup @@ -124,30 +128,31 @@ func NewUnstartedServer(handler http.Handler) *Server { // Start starts a server from NewUnstartedServer. func (s *Server) Start() { - if s.URL != "" { + if s.started { panic("Server already started") } + s.started = true + s.wrap() - if s.client == nil { - tr := &http.Transport{} - dialer := net.Dialer{} - // User code may set either of Dial or DialContext, with DialContext taking precedence. - // We set DialContext here to preserve any context values that are passed in, - // but fall back to Dial if the user has set it. - tr.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { - if tr.Dial != nil { - return tr.Dial(network, addr) - } - if addr == "example.com:80" || strings.HasSuffix(addr, ".example.com:80") { - addr = s.Listener.Addr().String() - } - return dialer.DialContext(ctx, network, addr) + tr := &http.Transport{} + s.client = &http.Client{Transport: tr} + if s.Listener == nil { + return + } + dialer := net.Dialer{} + // User code may set either of Dial or DialContext, with DialContext taking precedence. + // We set DialContext here to preserve any context values that are passed in, + // but fall back to Dial if the user has set it. + tr.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { + if tr.Dial != nil { + return tr.Dial(network, addr) } - s.client = &http.Client{Transport: tr} - + if addr == "example.com:80" || strings.HasSuffix(addr, ".example.com:80") { + addr = s.Listener.Addr().String() + } + return dialer.DialContext(ctx, network, addr) } s.URL = "http://" + s.Listener.Addr().String() - s.wrap() s.goServe() if serveFlag != "" { fmt.Fprintln(os.Stderr, "httptest: serving on", s.URL) @@ -157,12 +162,13 @@ func (s *Server) Start() { // StartTLS starts TLS on a server from NewUnstartedServer. func (s *Server) StartTLS() { - if s.URL != "" { + if s.started { panic("Server already started") } - if s.client == nil { - s.client = &http.Client{} - } + s.started = true + s.wrap() + + s.client = &http.Client{} cert, err := tls.X509KeyPair(testcert.LocalhostCert, testcert.LocalhostKey) if err != nil { panic(fmt.Sprintf("httptest: NewTLSServer: %v", err)) @@ -190,12 +196,18 @@ func (s *Server) StartTLS() { } certpool := x509.NewCertPool() certpool.AddCert(s.certificate) + tr := &http.Transport{ TLSClientConfig: &tls.Config{ RootCAs: certpool, }, ForceAttemptHTTP2: s.EnableHTTP2, } + s.client.Transport = tr + + if s.Listener == nil { + return + } dialer := net.Dialer{} tr.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { if tr.Dial != nil { @@ -206,10 +218,8 @@ func (s *Server) StartTLS() { } return dialer.DialContext(ctx, network, addr) } - s.client.Transport = tr s.Listener = tls.NewListener(s.Listener, s.TLS) s.URL = "https://" + s.Listener.Addr().String() - s.wrap() s.goServe() } @@ -231,7 +241,9 @@ func (s *Server) Close() { s.mu.Lock() if !s.closed { s.closed = true - s.Listener.Close() + if s.Listener != nil { + s.Listener.Close() + } s.Config.SetKeepAlivesEnabled(false) for c, st := range s.conns { // Force-close any idle connections (those between @@ -275,7 +287,6 @@ func (s *Server) Close() { t.CloseIdleConnections() } } - s.wg.Wait() } |
