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 []byte) {
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 := []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://blog.golang.org/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 []byte)
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 []byte, 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 [][]byte)
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 func (t *ClientTrace) compose(old *ClientTrace) {
174 if old == nil {
175 return
176 }
177 if old.GetConn != nil {
178 if prev, cur := old.GetConn, t.GetConn; cur != nil {
179 t.GetConn = func(hostPort string) { cur(hostPort); prev(hostPort) }
180 } else {
181 t.GetConn = prev
182 }
183 }
184 if old.GotConn != nil {
185 if prev, cur := old.GotConn, t.GotConn; cur != nil {
186 t.GotConn = func(info GotConnInfo) { cur(info); prev(info) }
187 } else {
188 t.GotConn = prev
189 }
190 }
191 if old.PutIdleConn != nil {
192 if prev, cur := old.PutIdleConn, t.PutIdleConn; cur != nil {
193 t.PutIdleConn = func(err error) { cur(err); prev(err) }
194 } else {
195 t.PutIdleConn = prev
196 }
197 }
198 if old.GotFirstResponseByte != nil {
199 if prev, cur := old.GotFirstResponseByte, t.GotFirstResponseByte; cur != nil {
200 t.GotFirstResponseByte = func() { cur(); prev() }
201 } else {
202 t.GotFirstResponseByte = prev
203 }
204 }
205 if old.Got100Continue != nil {
206 if prev, cur := old.Got100Continue, t.Got100Continue; cur != nil {
207 t.Got100Continue = func() { cur(); prev() }
208 } else {
209 t.Got100Continue = prev
210 }
211 }
212 if old.Got1xxResponse != nil {
213 if prev, cur := old.Got1xxResponse, t.Got1xxResponse; cur != nil {
214 t.Got1xxResponse = func(code int, header textproto.MIMEHeader) error { cur(code, header); return prev(code, header) }
215 } else {
216 t.Got1xxResponse = prev
217 }
218 }
219 if old.DNSStart != nil {
220 if prev, cur := old.DNSStart, t.DNSStart; cur != nil {
221 t.DNSStart = func(info DNSStartInfo) { cur(info); prev(info) }
222 } else {
223 t.DNSStart = prev
224 }
225 }
226 if old.DNSDone != nil {
227 if prev, cur := old.DNSDone, t.DNSDone; cur != nil {
228 t.DNSDone = func(info DNSDoneInfo) { cur(info); prev(info) }
229 } else {
230 t.DNSDone = prev
231 }
232 }
233 if old.ConnectStart != nil {
234 if prev, cur := old.ConnectStart, t.ConnectStart; cur != nil {
235 t.ConnectStart = func(network, addr []byte) { cur(network, addr); prev(network, addr) }
236 } else {
237 t.ConnectStart = prev
238 }
239 }
240 if old.ConnectDone != nil {
241 if prev, cur := old.ConnectDone, t.ConnectDone; cur != nil {
242 t.ConnectDone = func(network, addr []byte, err error) { cur(network, addr, err); prev(network, addr, err) }
243 } else {
244 t.ConnectDone = prev
245 }
246 }
247 if old.TLSHandshakeStart != nil {
248 if prev, cur := old.TLSHandshakeStart, t.TLSHandshakeStart; cur != nil {
249 t.TLSHandshakeStart = func() { cur(); prev() }
250 } else {
251 t.TLSHandshakeStart = prev
252 }
253 }
254 if old.TLSHandshakeDone != nil {
255 if prev, cur := old.TLSHandshakeDone, t.TLSHandshakeDone; cur != nil {
256 t.TLSHandshakeDone = func(cs tls.ConnectionState, err error) { cur(cs, err); prev(cs, err) }
257 } else {
258 t.TLSHandshakeDone = prev
259 }
260 }
261 if old.WroteHeaderField != nil {
262 if prev, cur := old.WroteHeaderField, t.WroteHeaderField; cur != nil {
263 t.WroteHeaderField = func(key string, value [][]byte) { cur(key, value); prev(key, value) }
264 } else {
265 t.WroteHeaderField = prev
266 }
267 }
268 if old.WroteHeaders != nil {
269 if prev, cur := old.WroteHeaders, t.WroteHeaders; cur != nil {
270 t.WroteHeaders = func() { cur(); prev() }
271 } else {
272 t.WroteHeaders = prev
273 }
274 }
275 if old.Wait100Continue != nil {
276 if prev, cur := old.Wait100Continue, t.Wait100Continue; cur != nil {
277 t.Wait100Continue = func() { cur(); prev() }
278 } else {
279 t.Wait100Continue = prev
280 }
281 }
282 if old.WroteRequest != nil {
283 if prev, cur := old.WroteRequest, t.WroteRequest; cur != nil {
284 t.WroteRequest = func(info WroteRequestInfo) { cur(info); prev(info) }
285 } else {
286 t.WroteRequest = prev
287 }
288 }
289 }
290 291 // DNSStartInfo contains information about a DNS request.
292 type DNSStartInfo struct {
293 Host string
294 }
295 296 // DNSDoneInfo contains information about the results of a DNS lookup.
297 type DNSDoneInfo struct {
298 // Addrs are the IPv4 and/or IPv6 addresses found in the DNS
299 // lookup. The contents of the slice should not be mutated.
300 Addrs []net.IPAddr
301 302 // Err is any error that occurred during the DNS lookup.
303 Err error
304 305 // Coalesced is whether the Addrs were shared with another
306 // caller who was doing the same DNS lookup concurrently.
307 Coalesced bool
308 }
309 310 func (t *ClientTrace) hasNetHooks() bool {
311 if t == nil {
312 return false
313 }
314 return t.DNSStart != nil || t.DNSDone != nil || t.ConnectStart != nil || t.ConnectDone != nil
315 }
316 317 // GotConnInfo is the argument to the [ClientTrace.GotConn] function and
318 // contains information about the obtained connection.
319 type GotConnInfo struct {
320 // Conn is the connection that was obtained. It is owned by
321 // the http.Transport and should not be read, written or
322 // closed by users of ClientTrace.
323 Conn net.Conn
324 325 // Reused is whether this connection has been previously
326 // used for another HTTP request.
327 Reused bool
328 329 // WasIdle is whether this connection was obtained from an
330 // idle pool.
331 WasIdle bool
332 333 // IdleTime reports how long the connection was previously
334 // idle, if WasIdle is true.
335 IdleTime time.Duration
336 }
337