// Copyright 2026 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.27 && !http2legacy

// Transport wrapping a net/http.Transport.

package http2

import (
	
	
	
	
	
	
	
	
	
	
)

func ( *http.Transport) error {
	,  := configureTransports()
	return 
}

func ( *http.Transport) (*Transport, error) {
	// ConfigureTransport returns an http2.Transport with a configuration
	// linked to the http.Transport's.
	 := &Transport{}
	.configure()
	// Enable HTTP/2 on the transport, as the pre-wrapping implementation did:
	// net/http does not auto-enable it for a transport with a custom
	// TLSClientConfig or dialer.
	if .TLSClientConfig == nil {
		.TLSClientConfig = &tls.Config{}
	}
	if .Protocols == nil {
		.Protocols = new(http.Protocols)
		.Protocols.SetHTTP1(true)
	}
	.Protocols.SetHTTP2(true)
	return , nil
}

// transportConfig is passed to net/http.Transport.RegisterProtocol("http/2", config).
// It provides the net/http.Transport with access to the configuration in the
// x/net/http2.Transport.
type transportConfig struct {
	t *Transport
}

// Registered is called by net/http.Transport.RegisterProtocol,
// to let us know that it understands the registration mechanism we're using.
func ( transportConfig) ( *http.Transport) {
	.t.lazyt1 = 
}

func ( transportConfig) () bool {
	return .t.DisableCompression
}

func ( transportConfig) () int64 {
	return int64(.t.MaxHeaderListSize)
}

func ( transportConfig) () time.Duration {
	return .t.IdleConnTimeout
}

func ( transportConfig) () http.HTTP2Config {
	return http.HTTP2Config{
		StrictMaxConcurrentRequests: .t.StrictMaxConcurrentStreams,
		MaxDecoderHeaderTableSize:   int(.t.MaxDecoderHeaderTableSize),
		MaxEncoderHeaderTableSize:   int(.t.MaxEncoderHeaderTableSize),
		MaxReadFrameSize:            int(.t.MaxReadFrameSize),
		SendPingTimeout:             .t.ReadIdleTimeout,
		PingTimeout:                 .t.PingTimeout,
		WriteByteTimeout:            .t.WriteByteTimeout,
		CountError:                  .t.CountError,
	}
}

// ExternalRoundTrip reports whether the Transport wants to take control of the RoundTrip call.
// If the user hasn't configured a custom connection pool, we leave the RoundTrip up to net/http.
func ( transportConfig) () bool {
	return .t.ConnPool != nil
}

// RoundTrip is used when the http.Transport is passing control of the full
// RoundTrip to us--connection pooling, retries, etc.
//
// This is only used when the http2.Transport has a user-provided ConnPool.
// Any other time, net/http handles everything.
func ( transportConfig) ( *http.Request) (*http.Response, error) {
	if .t.ConnPool == nil {
		return nil, http.ErrSkipAltProtocol
	}
	return .t.RoundTrip()
}

// netConnContextKey passes a net.Conn to http.Transport.NewClientConn.
// See http2.Transport.NewClientConn.
type netConnContextKey struct{}

// ConnFromContext lets the http.Transport fetch a net.Conn out of a context
// passed to NewClientConn. See http2.Transport.NewClientConn.
func ( transportConfig) ( context.Context) net.Conn {
	,  := .Value(netConnContextKey{}).(net.Conn)
	return 
}

// http2TransportContextKey marks a RoundTrip as needing its dial handled by the http2.Transport.
// We set this for http2.RoundTrip calls, where the historical behavior is to use the
// http2.Transport's dialer.
type http2TransportContextKey struct{}

// DialFromContext dials a new connection using the http2.Transport's DialTLS/DialTLSContext.
func ( transportConfig) ( context.Context, ,  string) (net.Conn, error) {
	,  := .Value(http2TransportContextKey{}).(*transportRoundTripState)
	if  == nil {
		// We're being called from a RoundTrip that did not start with an http2.Transport.
		// Use the http.Transport's dialer.
		return nil, errors.ErrUnsupported
	}
	return .t.dialForRoundTrip(, , , )
}

type transportInternal struct {
	initOnce sync.Once
	lazyt1   *http.Transport

	dialMu sync.Mutex
	dials  map[string]*transportDialState
}

