// Copyright 2025 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. //go:build go1.24 package http3 import ( "context" "fmt" "sync" "golang.org/x/net/quic" ) // A Transport is an HTTP/3 transport. // // It does not manage a pool of connections, // and therefore does not implement net/http.RoundTripper. // // TODO: Provide a way to register an HTTP/3 transport with a net/http.Transport's // connection pool. type Transport struct { // Endpoint is the QUIC endpoint used by connections created by the transport. // If unset, it is initialized by the first call to Dial. Endpoint *quic.Endpoint // Config is the QUIC configuration used for client connections. // The Config may be nil. // // Dial may clone and modify the Config. // The Config must not be modified after calling Dial. Config *quic.Config initOnce sync.Once initErr error } func (tr *Transport) init() error { tr.initOnce.Do(func() { tr.Config = initConfig(tr.Config) if tr.Endpoint == nil { tr.Endpoint, tr.initErr = quic.Listen("udp", ":0", nil) } }) return tr.initErr } // Dial creates a new HTTP/3 client connection. func (tr *Transport) Dial(ctx context.Context, target string) (*ClientConn, error) { if err := tr.init(); err != nil { return nil, err } qconn, err := tr.Endpoint.Dial(ctx, "udp", target, tr.Config) if err != nil { return nil, err } return newClientConn(ctx, qconn) } // A ClientConn is a client HTTP/3 connection. // // Multiple goroutines may invoke methods on a ClientConn simultaneously. type ClientConn struct { qconn *quic.Conn genericConn enc qpackEncoder dec qpackDecoder } func newClientConn(ctx context.Context, qconn *quic.Conn) (*ClientConn, error) { cc := &ClientConn{ qconn: qconn, } cc.enc.init() // Create control stream and send SETTINGS frame. controlStream, err := newConnStream(ctx, cc.qconn, streamTypeControl) if err != nil { return nil, fmt.Errorf("http3: cannot create control stream: %v", err) } controlStream.writeSettings() controlStream.Flush() go cc.acceptStreams(qconn, cc) return cc, nil } // Close closes the connection. // Any in-flight requests are canceled. // Close does not wait for the peer to acknowledge the connection closing. func (cc *ClientConn) Close() error { // Close the QUIC connection immediately with a status of NO_ERROR. cc.qconn.Abort(nil) // Return any existing error from the peer, but don't wait for it. ctx, cancel := context.WithCancel(context.Background()) cancel() return cc.qconn.Wait(ctx) } func (cc *ClientConn) handleControlStream(st *stream) error { // "A SETTINGS frame MUST be sent as the first frame of each control stream [...]" // https://www.rfc-editor.org/rfc/rfc9114.html#section-7.2.4-2 if err := st.readSettings(func(settingsType, settingsValue int64) error { switch settingsType { case settingsMaxFieldSectionSize: _ = settingsValue // TODO case settingsQPACKMaxTableCapacity: _ = settingsValue // TODO case settingsQPACKBlockedStreams: _ = settingsValue // TODO default: // Unknown settings types are ignored. } return nil }); err != nil { return err } for { ftype, err := st.readFrameHeader() if err != nil { return err } switch ftype { case frameTypeCancelPush: // "If a CANCEL_PUSH frame is received that references a push ID // greater than currently allowed on the connection, // this MUST be treated as a connection error of type H3_ID_ERROR." // https://www.rfc-editor.org/rfc/rfc9114.html#section-7.2.3-7 return &connectionError{ code: errH3IDError, message: "CANCEL_PUSH received when no MAX_PUSH_ID has been sent", } case frameTypeGoaway: // TODO: Wait for requests to complete before closing connection. return errH3NoError default: // Unknown frames are ignored. if err := st.discardUnknownFrame(ftype); err != nil { return err } } } } func (cc *ClientConn) handleEncoderStream(*stream) error { // TODO return nil } func (cc *ClientConn) handleDecoderStream(*stream) error { // TODO return nil } func (cc *ClientConn) handlePushStream(*stream) error { // "A client MUST treat receipt of a push stream as a connection error // of type H3_ID_ERROR when no MAX_PUSH_ID frame has been sent [...]" // https://www.rfc-editor.org/rfc/rfc9114.html#section-4.6-3 return &connectionError{ code: errH3IDError, message: "push stream created when no MAX_PUSH_ID has been sent", } } func (cc *ClientConn) handleRequestStream(st *stream) { // "Clients MUST treat receipt of a server-initiated bidirectional // stream as a connection error of type H3_STREAM_CREATION_ERROR [...]" // https://www.rfc-editor.org/rfc/rfc9114.html#section-6.1-3 cc.abort(&connectionError{ code: errH3StreamCreationError, message: "server created bidirectional stream", }) } // abort closes the connection with an error. func (cc *ClientConn) abort(err error) { if e, ok := err.(*connectionError); ok { cc.qconn.Abort(&quic.ApplicationError{ Code: uint64(e.code), Reason: e.message, }) } else { cc.qconn.Abort(err) } }