Skip to content

proposal: net/http: pluggable HTTP/3 #77440

Description

@neild

This is a proposal to add the ability to plug an HTTP/3 implementation into the net/http Transport and Server. This is not a proposal to add HTTP/3 support (that's #32204).

The goal of this proposal is to make it possible for users to use HTTP/3 with net/http, with the HTTP/3 implementation provided by a separate package outside of net/http. For example, we would like to support sending HTTP/3 requests using a net/http.Transport:

tr := &http.Transport{}

// Register an external HTTP/3 implementation.
// "http3" is some package providing that implementation.
http3.RegisterTransport(tr)

// Configure the Transport to use HTTP/3.
tr.Protocols = new(http.Protocols)
tr.Protocols.SetHTTP3(true)

// This request is sent using HTTP/3.
resp, err := tr.RoundTrip(req)

We would also like to support serving HTTP/3 using a net/http.Server:

srv := &http.Server{Addr: "localhost:8000"}

// Register an external HTTP/3 implementation.
// "http3" is some package providing that implementation.
http3.RegisterServer(srv)

// Configure the Server to use HTTP/3.
tr.Protocols = new(http.Protocols)
tr.Protocols.SetHTTP3(true)

// The server serves HTTP/3 on localhost:8000.
srv.ListenAndServeTLS(certFile, keyFile)

There are several related changes to net/http required to make the above work.

Add HTTP/3 to http.Protocols

The http.Protocols type describes a selection from a set of protocols, currently HTTP/1, HTTP/2, and unencrypted HTTP/2. The Transport.Protocols and Server.Protocols configuration fields let a user select which protocols to use.

We will add HTTP/3 to the the set of protocols:

package http

// ...
// * HTTP3 is the HTTP/3 protocol over a QUIC connection.
//   The net/http package does not directly support HTTP/3 at this time,
//   but support may be provided by external packages.
type Protocols struct{} // existing type

// HTTP3 reports whether p includes HTTP/3.
func (p Protocols) HTTP3() bool

// SetHTTP3 adds or removes HTTP/3 from p.
func (p *Protocols) SetHTTP3(ok bool)

This is the only change to net/http's exported API.

Define HTTP/3-enabled client and server behavior

We must define what happens when Transport.Protocols or Server.Protocols includes HTTP/3.

When Transport.Protocols includes HTTP3 and does not include HTTP1 or HTTP2,
the transport will use HTTP/3 for requests for HTTP/3 URLs. Transports will not, at this time, use HTTP/3 when any other protocol is available.

This is the minimal viable choice for Transport: There are many possible ways to choose between HTTP/1, HTTP/2, and HTTP/3. We might, for example, use HTTP/3 when an HTTPS DNS record is present. We might upgrade from HTTP/1 or /2 after receiving an Alt-Svc HTTP response header or HTTP/2 ALTSVC frame. We might try initiating both TLS and QUIC connections at the same time and pick the first to return. All of these are reasonable choices for the future. For the purposes of this proposal, however, we will begin with the simplest option of using HTTP/3 only when the user has unambiguously indicated that this is the correct protocol to use.

When Server.Protocols includes HTTP3, Server.ListenAndServeTLS will listen for HTTP/3 connections.

HTTP/3 client registration

The net/http.Transport needs a way to create new HTTP/3 connections.

Our HTTP/2 implementation currently registers a way to create new HTTP/2 connections using the Transport.RegisterProtocol function. This is a long-standing abuse of RegisterProtocol: This function is intended for registering implementations of protocol schemes such as "ftp" or "file", but we also use it to connect the HTTP/2 implementation to net/http. We will extend the existing mechanism for creating HTTP/2 connections to handle HTTP/3 as well.

// RegisterProtocol is an existing method of Transport.
func (t *Transport) RegisterProtocol(scheme string, rt RoundTripper)

To register an HTTP/3 client implementation, call Transport.RegisterProtocol with a scheme of "http/3" and a RoundTripper which implements the following interface:

type dialClientConner interface {
	// DialClientConn creates a new client connection to address.
	//
	// If proxy is non-nil, the connection should use the provided proxy.
	// If HTTP/3 proxies are not supported, DialClientConn should return
	// an error wrapping [errors.ErrUnsupported].
	//
	// The RoundTripper returned by DialClientConn must also implement the
	// following methods to support [ClientConn] methods of the same name:
	//	Close() error
	//	Err() error
	// 	Reserve() error
	//	Release() error
	//	Available() int
	//	InFlight() int
	//
	// The client connection should arrange to call internalStateHook
	// when the connection closes, when requests complete, and when the
	// connection concurrency limit changes.
	//
	// The client connection must call the internal state hook when
	// the connection state changes asynchronously, such as when a request completes.
	//
	// The internal state hook need not be called after synchronous changes
	// to the state: Close, Reserve, Release, and RoundTrip calls
	// which don't start a request do not need to call the hook.
	DialClientConn(ctx context.Context, address string, proxy *url.URL, internalStateHook func()) (RoundTripper, error)
}

The RoundTripper may also implement the following interface:

type closeIdleConnectionser interface {
	// CloseIdleConnections is called by Transport.CloseIdleConnections.
	//
	// The transport will close idle connections created with DialClientConn
	// before calling this method. The HTTP/3 transport should not attempt to
	// close idle connections, but may clean up shared resources such as
	// UDP sockets if no connections remain.
	CloseIdleConnections()
}

Note that while Transport.RegisterProtocol registers a RoundTripper, the RoundTrip method is never called. Note also that we do not propose adding exported definitions of the above interfaces to net/http: We expect that only a low single-digit number of packages will ever use this registration API, and it is not worth adding user-visible complexity to net/http for this case.

The above is an extension of the existing mechanism used to create HTTP/2 connections. It differs in that net/http takes responsibility for creating the *tls.Conn underlying an HTTP/2 connection, but the HTTP/3 implementation is responsible for creating the QUIC connection underlying an HTTP/3 connection.

HTTP/3 server registration

The net/http.Server needs a way to listen for HTTP/3 connections.

To register an HTTP/3 server implementation, add an "http/3" entry to the existing Server.TLSNextProto configuration field.

type Server {
	// TLSNextProto is an existing field of Server.
	TLSNextProto map[string]func(*Server, *tls.Conn, Handler)
}

Server.ListenAndServeTLS will call this function with the server, a nil *tls.Conn, and a Handler that implements the following additional methods:

type http3ServerHandler interface {
	// Addr is the address to listen on.
	Addr() string

	// TLSConfig is the *tls.Config to use.
	TLSConfig() *tls.Config

	// BaseContext is the base context to use for server requests.
	BaseContext() context.Context

	// ListenErrHook is called by an HTTP/3 server implementation to
	// propagate any error it encounters when trying to listen, if any, to
	// net/http.
	ListenErrHook(error)
}

The additional methods on the Handler are similar to an existing mechanism used to pass information to the HTTP/2 server.

Note that while existing TLSNextProto functions are called once per connection (net/http accepts a TLS connection and hands it off to the HTTP/2 implementation), TLSNextProto["http/3"] is called once at listen time. This is a somewhat confusing abuse of TLSNextProto, but has the advantage of avoiding the need to add additional user-visible API surface to net/http.

Anticipated Questions

Q: Implementing HTTP/2 as a package external to net/http led to a messy dependency situation which is still being cleaned up. Doesn't this do that all over again?

A: The messy dependency situation for HTTP/2 arises from us supporting HTTP/2 by default in net/http. We do not anticipate supporting HTTP/3 in net/http in the same way in the near future. HTTP/3 is a comparatively heavyweight dependency and comes with a complex set of tradeoffs. We do not want to require all net/http users to bring in that dependency.

So long as HTTP/3 users need to import a separate package such as github.com/quic-go/quic-go or golang.org/x/net/http3, the dependency situation is fine.

Q: Why not just let HTTP/3 be used independently of net/http? Why do we need to plug it into net/http at all?

A: There are several benefits to allowing HTTP/3 implementations to be plugged into net/http:

  • Users can adopt HTTP/3 with relatively minimal change to their code.
  • HTTP/3 libraries can re-use existing logic within net/http, such as our connection pooling implementation.
  • In the future, net/http.Transport should be able to select between HTTP/1, HTTP/2, and HTTP/3 on a case-by-case basis.
  • We can reuse most of our existing net/http tests to test our HTTP/3 implementation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    LibraryProposalIssues describing a requested change to the Go standard library or x/ libraries, but not to a toolProposalProposal-Hold

    Type

    No type

    Projects

    Status
    Hold

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions