Help us shape the future of Go! Take the 2026 Go Developer Survey

Source file src/net/http/internal/http3/transport.go

     1  // Copyright 2025 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package http3
     6  
     7  import (
     8  	"context"
     9  	"crypto/tls"
    10  	"errors"
    11  	"fmt"
    12  	"math"
    13  	"net"
    14  	"net/http"
    15  	"net/url"
    16  	"sync"
    17  
    18  	"golang.org/x/net/quic"
    19  )
    20  
    21  // A transport is an HTTP/3 transport.
    22  //
    23  // It does not manage a pool of connections,
    24  // and therefore does not implement net/http.RoundTripper.
    25  //
    26  // TODO: Provide a way to register an HTTP/3 transport with a net/http.transport's
    27  // connection pool.
    28  type transport struct {
    29  	tr1  *http.Transport
    30  	opts TransportOpts
    31  
    32  	mu sync.Mutex // Guards fields below.
    33  	// endpoint is the QUIC endpoint used by connections created by the
    34  	// transport. If CloseIdleConnections is called when activeConns is empty,
    35  	// endpoint will be unset. If unset, endpoint will be initialized by any
    36  	// call to dial.
    37  	endpoint      *quic.Endpoint
    38  	activeConns   map[*clientConn]struct{}
    39  	inFlightDials int
    40  }
    41  
    42  // netHTTPTransport implements the net/http.dialClientConner interface,
    43  // allowing our HTTP/3 transport to integrate with net/http.
    44  type netHTTPTransport struct {
    45  	*transport
    46  }
    47  
    48  // Registered is called to record successful registration with a net/http Transport.
    49  func (t netHTTPTransport) Registered(tr1 *http.Transport) {
    50  	t.transport.tr1 = tr1
    51  }
    52  
    53  // RoundTrip is defined since Transport.RegisterProtocol takes in a
    54  // RoundTripper. However, this method will never be used as net/http's
    55  // dialClientConner interface does not have a RoundTrip method and will only
    56  // use DialClientConn to create a new RoundTripper.
    57  func (t netHTTPTransport) RoundTrip(*http.Request) (*http.Response, error) {
    58  	panic("netHTTPTransport.RoundTrip should never be called")
    59  }
    60  
    61  // DialClientConn creates a new client connection to address.
    62  //
    63  // If proxy is non-nil, the connection should use the provided proxy.
    64  // If HTTP/3 proxies are not supported, DialClientConn should return
    65  // an error wrapping [errors.ErrUnsupported].
    66  //
    67  // The RoundTripper returned by DialClientConn must also implement the
    68  // http.genericClientConn interface.
    69  //
    70  // The client connection should arrange to call internalStateHook
    71  // when the connection closes, when requests complete, and when the
    72  // connection concurrency limit changes.
    73  //
    74  // The client connection must call the internal state hook when
    75  // the connection state changes asynchronously, such as when a request completes.
    76  //
    77  // The internal state hook need not be called after synchronous changes
    78  // to the state: Close, Reserve, Release, and RoundTrip calls
    79  // which don't start a request do not need to call the hook.
    80  func (t netHTTPTransport) DialClientConn(ctx context.Context, addr string, _ *url.URL, tlsConfig *tls.Config, stateHook func()) (http.RoundTripper, error) {
    81  	return t.transport.dial(ctx, addr, tlsConfig, stateHook)
    82  }
    83  
    84  type TransportOpts struct {
    85  	// ListenQUIC determines how the transport will open a QUIC endpoint.
    86  	// By default, quic.Listen("udp", addr, config) is used.
    87  	// ListenQUIC might be called multiple times.
    88  	ListenQUIC func(addr string, config *quic.Config) (*quic.Endpoint, error)
    89  
    90  	// ListenPacket specifies the function for creating a UDP listener.
    91  	// If ListenPacket is nil, then the transport listens using net.ListenPacket.
    92  	//
    93  	// If ListenQUIC and ListenPacket are both set, ListenQUIC takes priority.
    94  	ListenPacket func(network, addr string) (net.PacketConn, error)
    95  
    96  	// QUICConfig is the QUIC configuration used by the transport.
    97  	// QUICConfig may be nil and should not be modified after calling
    98  	// RegisterTransport.
    99  	//
   100  	// The QUICConfig's TLSConfig is not used.
   101  	// Set the TLSConfig on the net/http Transport instead.
   102  	QUICConfig *quic.Config
   103  }
   104  
   105  // RegisterTransport configures a net/http HTTP/1 Transport to use HTTP/3.
   106  func RegisterTransport(tr *http.Transport, opts TransportOpts) error {
   107  	tr3 := &transport{
   108  		opts:        opts,
   109  		activeConns: make(map[*clientConn]struct{}),
   110  	}
   111  	// RegisterProtocol will set tr3.tr1.
   112  	tr.RegisterProtocol("http/3", netHTTPTransport{tr3})
   113  	if tr3.tr1 != tr {
   114  		return errors.New("http3: net/http does not support HTTP/3")
   115  	}
   116  	return nil
   117  }
   118  
   119  func (tr *transport) incInFlightDials() {
   120  	tr.mu.Lock()
   121  	defer tr.mu.Unlock()
   122  	tr.inFlightDials++
   123  }
   124  
   125  func (tr *transport) decInFlightDials() {
   126  	tr.mu.Lock()
   127  	defer tr.mu.Unlock()
   128  	tr.inFlightDials--
   129  }
   130  
   131  func (tr *transport) initEndpoint() (err error) {
   132  	tr.mu.Lock()
   133  	defer tr.mu.Unlock()
   134  	// This might cause rare issues on Darwin. Unlike Linux, Darwin kernel
   135  	// seems to have the following behaviors:
   136  	// - After closing a UDP socket, the port that was bound to the socket
   137  	//   might not be immediately usable again.
   138  	// - When doing IPv6 dual-stack binding (e.g., bind to ":0"), it will
   139  	//   happily bind the IPv6 port, even when the IPv4 port is unavailable.
   140  	//
   141  	// When both of these are combined, in practice, it is possible for the
   142  	// following to happen:
   143  	// 1. Transport binds ":0", creating a dual-stack IPv6 UDP socket.
   144  	//    Everything works as expected.
   145  	// 2. At some point, CloseIdleConnections is called and the socket is
   146  	//    closed.
   147  	// 3. Soon after, a new dial is started, and a new dual-stack IPv6 socket
   148  	//    is coincidentally assigned the same port as the previous socket.
   149  	// 4. If the IPv4 port is still unavailable, Darwin's permissive binding
   150  	//    behavior will cause us to have a socket that silently is unable to
   151  	//    receive packets on its IPv4 address.
   152  	// 5. If the dial target is an IPv4 address, transport will be able to send
   153  	//    packets to the target, but will be unable to receive its reply.
   154  	//
   155  	// TransportOpts.ListenQUIC can technically be configured to avoid
   156  	// dual-stack binding to avoid this issue, and high socket churn is
   157  	// probably uncommon for regular use cases. However, finding a workaround
   158  	// for this eventually would be ideal.
   159  	if tr.endpoint == nil {
   160  		quicConfig := newQUICConfig(tr.opts.QUICConfig, tr.tr1.TLSClientConfig)
   161  		if tr.opts.ListenQUIC != nil {
   162  			tr.endpoint, err = tr.opts.ListenQUIC(":0", quicConfig)
   163  		} else if tr.opts.ListenPacket != nil {
   164  			var conn net.PacketConn
   165  			conn, err = tr.opts.ListenPacket("udp", ":0")
   166  			if err != nil {
   167  				return err
   168  			}
   169  			tr.endpoint, err = quic.NewEndpoint(conn, quicConfig)
   170  			if err != nil {
   171  				conn.Close()
   172  			}
   173  		} else {
   174  			tr.endpoint, err = quic.Listen("udp", ":0", quicConfig)
   175  		}
   176  	}
   177  	return err
   178  }
   179  
   180  // dial creates a new HTTP/3 client connection.
   181  func (tr *transport) dial(ctx context.Context, target string, tlsConfig *tls.Config, stateHook func()) (*clientConn, error) {
   182  	tr.incInFlightDials()
   183  	defer tr.decInFlightDials()
   184  
   185  	if err := tr.initEndpoint(); err != nil {
   186  		return nil, err
   187  	}
   188  	qconn, err := tr.endpoint.Dial(ctx, "udp", target, newQUICConfig(tr.opts.QUICConfig, tlsConfig))
   189  	if err != nil {
   190  		return nil, err
   191  	}
   192  	return tr.newClientConn(ctx, qconn, stateHook)
   193  }
   194  
   195  // CloseIdleConnections is called by net/http.Transport.CloseIdleConnections
   196  // after all existing idle connections are closed using http3.clientConn.Close.
   197  //
   198  // When the transport has no active connections anymore, calling this method
   199  // will make the transport clean up any shared resources that are no longer
   200  // required, such as its QUIC endpoint.
   201  func (tr *transport) CloseIdleConnections() {
   202  	tr.mu.Lock()
   203  	defer tr.mu.Unlock()
   204  	if tr.endpoint == nil || len(tr.activeConns) > 0 || tr.inFlightDials > 0 {
   205  		return
   206  	}
   207  	tr.endpoint.Close(canceledCtx)
   208  	tr.endpoint = nil
   209  }
   210  
   211  // A clientConn is a client HTTP/3 connection.
   212  //
   213  // Multiple goroutines may invoke methods on a clientConn simultaneously.
   214  type clientConn struct {
   215  	tr           *transport
   216  	unregistered chan struct{} // closed when clientConn is unregistered from tr.
   217  
   218  	qconn *quic.Conn
   219  	genericConn
   220  
   221  	enc qpackEncoder
   222  	dec qpackDecoder
   223  
   224  	// Guarded by genericConn.mu
   225  	reserved int
   226  	active   int
   227  	closed   bool
   228  
   229  	stateHook func()
   230  }
   231  
   232  func (tr *transport) registerConn(cc *clientConn) {
   233  	tr.mu.Lock()
   234  	defer tr.mu.Unlock()
   235  	tr.activeConns[cc] = struct{}{}
   236  }
   237  
   238  func (tr *transport) unregisterConn(cc *clientConn) {
   239  	tr.mu.Lock()
   240  	defer tr.mu.Unlock()
   241  	delete(tr.activeConns, cc)
   242  	close(cc.unregistered)
   243  }
   244  
   245  func (tr *transport) maxResponseHeaderBytes() int64 {
   246  	if tr.tr1 != nil && tr.tr1.MaxResponseHeaderBytes > 0 {
   247  		return tr.tr1.MaxResponseHeaderBytes
   248  	}
   249  	return 10 << 20 // Matches HTTP/1 and HTTP/2 default of 10 MB.
   250  }
   251  
   252  func (tr *transport) newClientConn(ctx context.Context, qconn *quic.Conn, stateHook func()) (*clientConn, error) {
   253  	cc := &clientConn{
   254  		tr:           tr,
   255  		unregistered: make(chan struct{}),
   256  		qconn:        qconn,
   257  		stateHook:    stateHook,
   258  	}
   259  	tr.registerConn(cc)
   260  	cc.enc.init()
   261  
   262  	// Create control stream and send SETTINGS frame.
   263  	controlStream, err := newConnStream(ctx, cc.qconn, streamTypeControl)
   264  	if err != nil {
   265  		tr.unregisterConn(cc)
   266  		return nil, fmt.Errorf("http3: cannot create control stream: %v", err)
   267  	}
   268  	controlStream.writeSettings(settingsMaxFieldSectionSize, tr.maxResponseHeaderBytes())
   269  	controlStream.Flush()
   270  
   271  	go func() {
   272  		cc.acceptStreams(qconn, cc)
   273  		cc.mu.Lock()
   274  		cc.closed = true
   275  		cc.mu.Unlock()
   276  		cc.maybeCallStateHook()
   277  		tr.unregisterConn(cc)
   278  	}()
   279  	return cc, nil
   280  }
   281  
   282  func (cc *clientConn) Close() error {
   283  	err := cc.qconn.Close()
   284  	// Wait until cc is actually unregistered from the transport before
   285  	// returning. Otherwise, a race condition might occur: CloseIdleConnections
   286  	// might be called before cc gets a chance to be unregistered; if so,
   287  	// CloseIdleConnections will unexpectedly not close its QUIC endpoint,
   288  	// thinking that there is still an active cc.
   289  	<-cc.unregistered
   290  	return err
   291  }
   292  
   293  func (cc *clientConn) IsAlt() bool {
   294  	return true
   295  }
   296  
   297  func (cc *clientConn) IsReused() bool {
   298  	return false
   299  }
   300  
   301  func (cc *clientConn) NetConn() net.Conn {
   302  	return nil
   303  }
   304  
   305  func (cc *clientConn) CloseWithError(err error) {
   306  	cc.Close()
   307  }
   308  
   309  func (cc *clientConn) Err() error {
   310  	cc.mu.Lock()
   311  	defer cc.mu.Unlock()
   312  	if cc.closed {
   313  		return errors.New("connection closed")
   314  	}
   315  	return nil
   316  }
   317  
   318  func (cc *clientConn) Reserve() error {
   319  	cc.mu.Lock()
   320  	defer cc.mu.Unlock()
   321  	if cc.closed {
   322  		return errors.New("connection closed")
   323  	}
   324  	cc.reserved++
   325  	return nil
   326  }
   327  
   328  func (cc *clientConn) Release() {
   329  	cc.mu.Lock()
   330  	defer cc.mu.Unlock()
   331  	// This is consistent with RoundTrip: both Release and RoundTrip will
   332  	// consume a reservation iff one exists.
   333  	if cc.reserved > 0 {
   334  		cc.reserved--
   335  	}
   336  }
   337  
   338  func (cc *clientConn) Available() int {
   339  	cc.mu.Lock()
   340  	defer cc.mu.Unlock()
   341  	if cc.closed {
   342  		return 0
   343  	}
   344  	// The general recommendation for HTTP/3 is to reuse the same connection
   345  	// for multiple requests rather than creating new connections. As of now,
   346  	// we don't have a good understanding of when one might want to create
   347  	// multiple HTTP/3 connections to the same server.
   348  	// Therefore, for ClientConn API, let HTTP/3 connections have no limit.
   349  	// Starting a new RoundTrip when we are at the connection limit will just
   350  	// block until a new max stream limit is received.
   351  	return math.MaxInt
   352  }
   353  
   354  func (cc *clientConn) InFlight() int {
   355  	cc.mu.Lock()
   356  	defer cc.mu.Unlock()
   357  	if cc.closed {
   358  		return 0
   359  	}
   360  	return cc.reserved + cc.active
   361  }
   362  
   363  func (cc *clientConn) maybeCallStateHook() {
   364  	if cc.stateHook != nil {
   365  		cc.stateHook()
   366  	}
   367  }
   368  
   369  func (cc *clientConn) handleControlStream(st *stream) error {
   370  	// "A SETTINGS frame MUST be sent as the first frame of each control stream [...]"
   371  	// https://www.rfc-editor.org/rfc/rfc9114.html#section-7.2.4-2
   372  	if err := st.readSettings(func(settingsType, settingsValue int64) error {
   373  		switch settingsType {
   374  		case settingsMaxFieldSectionSize:
   375  			_ = settingsValue // TODO
   376  		case settingsQPACKMaxTableCapacity:
   377  			_ = settingsValue // TODO
   378  		case settingsQPACKBlockedStreams:
   379  			_ = settingsValue // TODO
   380  		default:
   381  			// Unknown settings types are ignored.
   382  		}
   383  		return nil
   384  	}); err != nil {
   385  		return err
   386  	}
   387  
   388  	for {
   389  		ftype, err := st.readFrameHeader()
   390  		if err != nil {
   391  			return err
   392  		}
   393  		switch ftype {
   394  		case frameTypeCancelPush:
   395  			// "If a CANCEL_PUSH frame is received that references a push ID
   396  			// greater than currently allowed on the connection,
   397  			// this MUST be treated as a connection error of type H3_ID_ERROR."
   398  			// https://www.rfc-editor.org/rfc/rfc9114.html#section-7.2.3-7
   399  			return &connectionError{
   400  				code:    errH3IDError,
   401  				message: "CANCEL_PUSH received when no MAX_PUSH_ID has been sent",
   402  			}
   403  		case frameTypeGoaway:
   404  			// TODO: Wait for requests to complete before closing connection.
   405  			return errH3NoError
   406  		default:
   407  			// Unknown frames are ignored.
   408  			if err := st.discardUnknownFrame(ftype); err != nil {
   409  				return err
   410  			}
   411  		}
   412  	}
   413  }
   414  
   415  func (cc *clientConn) handleEncoderStream(*stream) error {
   416  	// TODO
   417  	return nil
   418  }
   419  
   420  func (cc *clientConn) handleDecoderStream(*stream) error {
   421  	// TODO
   422  	return nil
   423  }
   424  
   425  func (cc *clientConn) handlePushStream(*stream) error {
   426  	// "A client MUST treat receipt of a push stream as a connection error
   427  	// of type H3_ID_ERROR when no MAX_PUSH_ID frame has been sent [...]"
   428  	// https://www.rfc-editor.org/rfc/rfc9114.html#section-4.6-3
   429  	return &connectionError{
   430  		code:    errH3IDError,
   431  		message: "push stream created when no MAX_PUSH_ID has been sent",
   432  	}
   433  }
   434  
   435  func (cc *clientConn) handleRequestStream(st *stream) error {
   436  	// "Clients MUST treat receipt of a server-initiated bidirectional
   437  	// stream as a connection error of type H3_STREAM_CREATION_ERROR [...]"
   438  	// https://www.rfc-editor.org/rfc/rfc9114.html#section-6.1-3
   439  	return &connectionError{
   440  		code:    errH3StreamCreationError,
   441  		message: "server created bidirectional stream",
   442  	}
   443  }
   444  
   445  // abort closes the connection with an error.
   446  func (cc *clientConn) abort(err error) {
   447  	if e, ok := err.(*connectionError); ok {
   448  		cc.qconn.Abort(&quic.ConnectionCloseError{
   449  			Code:   uint64(e.code),
   450  			Reason: e.message,
   451  		})
   452  	} else {
   453  		cc.qconn.Abort(err)
   454  	}
   455  }
   456  

View as plain text