// 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. package http import ( "context" "errors" "fmt" "net" "net/http/httptrace" "net/url" "sync" ) // A ClientConn is a client connection to an HTTP server. // // Unlike a [Transport], a ClientConn represents a single connection. // Most users should use a Transport rather than creating client connections directly. type ClientConn struct { cc genericClientConn stateHookMu sync.Mutex userStateHook func(*ClientConn) stateHookRunning bool lastAvailable int lastInFlight int lastClosed bool } // newClientConner is the interface implemented by HTTP/2 transports to create new client conns. // // The http package (this package) needs a way to ask the http2 package to // create a client connection. // // Transport.TLSNextProto["h2"] contains a function which appears to do this, // but for historical reasons it does not: The TLSNextProto function adds a // *tls.Conn to the http2.Transport's connection pool and returns a RoundTripper // which is backed by that connection pool. NewClientConn needs a way to get a // single client connection out of the http2 package. // // The http2 package registers a RoundTripper with Transport.RegisterProtocol. // If this RoundTripper implements newClientConner, then Transport.NewClientConn will use // it to create new HTTP/2 client connections. type newClientConner interface { // NewClientConn creates a new client connection from a net.Conn. // // The RoundTripper returned by NewClientConn must implement genericClientConn. // (We don't define NewClientConn as returning genericClientConn, // because either we'd need to make genericClientConn an exported type // or define it as a type alias. Neither is particularly appealing.) // // The state hook passed here is the internal state hook // (ClientConn.maybeRunStateHook). The internal state hook calls // the user state hook (if any), which is set by the user with // ClientConn.SetStateHook. // // The client connection should arrange to call the internal state hook // 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. // // The general idea is that if we call (for example) Close, // we know that the connection state has probably changed and we // don't need the state hook to tell us that. // However, if the connection closes asynchronously // (because, for example, the other end of the conn closed it), // the state hook needs to inform us. NewClientConn(nc net.Conn, internalStateHook func()) (RoundTripper, error) } // genericClientConn is an interface implemented by HTTP/2 client conns // returned from newClientConner.NewClientConn. // // See the newClientConner doc comment for more information. type genericClientConn interface { Close() error Err() error RoundTrip(req *Request) (*Response, error) Reserve() error Release() Available() int InFlight() int } // NewClientConn creates a new client connection to the given address. // // If scheme is "http", the connection is unencrypted. // If scheme is "https", the connection uses TLS. // // The protocol used for the new connection is determined by the scheme, // Transport.Protocols configuration field, and protocols supported by the // server. See Transport.Protocols for more details. // // If Transport.Proxy is set and indicates that a request sent to the given // address should use a proxy, the new connection uses that proxy. // // NewClientConn always creates a new connection, // even if the Transport has an existing cached connection to the given host. // // The new connection is not added to the Transport's connection cache, // and will not be used by [Transport.RoundTrip]. // It does not count against the MaxIdleConns and MaxConnsPerHost limits. // // The caller is responsible for closing the new connection. func (t *Transport) NewClientConn(ctx context.Context, scheme, address string) (*ClientConn, error) { t.nextProtoOnce.Do(t.onceSetNextProtoDefaults) switch scheme { case "http", "https": default: return nil, fmt.Errorf("net/http: invalid scheme %q", scheme) } host, port, err := net.SplitHostPort(address) if err != nil { return nil, err } if port == "" { port = schemePort(scheme) } var proxyURL *url.URL if t.Proxy != nil { // Transport.Proxy takes a *Request, so create a fake one to pass it. req := &Request{ ctx: ctx, Method: "GET", URL: &url.URL{ Scheme: scheme, Host: host, Path: "/", }, Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, Header: make(Header), Body: NoBody, Host: host, } var err error proxyURL, err = t.Proxy(req) if err != nil { return nil, err } } cm := connectMethod{ targetScheme: scheme, targetAddr: net.JoinHostPort(host, port), proxyURL: proxyURL, } // The state hook is a bit tricky: // The persistConn has a state hook which calls ClientConn.maybeRunStateHook, // which in turn calls the user-provided state hook (if any). // // ClientConn.maybeRunStateHook handles debouncing hook calls for both // HTTP/1 and HTTP/2. // // Since there's no need to change the persistConn's hook, we set it at creation time. cc := &ClientConn{} const isClientConn = true pconn, err := t.dialConn(ctx, cm, isClientConn, cc.maybeRunStateHook) if err != nil { return nil, err } // Note that cc.maybeRunStateHook may have been called // in the short window between dialConn and now. // This is fine. cc.stateHookMu.Lock() defer cc.stateHookMu.Unlock() if pconn.alt != nil { // If pconn.alt is set, this is a connection implemented in another package // (probably x/net/http2) or the bundled copy in h2_bundle.go. gc, ok := pconn.alt.(genericClientConn) if !ok { return nil, errors.New("http: NewClientConn returned something that is not a ClientConn") } cc.cc = gc cc.lastAvailable = gc.Available() } else { // This is an HTTP/1 connection. pconn.availch = make(chan struct{}, 1) pconn.availch <- struct{}{} cc.cc = http1ClientConn{pconn} cc.lastAvailable = 1 } return cc, nil } // Close closes the connection. // Outstanding RoundTrip calls are interrupted. func (cc *ClientConn) Close() error { defer cc.maybeRunStateHook() return cc.cc.Close() } // Err reports any fatal connection errors. // It returns nil if the connection is usable. // If it returns non-nil, the connection can no longer be used. func (cc *ClientConn) Err() error { return cc.cc.Err() } func validateClientConnRequest(req *Request) error { if req.URL == nil { return errors.New("http: nil Request.URL") } if req.Header == nil { return errors.New("http: nil Request.Header") } // Validate the outgoing headers. if err := validateHeaders(req.Header); err != "" { return fmt.Errorf("http: invalid header %s", err) } // Validate the outgoing trailers too. if err := validateHeaders(req.Trailer); err != "" { return fmt.Errorf("http: invalid trailer %s", err) } if req.Method != "" && !validMethod(req.Method) { return fmt.Errorf("http: invalid method %q", req.Method) } if req.URL.Host == "" { return errors.New("http: no Host in request URL") } return nil } // RoundTrip implements the [RoundTripper] interface. // // The request is sent on the client connection, // regardless of the URL being requested or any proxy settings. // // If the connection is at its concurrency limit, // RoundTrip waits for the connection to become available // before sending the request. func (cc *ClientConn) RoundTrip(req *Request) (*Response, error) { defer cc.maybeRunStateHook() if err := validateClientConnRequest(req); err != nil { cc.Release() return nil, err } return cc.cc.RoundTrip(req) } // Available reports the number of requests that may be sent // to the connection without blocking. // It returns 0 if the connection is closed. func (cc *ClientConn) Available() int { return cc.cc.Available() } // InFlight reports the number of requests in flight, // including reserved requests. // It returns 0 if the connection is closed. func (cc *ClientConn) InFlight() int { return cc.cc.InFlight() } // Reserve reserves a concurrency slot on the connection. // If Reserve returns nil, one additional RoundTrip call may be made // without waiting for an existing request to complete. // // The reserved concurrency slot is accounted as an in-flight request. // A successful call to RoundTrip will decrement the Available count // and increment the InFlight count. // // Each successful call to Reserve should be followed by exactly one call // to RoundTrip or Release, which will consume or release the reservation. // // If the connection is closed or at its concurrency limit, // Reserve returns an error. func (cc *ClientConn) Reserve() error { defer cc.maybeRunStateHook() return cc.cc.Reserve() } // Release releases an unused concurrency slot reserved by Reserve. // If there are no reserved concurrency slots, it has no effect. func (cc *ClientConn) Release() { defer cc.maybeRunStateHook() cc.cc.Release() } // shouldRunStateHook returns the user's state hook if we should call it, // or nil if we don't need to call it at this time. func (cc *ClientConn) shouldRunStateHook(stopRunning bool) func(*ClientConn) { cc.stateHookMu.Lock() defer cc.stateHookMu.Unlock() if cc.cc == nil { return nil } if stopRunning { cc.stateHookRunning = false } if cc.userStateHook == nil { return nil } if cc.stateHookRunning { return nil } var ( available = cc.Available() inFlight = cc.InFlight() closed = cc.Err() != nil ) var hook func(*ClientConn) if available > cc.lastAvailable || inFlight < cc.lastInFlight || closed != cc.lastClosed { hook = cc.userStateHook cc.stateHookRunning = true } cc.lastAvailable = available cc.lastInFlight = inFlight cc.lastClosed = closed return hook } func (cc *ClientConn) maybeRunStateHook() { hook := cc.shouldRunStateHook(false) if hook == nil { return } // Run the hook synchronously. // // This means that if, for example, the user calls resp.Body.Close to finish a request, // the Close call will synchronously run the hook, giving the hook the chance to // return the ClientConn to a connection pool before the next request is made. hook(cc) // The connection state may have changed while the hook was running, // in which case we need to run it again. // // If we do need to run the hook again, do so in a new goroutine to avoid blocking // the current goroutine indefinitely. hook = cc.shouldRunStateHook(true) if hook != nil { go func() { for hook != nil { hook(cc) hook = cc.shouldRunStateHook(true) } }() } } // SetStateHook arranges for f to be called when the state of the connection changes. // At most one call to f is made at a time. // If the connection's state has changed since it was created, // f is called immediately in a separate goroutine. // f may be called synchronously from RoundTrip or Response.Body.Close. // // If SetStateHook is called multiple times, the new hook replaces the old one. // If f is nil, no further calls will be made to f after SetStateHook returns. // // f is called when Available increases (more requests may be sent on the connection), // InFlight decreases (existing requests complete), or Err begins returning non-nil // (the connection is no longer usable). func (cc *ClientConn) SetStateHook(f func(*ClientConn)) { cc.stateHookMu.Lock() cc.userStateHook = f cc.stateHookMu.Unlock() cc.maybeRunStateHook() } // http1ClientConn is a genericClientConn implementation backed by // an HTTP/1 *persistConn (pconn.alt is nil). type http1ClientConn struct { pconn *persistConn } func (cc http1ClientConn) RoundTrip(req *Request) (*Response, error) { ctx := req.Context() trace := httptrace.ContextClientTrace(ctx) // Convert Request.Cancel into context cancelation. ctx, cancel := context.WithCancelCause(req.Context()) if req.Cancel != nil { go awaitLegacyCancel(ctx, cancel, req) } treq := &transportRequest{Request: req, trace: trace, ctx: ctx, cancel: cancel} resp, err := cc.pconn.roundTrip(treq) if err != nil { return nil, err } resp.Request = req return resp, nil } func (cc http1ClientConn) Close() error { cc.pconn.close(errors.New("ClientConn closed")) return nil } func (cc http1ClientConn) Err() error { select { case <-cc.pconn.closech: return cc.pconn.closed default: return nil } } func (cc http1ClientConn) Available() int { cc.pconn.mu.Lock() defer cc.pconn.mu.Unlock() if cc.pconn.closed != nil || cc.pconn.reserved || cc.pconn.inFlight { return 0 } return 1 } func (cc http1ClientConn) InFlight() int { cc.pconn.mu.Lock() defer cc.pconn.mu.Unlock() if cc.pconn.closed == nil && (cc.pconn.reserved || cc.pconn.inFlight) { return 1 } return 0 } func (cc http1ClientConn) Reserve() error { cc.pconn.mu.Lock() defer cc.pconn.mu.Unlock() if cc.pconn.closed != nil { return cc.pconn.closed } select { case <-cc.pconn.availch: default: return errors.New("connection is unavailable") } cc.pconn.reserved = true return nil } func (cc http1ClientConn) Release() { cc.pconn.mu.Lock() defer cc.pconn.mu.Unlock() if cc.pconn.reserved { select { case cc.pconn.availch <- struct{}{}: default: panic("cannot release reservation") } cc.pconn.reserved = false } }