type transportDialState struct {
	dialc   chan struct{} // close when dial returns
	dialErr error         // dial result, set before dialc is closed
	rtdonec chan struct{} // closed when RoundTrip initiating the dial returns
}

func ( *Transport) () *http.Transport {
	.initOnce.Do(func() {
		if .lazyt1 != nil {
			return
		}
		 := &http.Transport{}
		.configure()
	})
	return .lazyt1
}

func ( *Transport) ( *http.Transport) {
	.RegisterProtocol("http/2", transportConfig{})
	// tr2.lazyt1 is set by transportConfig.Registered.
	if .lazyt1 !=  {
		panic("http2: net/http does not support this version of x/net/http2")
	}
}

// transportRoundTripState is the state of the dial for an http2.Transport.RoundTrip.
type transportRoundTripState struct {
	mu       sync.Mutex
	rtdone   bool          // set when RoundTrip returns
	rtdonec  chan struct{} // closed when RoundTrip returns
	gotconnc chan struct{} // closed when GotConn hook is called
}

// startDial is called when a dial starts.
//
// It returns done=true if the RoundTrip has already returned,
// in which case we should skip dialing.
func ( *transportRoundTripState) () ( chan struct{},  bool) {
	.mu.Lock()
	defer .mu.Unlock()
	if .rtdone {
		return nil, true
	}
	if .rtdonec == nil {
		.rtdonec = make(chan struct{})
	}
	.gotconnc = make(chan struct{})
	return .gotconnc, false
}

// gotConn is called when RoundTrip gets a connection.
// This may happen multiple times per RoundTrip, if a request is retried.
func ( *transportRoundTripState) () {
	.mu.Lock()
	defer .mu.Unlock()
	if .gotconnc != nil {
		close(.gotconnc)
		.gotconnc = nil
	}
}

// roundTripDone is called when RoundTrip returns.
func ( *transportRoundTripState) () {
	.mu.Lock()
	defer .mu.Unlock()
	.rtdone = true
	if .rtdonec != nil {
		close(.rtdonec)
	}
}

func ( *Transport) ( *http.Request,  RoundTripOpt) (*http.Response, error) {
	 := .init()

	if .URL.Scheme == "http" && !.AllowHTTP {
		return nil, errors.New("http2: unencrypted HTTP/2 not enabled")
	}

	// When the Transport has a user-provided connection pool (unusual, deprecated),
	// we need to handle picking a connection, retrys, etc.
	if .ConnPool != nil {
		return .roundTripViaPool(, , .ConnPool)
	}

	// Setting this context key lets net/http know that if it is necessary to dial
	// a new connection, we should handle the net.Dial.
	//
	// Both http.Transport and http2.Transport allow the user to provide a custom
	// dial function, and historically you only get the dial function from the
	// Transport you're calling RoundTrip on.
	//
	// In addition, http2.Transport coalesces dials, which http.Transport historically
	// has not.
	 := &transportRoundTripState{}
	defer .roundTripDone()
	 := context.WithValue(.Context(), http2TransportContextKey{}, )
	 = httptrace.WithClientTrace(, &httptrace.ClientTrace{
		GotConn: func(httptrace.GotConnInfo) {
			// Tell dialForRoundTrip that we have received a connection.
			.gotConn()
		},
	})
	 = .WithContext()
	return .RoundTrip()
}

var errCoalescedDialAbandoned = errors.New("http2: abandoned coalesced dial")

const coalescedDialRetryTimeout = 50 * time.Millisecond

