Source file src/net/http/httptrace/trace.go

     1  // Copyright 2016 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 httptrace provides mechanisms to trace the events within
     6  // HTTP client requests.
     7  package httptrace
     8  
     9  import (
    10  	"context"
    11  	"crypto/tls"
    12  	"internal/nettrace"
    13  	"net"
    14  	"net/textproto"
    15  	"time"
    16  )
    17  
    18  // unique type to prevent assignment.
    19  type clientEventContextKey struct{}
    20  
    21  // ContextClientTrace returns the [ClientTrace] associated with the
    22  // provided context. If none, it returns nil.
    23  func ContextClientTrace(ctx context.Context) *ClientTrace {
    24  	trace, _ := ctx.Value(clientEventContextKey{}).(*ClientTrace)
    25  	return trace
    26  }
    27  
    28  // WithClientTrace returns a new context based on the provided parent
    29  // ctx. HTTP client requests made with the returned context will use
    30  // the provided trace hooks, in addition to any previous hooks
    31  // registered with ctx. Any hooks defined in the provided trace will
    32  // be called first.
    33  func WithClientTrace(ctx context.Context, trace *ClientTrace) context.Context {
    34  	if trace == nil {
    35  		panic("nil trace")
    36  	}
    37  	old := ContextClientTrace(ctx)
    38  	trace.compose(old)
    39  
    40  	ctx = context.WithValue(ctx, clientEventContextKey{}, trace)
    41  	if trace.hasNetHooks() {
    42  		nt := &nettrace.Trace{
    43  			ConnectStart: trace.ConnectStart,
    44  			ConnectDone:  trace.ConnectDone,
    45  		}
    46  		if trace.DNSStart != nil {
    47  			nt.DNSStart = func(name string) {
    48  				trace.DNSStart(DNSStartInfo{Host: name})
    49  			}
    50  		}
    51  		if trace.DNSDone != nil {
    52  			nt.DNSDone = func(netIPs []any, coalesced bool, err error) {
    53  				addrs := make([]net.IPAddr, len(netIPs))
    54  				for i, ip := range netIPs {
    55  					addrs[i] = ip.(net.IPAddr)
    56  				}
    57  				trace.DNSDone(DNSDoneInfo{
    58  					Addrs:     addrs,
    59  					Coalesced: coalesced,
    60  					Err:       err,
    61  				})
    62  			}
    63  		}
    64  		ctx = context.WithValue(ctx, nettrace.TraceKey{}, nt)
    65  	}
    66  	return ctx
    67  }
    68  
    69  // ClientTrace is a set of hooks to run at various stages of an outgoing
    70  // HTTP request. Any particular hook may be nil. Functions may be
    71  // called concurrently from different goroutines and some may be called
    72  // after the request has completed or failed.
    73  //
    74  // ClientTrace currently traces a single HTTP request & response
    75  // during a single round trip and has no hooks that span a series
    76  // of redirected requests.
    77  //
    78  // See https://go.dev/blog/http-tracing for more.
    79  type ClientTrace struct {
    80  	// GetConn is called before a connection is created or
    81  	// retrieved from an idle pool. The hostPort is the
    82  	// "host:port" of the target or proxy. GetConn is called even
    83  	// if there's already an idle cached connection available.
    84  	GetConn func(hostPort string)
    85  
    86  	// GotConn is called after a successful connection is
    87  	// obtained. There is no hook for failure to obtain a
    88  	// connection; instead, use the error from
    89  	// Transport.RoundTrip.
    90  	GotConn func(GotConnInfo)
    91  
    92  	// PutIdleConn is called when the connection is returned to
    93  	// the idle pool. If err is nil, the connection was
    94  	// successfully returned to the idle pool. If err is non-nil,
    95  	// it describes why not. PutIdleConn is not called if
    96  	// connection reuse is disabled via Transport.DisableKeepAlives.
    97  	// PutIdleConn is called before the caller's Response.Body.Close
    98  	// call returns.
    99  	// For HTTP/2, this hook is not currently used.
   100  	PutIdleConn func(err error)
   101  
   102  	// GotFirstResponseByte is called when the first byte of the response
   103  	// headers is available.
   104  	GotFirstResponseByte func()
   105  
   106  	// Got100Continue is called if the server replies with a "100
   107  	// Continue" response.
   108  	Got100Continue func()
   109  
   110  	// Got1xxResponse is called for each 1xx informational response header
   111  	// returned before the final non-1xx response. Got1xxResponse is called
   112  	// for "100 Continue" responses, even if Got100Continue is also defined.
   113  	// If it returns an error, the client request is aborted with that error value.
   114  	Got1xxResponse func(code int, header textproto.MIMEHeader) error
   115  
   116  	// DNSStart is called when a DNS lookup begins.
   117  	DNSStart func(DNSStartInfo)
   118  
   119  	// DNSDone is called when a DNS lookup ends.
   120  	DNSDone func(DNSDoneInfo)
   121  
   122  	// ConnectStart is called when a new connection's Dial begins.
   123  	// If net.Dialer.DualStack (IPv6 "Happy Eyeballs") support is
   124  	// enabled, this may be called multiple times.
   125  	ConnectStart func(network, addr string)
   126  
   127  	// ConnectDone is called when a new connection's Dial
   128  	// completes. The provided err indicates whether the
   129  	// connection completed successfully.
   130  	// If net.Dialer.DualStack ("Happy Eyeballs") support is
   131  	// enabled, this may be called multiple times.
   132  	ConnectDone func(network, addr string, err error)
   133  
   134  	// TLSHandshakeStart is called when the TLS handshake is started. When
   135  	// connecting to an HTTPS site via an HTTP proxy, the handshake happens
   136  	// after the CONNECT request is processed by the proxy.
   137  	TLSHandshakeStart func()
   138  
   139  	// TLSHandshakeDone is called after the TLS handshake with either the
   140  	// successful handshake's connection state, or a non-nil error on handshake
   141  	// failure.
   142  	TLSHandshakeDone func(tls.ConnectionState, error)
   143  
   144  	// WroteHeaderField is called after the Transport has written
   145  	// each request header. At the time of this call the values
   146  	// might be buffered and not yet written to the network.
   147  	WroteHeaderField func(key string, value []string)
   148  
   149  	// WroteHeaders is called after the Transport has written
   150  	// all request headers.
   151  	WroteHeaders func()
   152  
   153  	// Wait100Continue is called if the Request specified
   154  	// "Expect: 100-continue" and the Transport has written the
   155  	// request headers but is waiting for "100 Continue" from the
   156  	// server before writing the request body.
   157  	Wait100Continue func()
   158  
   159  	// WroteRequest is called with the result of writing the
   160  	// request and any body. It may be called multiple times
   161  	// in the case of retried requests.
   162  	WroteRequest func(WroteRequestInfo)
   163  }
   164  
   165  // WroteRequestInfo contains information provided to the WroteRequest
   166  // hook.
   167  type WroteRequestInfo struct {
   168  	// Err is any error encountered while writing the Request.
   169  	Err error
   170  }
   171  
   172  // compose modifies t such that it respects the previously-registered hooks in old,
   173  // subject to the composition policy requested in t.Compose.
   174  func (t *ClientTrace) compose(old *ClientTrace) {
   175  	if old == nil {
   176  		return
   177  	}
   178  	t.GetConn = compose1to0(t.GetConn, old.GetConn)
   179  	t.GotConn = compose1to0(t.GotConn, old.GotConn)
   180  	t.PutIdleConn = compose1to0(t.PutIdleConn, old.PutIdleConn)
   181  	t.GotFirstResponseByte = compose0to0(t.GotFirstResponseByte, old.GotFirstResponseByte)
   182  	t.Got100Continue = compose0to0(t.Got100Continue, old.Got100Continue)
   183  	t.Got1xxResponse = compose2to1(t.Got1xxResponse, old.Got1xxResponse)
   184  	t.DNSStart = compose1to0(t.DNSStart, old.DNSStart)
   185  	t.DNSDone = compose1to0(t.DNSDone, old.DNSDone)
   186  	t.ConnectStart = compose2to0(t.ConnectStart, old.ConnectStart)
   187  	t.ConnectDone = compose3to0(t.ConnectDone, old.ConnectDone)
   188  	t.TLSHandshakeStart = compose0to0(t.TLSHandshakeStart, old.TLSHandshakeStart)
   189  	t.TLSHandshakeDone = compose2to0(t.TLSHandshakeDone, old.TLSHandshakeDone)
   190  	t.WroteHeaderField = compose2to0(t.WroteHeaderField, old.WroteHeaderField)
   191  	t.WroteHeaders = compose0to0(t.WroteHeaders, old.WroteHeaders)
   192  	t.Wait100Continue = compose0to0(t.Wait100Continue, old.Wait100Continue)
   193  	t.WroteRequest = compose1to0(t.WroteRequest, old.WroteRequest)
   194  }
   195  
   196  func compose0to0[F func()](f1, f2 F) F {
   197  	if f1 == nil {
   198  		return f2
   199  	}
   200  	if f2 == nil {
   201  		return f1
   202  	}
   203  	return func() {
   204  		f1()
   205  		f2()
   206  	}
   207  }
   208  
   209  func compose1to0[F func(A), A any](f1, f2 F) F {
   210  	if f1 == nil {
   211  		return f2
   212  	}
   213  	if f2 == nil {
   214  		return f1
   215  	}
   216  	return func(a A) {
   217  		f1(a)
   218  		f2(a)
   219  	}
   220  }
   221  
   222  func compose2to0[F func(A, B), A, B any](f1, f2 F) F {
   223  	if f1 == nil {
   224  		return f2
   225  	}
   226  	if f2 == nil {
   227  		return f1
   228  	}
   229  	return func(a A, b B) {
   230  		f1(a, b)
   231  		f2(a, b)
   232  	}
   233  }
   234  
   235  func compose2to1[F func(A, B) R, A, B, R any](f1, f2 F) F {
   236  	if f1 == nil {
   237  		return f2
   238  	}
   239  	if f2 == nil {
   240  		return f1
   241  	}
   242  	return func(a A, b B) R {
   243  		f1(a, b)
   244  		return f2(a, b)
   245  	}
   246  }
   247  
   248  func compose3to0[F func(A, B, C), A, B, C any](f1, f2 F) F {
   249  	if f1 == nil {
   250  		return f2
   251  	}
   252  	if f2 == nil {
   253  		return f1
   254  	}
   255  	return func(a A, b B, c C) {
   256  		f1(a, b, c)
   257  		f2(a, b, c)
   258  	}
   259  }
   260  
   261  // DNSStartInfo contains information about a DNS request.
   262  type DNSStartInfo struct {
   263  	Host string
   264  }
   265  
   266  // DNSDoneInfo contains information about the results of a DNS lookup.
   267  type DNSDoneInfo struct {
   268  	// Addrs are the IPv4 and/or IPv6 addresses found in the DNS
   269  	// lookup. The contents of the slice should not be mutated.
   270  	Addrs []net.IPAddr
   271  
   272  	// Err is any error that occurred during the DNS lookup.
   273  	Err error
   274  
   275  	// Coalesced is whether the Addrs were shared with another
   276  	// caller who was doing the same DNS lookup concurrently.
   277  	Coalesced bool
   278  }
   279  
   280  func (t *ClientTrace) hasNetHooks() bool {
   281  	if t == nil {
   282  		return false
   283  	}
   284  	return t.DNSStart != nil || t.DNSDone != nil || t.ConnectStart != nil || t.ConnectDone != nil
   285  }
   286  
   287  // GotConnInfo is the argument to the [ClientTrace.GotConn] function and
   288  // contains information about the obtained connection.
   289  type GotConnInfo struct {
   290  	// Conn is the connection that was obtained. It is owned by
   291  	// the http.Transport and should not be read, written or
   292  	// closed by users of ClientTrace.
   293  	Conn net.Conn
   294  
   295  	// Reused is whether this connection has been previously
   296  	// used for another HTTP request.
   297  	Reused bool
   298  
   299  	// WasIdle is whether this connection was obtained from an
   300  	// idle pool.
   301  	WasIdle bool
   302  
   303  	// IdleTime reports how long the connection was previously
   304  	// idle, if WasIdle is true.
   305  	IdleTime time.Duration
   306  }
   307  

View as plain text