Dan Willemsen | 38f2dba | 2016-07-08 14:54:35 -0700 | [diff] [blame] | 1 | // Copyright 2016 The Go Authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style |
Dan Willemsen | ebae302 | 2017-01-13 23:01:08 -0800 | [diff] [blame] | 3 | // license that can be found in the LICENSE file. |
Dan Willemsen | 38f2dba | 2016-07-08 14:54:35 -0700 | [diff] [blame] | 4 | |
| 5 | // Package httptrace provides mechanisms to trace the events within |
| 6 | // HTTP client requests. |
| 7 | package httptrace |
| 8 | |
| 9 | import ( |
| 10 | "context" |
Dan Willemsen | ebae302 | 2017-01-13 23:01:08 -0800 | [diff] [blame] | 11 | "crypto/tls" |
Dan Willemsen | 38f2dba | 2016-07-08 14:54:35 -0700 | [diff] [blame] | 12 | "internal/nettrace" |
| 13 | "net" |
Dan Willemsen | c741332 | 2018-08-27 23:21:26 -0700 | [diff] [blame] | 14 | "net/textproto" |
Dan Willemsen | 38f2dba | 2016-07-08 14:54:35 -0700 | [diff] [blame] | 15 | "reflect" |
| 16 | "time" |
| 17 | ) |
| 18 | |
| 19 | // unique type to prevent assignment. |
| 20 | type clientEventContextKey struct{} |
| 21 | |
| 22 | // ContextClientTrace returns the ClientTrace associated with the |
| 23 | // provided context. If none, it returns nil. |
| 24 | func ContextClientTrace(ctx context.Context) *ClientTrace { |
| 25 | trace, _ := ctx.Value(clientEventContextKey{}).(*ClientTrace) |
| 26 | return trace |
| 27 | } |
| 28 | |
| 29 | // WithClientTrace returns a new context based on the provided parent |
| 30 | // ctx. HTTP client requests made with the returned context will use |
| 31 | // the provided trace hooks, in addition to any previous hooks |
| 32 | // registered with ctx. Any hooks defined in the provided trace will |
| 33 | // be called first. |
| 34 | func WithClientTrace(ctx context.Context, trace *ClientTrace) context.Context { |
| 35 | if trace == nil { |
| 36 | panic("nil trace") |
| 37 | } |
| 38 | old := ContextClientTrace(ctx) |
| 39 | trace.compose(old) |
| 40 | |
| 41 | ctx = context.WithValue(ctx, clientEventContextKey{}, trace) |
| 42 | if trace.hasNetHooks() { |
| 43 | nt := &nettrace.Trace{ |
| 44 | ConnectStart: trace.ConnectStart, |
| 45 | ConnectDone: trace.ConnectDone, |
| 46 | } |
| 47 | if trace.DNSStart != nil { |
| 48 | nt.DNSStart = func(name string) { |
| 49 | trace.DNSStart(DNSStartInfo{Host: name}) |
| 50 | } |
| 51 | } |
| 52 | if trace.DNSDone != nil { |
Dan Willemsen | bc60c3c | 2021-12-15 01:09:00 -0800 | [diff] [blame] | 53 | nt.DNSDone = func(netIPs []any, coalesced bool, err error) { |
Dan Willemsen | 38f2dba | 2016-07-08 14:54:35 -0700 | [diff] [blame] | 54 | addrs := make([]net.IPAddr, len(netIPs)) |
| 55 | for i, ip := range netIPs { |
| 56 | addrs[i] = ip.(net.IPAddr) |
| 57 | } |
| 58 | trace.DNSDone(DNSDoneInfo{ |
| 59 | Addrs: addrs, |
| 60 | Coalesced: coalesced, |
| 61 | Err: err, |
| 62 | }) |
| 63 | } |
| 64 | } |
| 65 | ctx = context.WithValue(ctx, nettrace.TraceKey{}, nt) |
| 66 | } |
| 67 | return ctx |
| 68 | } |
| 69 | |
Dan Willemsen | ebae302 | 2017-01-13 23:01:08 -0800 | [diff] [blame] | 70 | // ClientTrace is a set of hooks to run at various stages of an outgoing |
| 71 | // HTTP request. Any particular hook may be nil. Functions may be |
| 72 | // called concurrently from different goroutines and some may be called |
| 73 | // after the request has completed or failed. |
| 74 | // |
| 75 | // ClientTrace currently traces a single HTTP request & response |
| 76 | // during a single round trip and has no hooks that span a series |
| 77 | // of redirected requests. |
| 78 | // |
| 79 | // See https://blog.golang.org/http-tracing for more. |
Dan Willemsen | 38f2dba | 2016-07-08 14:54:35 -0700 | [diff] [blame] | 80 | type ClientTrace struct { |
| 81 | // GetConn is called before a connection is created or |
| 82 | // retrieved from an idle pool. The hostPort is the |
| 83 | // "host:port" of the target or proxy. GetConn is called even |
| 84 | // if there's already an idle cached connection available. |
| 85 | GetConn func(hostPort string) |
| 86 | |
| 87 | // GotConn is called after a successful connection is |
| 88 | // obtained. There is no hook for failure to obtain a |
| 89 | // connection; instead, use the error from |
| 90 | // Transport.RoundTrip. |
| 91 | GotConn func(GotConnInfo) |
| 92 | |
| 93 | // PutIdleConn is called when the connection is returned to |
| 94 | // the idle pool. If err is nil, the connection was |
| 95 | // successfully returned to the idle pool. If err is non-nil, |
| 96 | // it describes why not. PutIdleConn is not called if |
| 97 | // connection reuse is disabled via Transport.DisableKeepAlives. |
| 98 | // PutIdleConn is called before the caller's Response.Body.Close |
| 99 | // call returns. |
| 100 | // For HTTP/2, this hook is not currently used. |
| 101 | PutIdleConn func(err error) |
| 102 | |
| 103 | // GotFirstResponseByte is called when the first byte of the response |
| 104 | // headers is available. |
| 105 | GotFirstResponseByte func() |
| 106 | |
| 107 | // Got100Continue is called if the server replies with a "100 |
| 108 | // Continue" response. |
| 109 | Got100Continue func() |
| 110 | |
Dan Willemsen | c741332 | 2018-08-27 23:21:26 -0700 | [diff] [blame] | 111 | // Got1xxResponse is called for each 1xx informational response header |
| 112 | // returned before the final non-1xx response. Got1xxResponse is called |
| 113 | // for "100 Continue" responses, even if Got100Continue is also defined. |
| 114 | // If it returns an error, the client request is aborted with that error value. |
| 115 | Got1xxResponse func(code int, header textproto.MIMEHeader) error |
| 116 | |
Dan Willemsen | 38f2dba | 2016-07-08 14:54:35 -0700 | [diff] [blame] | 117 | // DNSStart is called when a DNS lookup begins. |
| 118 | DNSStart func(DNSStartInfo) |
| 119 | |
| 120 | // DNSDone is called when a DNS lookup ends. |
| 121 | DNSDone func(DNSDoneInfo) |
| 122 | |
| 123 | // ConnectStart is called when a new connection's Dial begins. |
| 124 | // If net.Dialer.DualStack (IPv6 "Happy Eyeballs") support is |
| 125 | // enabled, this may be called multiple times. |
| 126 | ConnectStart func(network, addr string) |
| 127 | |
| 128 | // ConnectDone is called when a new connection's Dial |
| 129 | // completes. The provided err indicates whether the |
Dan Willemsen | cc753b7 | 2021-08-31 13:25:42 -0700 | [diff] [blame] | 130 | // connection completed successfully. |
Dan Willemsen | 38f2dba | 2016-07-08 14:54:35 -0700 | [diff] [blame] | 131 | // If net.Dialer.DualStack ("Happy Eyeballs") support is |
| 132 | // enabled, this may be called multiple times. |
| 133 | ConnectDone func(network, addr string, err error) |
| 134 | |
Dan Willemsen | ebae302 | 2017-01-13 23:01:08 -0800 | [diff] [blame] | 135 | // TLSHandshakeStart is called when the TLS handshake is started. When |
Patrice Arruda | 748609c | 2020-06-25 12:12:21 -0700 | [diff] [blame] | 136 | // connecting to an HTTPS site via an HTTP proxy, the handshake happens |
| 137 | // after the CONNECT request is processed by the proxy. |
Dan Willemsen | ebae302 | 2017-01-13 23:01:08 -0800 | [diff] [blame] | 138 | TLSHandshakeStart func() |
| 139 | |
| 140 | // TLSHandshakeDone is called after the TLS handshake with either the |
| 141 | // successful handshake's connection state, or a non-nil error on handshake |
| 142 | // failure. |
| 143 | TLSHandshakeDone func(tls.ConnectionState, error) |
| 144 | |
Dan Willemsen | c741332 | 2018-08-27 23:21:26 -0700 | [diff] [blame] | 145 | // WroteHeaderField is called after the Transport has written |
| 146 | // each request header. At the time of this call the values |
| 147 | // might be buffered and not yet written to the network. |
| 148 | WroteHeaderField func(key string, value []string) |
| 149 | |
Dan Willemsen | 38f2dba | 2016-07-08 14:54:35 -0700 | [diff] [blame] | 150 | // WroteHeaders is called after the Transport has written |
Dan Willemsen | c741332 | 2018-08-27 23:21:26 -0700 | [diff] [blame] | 151 | // all request headers. |
Dan Willemsen | 38f2dba | 2016-07-08 14:54:35 -0700 | [diff] [blame] | 152 | WroteHeaders func() |
| 153 | |
| 154 | // Wait100Continue is called if the Request specified |
Colin Cross | d9c6b80 | 2019-03-19 21:10:31 -0700 | [diff] [blame] | 155 | // "Expect: 100-continue" and the Transport has written the |
Dan Willemsen | 38f2dba | 2016-07-08 14:54:35 -0700 | [diff] [blame] | 156 | // request headers but is waiting for "100 Continue" from the |
| 157 | // server before writing the request body. |
| 158 | Wait100Continue func() |
| 159 | |
| 160 | // WroteRequest is called with the result of writing the |
Dan Willemsen | ebae302 | 2017-01-13 23:01:08 -0800 | [diff] [blame] | 161 | // request and any body. It may be called multiple times |
| 162 | // in the case of retried requests. |
Dan Willemsen | 38f2dba | 2016-07-08 14:54:35 -0700 | [diff] [blame] | 163 | WroteRequest func(WroteRequestInfo) |
| 164 | } |
| 165 | |
| 166 | // WroteRequestInfo contains information provided to the WroteRequest |
| 167 | // hook. |
| 168 | type WroteRequestInfo struct { |
| 169 | // Err is any error encountered while writing the Request. |
| 170 | Err error |
| 171 | } |
| 172 | |
| 173 | // compose modifies t such that it respects the previously-registered hooks in old, |
| 174 | // subject to the composition policy requested in t.Compose. |
| 175 | func (t *ClientTrace) compose(old *ClientTrace) { |
| 176 | if old == nil { |
| 177 | return |
| 178 | } |
| 179 | tv := reflect.ValueOf(t).Elem() |
| 180 | ov := reflect.ValueOf(old).Elem() |
| 181 | structType := tv.Type() |
| 182 | for i := 0; i < structType.NumField(); i++ { |
| 183 | tf := tv.Field(i) |
| 184 | hookType := tf.Type() |
| 185 | if hookType.Kind() != reflect.Func { |
| 186 | continue |
| 187 | } |
| 188 | of := ov.Field(i) |
| 189 | if of.IsNil() { |
| 190 | continue |
| 191 | } |
| 192 | if tf.IsNil() { |
| 193 | tf.Set(of) |
| 194 | continue |
| 195 | } |
| 196 | |
| 197 | // Make a copy of tf for tf to call. (Otherwise it |
| 198 | // creates a recursive call cycle and stack overflows) |
| 199 | tfCopy := reflect.ValueOf(tf.Interface()) |
| 200 | |
| 201 | // We need to call both tf and of in some order. |
| 202 | newFunc := reflect.MakeFunc(hookType, func(args []reflect.Value) []reflect.Value { |
| 203 | tfCopy.Call(args) |
| 204 | return of.Call(args) |
| 205 | }) |
| 206 | tv.Field(i).Set(newFunc) |
| 207 | } |
| 208 | } |
| 209 | |
| 210 | // DNSStartInfo contains information about a DNS request. |
| 211 | type DNSStartInfo struct { |
| 212 | Host string |
| 213 | } |
| 214 | |
| 215 | // DNSDoneInfo contains information about the results of a DNS lookup. |
| 216 | type DNSDoneInfo struct { |
| 217 | // Addrs are the IPv4 and/or IPv6 addresses found in the DNS |
| 218 | // lookup. The contents of the slice should not be mutated. |
| 219 | Addrs []net.IPAddr |
| 220 | |
| 221 | // Err is any error that occurred during the DNS lookup. |
| 222 | Err error |
| 223 | |
| 224 | // Coalesced is whether the Addrs were shared with another |
| 225 | // caller who was doing the same DNS lookup concurrently. |
| 226 | Coalesced bool |
| 227 | } |
| 228 | |
| 229 | func (t *ClientTrace) hasNetHooks() bool { |
| 230 | if t == nil { |
| 231 | return false |
| 232 | } |
| 233 | return t.DNSStart != nil || t.DNSDone != nil || t.ConnectStart != nil || t.ConnectDone != nil |
| 234 | } |
| 235 | |
| 236 | // GotConnInfo is the argument to the ClientTrace.GotConn function and |
| 237 | // contains information about the obtained connection. |
| 238 | type GotConnInfo struct { |
| 239 | // Conn is the connection that was obtained. It is owned by |
| 240 | // the http.Transport and should not be read, written or |
| 241 | // closed by users of ClientTrace. |
| 242 | Conn net.Conn |
| 243 | |
| 244 | // Reused is whether this connection has been previously |
| 245 | // used for another HTTP request. |
| 246 | Reused bool |
| 247 | |
| 248 | // WasIdle is whether this connection was obtained from an |
| 249 | // idle pool. |
| 250 | WasIdle bool |
| 251 | |
| 252 | // IdleTime reports how long the connection was previously |
| 253 | // idle, if WasIdle is true. |
| 254 | IdleTime time.Duration |
| 255 | } |