// dialForRoundTrip is called (indirectly) by net/http when dialing a new connection
// for a request call which originated as an http2.Transport.RoundTrip call.
//
// It uses the http2.Transport's Dial hooks and coalesces dials.
func ( *Transport) ( context.Context,  *transportRoundTripState, ,  string) (net.Conn, error) {
	,  := .startDial()
	if  {
		// RoundTrip returned, no need for this dial to proceed.
		return nil, errCoalescedDialAbandoned
	}

	var  *transportDialState
	for {
		// The first dial to an address registers itself with t.dials.
		// When the dial finishes, it records the outcome and removes itself from t.dials
		// so future dials will start a new round of coalescing.
		//
		// Subsequent dials observe that a dial is in progress, and skip dialing.
		.dialMu.Lock()
		if .dials == nil {
			.dials = make(map[string]*transportDialState)
		}
		 := .dials[]
		if  == nil {
			// No entry in t.dials to coalesce with. Add ourselves as leader.
			 = &transportDialState{
				dialc:   make(chan struct{}),
				rtdonec: .rtdonec,
			}
			.dials[] = 
		}
		.dialMu.Unlock()
		if  == nil {
			// We are the leader, so we should dial for real.
			break
		}

		// Coalesce with a previous dial.
		var (
			          = false
			   = .dialc
			 = .rtdonec
			        <-chan time.Time
		)
		for ! {
			if  == nil &&  == nil &&  == nil {
				// The leader finished dialing, and its RoundTrip finished.
				//
				// There are several possibilities, which reduce to:
				//   - The RoundTrip we are dialing for is about to receive
				//     a connection (possibly the one the leader just dialed),
				//     but we haven't observed it yet.
				//   - There's something wrong with the leader's connection,
				//     such as a TLS handshake failure.
				//
				// We have no good way to distinguish between these cases.
				//
				// Wait a short time for the leader's connection (or some
				// other usable connection) to be delivered to our RoundTrip.
				// If it is not, start a dial of our own.
				 = time.After(coalescedDialRetryTimeout)
			}
			select {
			case <-:
				// The leader's dial completed.
				if .dialErr != nil {
					return nil, .dialErr
				}
				 = nil
			case <-:
				// The leader's RoundTrip completed.
				 = nil
			case <-:
				// The leader's dial and RoundTrip completed, and some time
				// has passed. Assume we're not getting a connection and
				// restart the dial process.
				 = true
			case <-.rtdonec:
				// Our RoundTrip returned.
				return nil, errCoalescedDialAbandoned
			case <-:
				// Our RoundTrip got a connection.
				return nil, errCoalescedDialAbandoned
			case <-.Done():
				// net/http abandoned the dial.
				return nil, .Err()
			}
		}
	}

	// Dial for real.
	 := .TLSClientConfig
	if  == nil {
		 = &tls.Config{}
	} else {
		 = .Clone()
	}
	if !slices.Contains(.NextProtos, "h2") {
		.NextProtos = append([]string{"h2"}, .NextProtos...)
	}
	if .ServerName == "" {
		, ,  := net.SplitHostPort()
		if  == nil {
			.ServerName = 
		}
	}
	,  := .dialTLS(, , , )

	// Remove this dial from t.dials, so future dial attempts will not coalesce with it.
	.dialMu.Lock()
	delete(.dials, )
	.dialMu.Unlock()

	// Notify any dial coalesced with this one that we are done.
	.dialErr = 
	close(.dialc)

	return , 
}

func ( *Transport) () {
	 := .init()
	.CloseIdleConnections()
}

func ( *Transport) ( net.Conn) (*ClientConn, error) {
	 := .init()
	// http.Transport's NewClientConn doesn't provide a supported way to create
	// a connection from a net.Conn. (This might be useful to add in the future?)
	// We're going to craftily sneak one in via the context key, with the
	// scheme of "http/2" telling NewClientConn to look for it.
	 := context.WithValue(context.Background(), netConnContextKey{}, )

	,  := .NewClientConn(, "http/2", "")
	if  != nil {
		return nil, 
	}
	 := &ClientConn{cc: , tr: , tconn: }
	.SetStateHook(.stateHook)
	return , nil
}

// ClientConn is the state of a single HTTP/2 client connection to an
// HTTP/2 server.
//
// Deprecated: Use [http.ClientConn] instead.
type ClientConn struct {
	cc         *http.ClientConn
	tconn      net.Conn
	tr         *Transport
	doNotReuse bool

	mu            sync.Mutex
	closing       bool
	closed        bool
	roundTrips    int
	reserved      int
	starting      int
	pending       int
	maxConcurrent int
	lastIdle      time.Time
	shutdownc     chan struct{}

	atomicReused uint32 // whether conn is being reused; atomic
}

