aboutsummaryrefslogtreecommitdiff
path: root/src/net/http
diff options
context:
space:
mode:
authorNicholas S. Husin <nsh@golang.org>2026-03-30 19:17:03 -0400
committerNicholas Husin <nsh@golang.org>2026-04-10 08:24:28 -0700
commit2f3c778b232dd53c41e1b623d25cd9f4ab28aaa5 (patch)
tree3959d70ffde2c7c385b0cc5c62cb3ac56dbe1516 /src/net/http
parentce4459cf0ee339b3bcf0ed10427079a234aade36 (diff)
downloadgo-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.go44
-rw-r--r--src/net/http/export_test.go1
-rw-r--r--src/net/http/http.go3
-rw-r--r--src/net/http/httptest/server.go63
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()
}