func ( *ClientConn) ( *http.Request) (*http.Response, error) {
	,  := func() (bool, error) {
		.mu.Lock()
		defer .mu.Unlock()
		if .doNotReuse {
			return false, errClientConnUnusable
		}

		// ClientConn.Shutdown will not shut down the conn while
		// cc.starting > 0 or cc.cc.InFlight() > 0.
		//
		// The starting state covers the gap between us deciding to
		// start sending the request, and actually sending it.
		.starting++

		.roundTrips++
		if .reserved == 0 {
			// We do not have a concurrency slot reserved for this request.
			return false, nil
		}
		.reserved--
		return true, nil
	}()
	if  != nil {
		return nil, 
	}
	// If we have no reservation, try to acquire one.
	// (This must be done without cc.mu held, since Reserve may call back to the state hook.)
	if ! && .cc.Reserve() != nil {
		// We could not acquire a concurrency slot, so bump the pending count
		// (requests waiting for a slot).
		.mu.Lock()
		.pending++
		.mu.Unlock()
	}
	,  := .cc.RoundTrip()
	.mu.Lock()
	.starting--
	if .pending > 0 {
		// A request completing frees up a concurrency slot for
		// a pending request to start.
		.pending--
	}
	.updateStateLocked()
	.mu.Unlock()
	return , 
}

func ( *ClientConn) () bool {
	return .cc.Available() > 0 && !.doNotReuse
}

func ( *ClientConn) () error {
	return .cc.Close()
}

func ( *ClientConn) ( context.Context) error {
	// Ask net/http to ping its connection by sending a request with a method of ":ping".
	,  := .cc.RoundTrip((&http.Request{
		Method: ":ping",
	}).WithContext())
	return 
}

func ( *ClientConn) () bool {
	if  := .cc.Reserve();  != nil {
		return false
	}
	 := true
	.mu.Lock()
	if .doNotReuse {
		 = false
	} else {
		.reserved++
	}
	.mu.Unlock()
	if ! {
		.cc.Release()
	}
	return 
}

func ( *ClientConn) () {
	.mu.Lock()
	defer .mu.Unlock()
	.doNotReuse = true
	.closing = true
}

func ( *ClientConn) ( context.Context) error {
	.mu.Lock()
	 := .cc.InFlight() + .starting
	if  > 0 && .shutdownc == nil {
		.shutdownc = make(chan struct{})
	}
	 := .shutdownc
	.mu.Unlock()
	if  != nil {
		// Wait for in-flight requests to finish.
		select {
		case <-:
		case <-.Done():
			return .Err()
		}
	}
	.cc.Close()
	return nil
}

func ( *ClientConn) () ClientConnState {
	.mu.Lock()
	defer .mu.Unlock()
	.updateStateLocked()
	return ClientConnState{
		Closed:               .closed,
		Closing:              .closing,
		StreamsActive:        .cc.InFlight() - .reserved,
		StreamsReserved:      .reserved,
		StreamsPending:       .pending,
		MaxConcurrentStreams: uint32(min(int64(.maxConcurrent), math.MaxUint32)),
		LastIdle:             .lastIdle,
	}
}

// stateHook is the http.ClientConn's state hook.
func ( *ClientConn) (*http.ClientConn) {
	.mu.Lock()
	defer .mu.Unlock()
	.updateStateLocked()
}

func ( *ClientConn) () {
	if .cc.Err() != nil && !.closed {
		.closing = true
		.closed = true
		if .tr.ConnPool != nil {
			// Do the ConnPool update in another goroutine,
			// to avoid holding the conn mutex while it runs.
			go .tr.ConnPool.MarkDead()
		}
	}
	if .cc.InFlight() == 0 && .roundTrips > 0 && .starting == 0 {
		.lastIdle = time.Now()
	}
	if !.closed {
		// This is slightly racy (a request could start or finish in between
		// the Available and InFlight calls), but the best we can do given that
		// the net/http ClientConn API doesn't expose the conn's max concurrency.
		.maxConcurrent = .cc.Available() + .cc.InFlight()
	}
	if .shutdownc != nil && .cc.InFlight()+.starting == 0 {
		close(.shutdownc)
		.shutdownc = nil
	}
}

func ( *ClientConn) () {}

// traceGotConn is (when http2legacy is not enabled) only used for tracing
// connections acquired while using a user-provided ClientConnPool.
func ( *http.Request,  *ClientConn,  bool) {
	 := httptrace.ContextClientTrace(.Context())
	if  == nil || .GotConn == nil {
		return
	}
	 := httptrace.GotConnInfo{Conn: .tconn}
	.Reused = 
	.GotConn()
}