-
Notifications
You must be signed in to change notification settings - Fork 18.1k
net.http: http.Client sometimes does not use a connection produced by a canceled dialer #53627
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
I have modified the code to enable http tracing. According to my test, the second request will use the connection from the first dial (see the first highlighted line below). But the second dial will continue because the call to Your log is a little different from mine. But I think that's because the first dial takes a little longer. I guess the second request still uses the connection from the first dial in your case. Can you run my modified code (attached to the end of this comment) and check the log again? My environment: 19:22:20.460226 Request 0: Starts
19:22:20.460357 **trace 0**GetConn: www.google.com:443
19:22:20.460377 Dial 0: Sleeps for 2 seconds!
19:22:21.460536 Request 0: Done with error Get "https://www.google.com/": context deadline exceeded
19:22:21.460638 Request 1: Starts
19:22:21.460667 **trace 1**GetConn: www.google.com:443
19:22:21.460742 Dial 1: Sleeps for 2 seconds!
19:22:22.460801 Dial 0: Performs actual dial
19:22:22.588999 Dial 0: Done with success
19:22:22.589029 **trace 0**TLSHandshakeStart
19:22:22.589048 **trace 0**TLSHandshakeDone
+ 19:22:22.589077 **trace 1**GotConn: {Conn:0xc0000a5180 Reused:true WasIdle:false IdleTime:0s}
19:22:22.589119 **trace 1**WroteHeaderField
19:22:22.589123 **trace 1**WroteHeaderField
19:22:22.589145 **trace 1**WroteHeaderField
19:22:22.589146 **trace 1**WroteHeaders
19:22:22.589169 **trace 1**WroteRequest
19:22:22.662630 **trace 1**GotFirstResponseByte
19:22:22.665751 **trace 1**PutIdleConn: <nil>
19:22:22.665822 Request 1: Got HTTP status 200
+ 19:22:23.461797 Dial 1: Performs actual dial
19:22:23.584438 Dial 1: Done with success
19:22:23.584499 **trace 1**TLSHandshakeStart
19:22:23.584515 **trace 1**TLSHandshakeDone modified codepackage main
import (
"context"
"crypto/tls"
"io/ioutil"
"log"
"net"
"net/http"
"net/http/httptrace"
"time"
)
func main() {
log.SetFlags(log.Lmicroseconds)
// Custom dialer using two seconds to dial
dial := 0
myDialer := func(ctx context.Context, network, addr string) (net.Conn, error) {
d := dial
dial += 1
dialer := tls.Dialer{}
log.Printf("Dial %v: Sleeps for 2 seconds!\n", d)
time.Sleep(2 * time.Second)
log.Printf("Dial %v: Performs actual dial\n", d)
// NOTE: Uses context.Background() to allow the dial to continue even if the request is aborted.
ret, err := dialer.DialContext(context.Background(), network, addr)
if err != nil {
log.Printf("Dial %v: Done with error %v\n", d, err)
} else {
log.Printf("Dial %v: Done with success\n", d)
}
return ret, err
}
tracer := func(r int) *httptrace.ClientTrace {
return &httptrace.ClientTrace{
GetConn: func(hostPort string) {
log.Printf("**trace %d**GetConn: %s\n", r, hostPort)
},
GotConn: func(connInfo httptrace.GotConnInfo) {
log.Printf("**trace %d**GotConn: %+v\n", r, connInfo)
},
PutIdleConn: func(err error) {
log.Printf("**trace %d**PutIdleConn: %+v\n", r, err)
},
GotFirstResponseByte: func() {
log.Printf("**trace %d**GotFirstResponseByte", r)
},
Got100Continue: func() {
log.Printf("**trace %d**Got100Continue", r)
},
DNSStart: func(httptrace.DNSStartInfo) {
log.Printf("**trace %d**DNSStart", r)
},
DNSDone: func(dnsInfo httptrace.DNSDoneInfo) {
log.Printf("**trace %d**DNS Info: %+v\n", r, dnsInfo)
},
ConnectStart: func(network string, addr string) {
log.Printf("**trace %d**ConnectStart", r)
},
ConnectDone: func(network string, addr string, err error) {
log.Printf("**trace %d**ConnectDone", r)
},
TLSHandshakeStart: func() {
log.Printf("**trace %d**TLSHandshakeStart", r)
},
TLSHandshakeDone: func(tls.ConnectionState, error) {
log.Printf("**trace %d**TLSHandshakeDone", r)
},
WroteHeaderField: func(key string, value []string) {
log.Printf("**trace %d**WroteHeaderField", r)
},
WroteHeaders: func() {
log.Printf("**trace %d**WroteHeaders", r)
},
Wait100Continue: func() {
log.Printf("**trace %d**Wait100Continue", r)
},
WroteRequest: func(httptrace.WroteRequestInfo) {
log.Printf("**trace %d**WroteRequest", r)
},
}
}
// HTTP client using custom dialer
client := http.Client{
Transport: &http.Transport{
DialTLSContext: myDialer,
},
}
// Function that makes an HTTP request
makeRequest := func(ctx context.Context, r int) {
req, err := http.NewRequestWithContext(ctx, "GET", "https://www.google.com/", nil)
if err != nil {
panic(err)
}
req = req.WithContext(httptrace.WithClientTrace(req.Context(), tracer(r)))
log.Printf("Request %v: Starts\n", r)
resp, err := client.Do(req)
if err != nil {
log.Printf("Request %v: Done with error %v\n", r, err)
} else {
ioutil.ReadAll(resp.Body)
resp.Body.Close()
log.Printf("Request %v: Got HTTP status %v\n", r, resp.StatusCode)
}
}
// Make request with 1 second timeout:
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
makeRequest(ctx, 0)
// Make request without timeout
makeRequest(context.Background(), 1)
// wait for the second dial
time.Sleep(2 * time.Second)
} |
I tried to run your code. It turns out that sometimes it works bad similar to what I got in my original report:
And sometimes it works well similar to what you got:
So perhaps you could try to run it a few times? I am happy to see that it sometimes seems to work as I would have hoped; so I guess this issue can be turned into a bug report rather than a proposal. I use go1.17.3 windows/amd64. |
I have updated the code to log the connection returned by dial to make it easy to find out which connection is used. I tested it with But with The bisect shows that it's fixed by https://go.dev/cl/379034 (for #50657). And the fix has been backported to 1.17.11 and 1.18.3. updated code to log
|
Thanks for the quick turnaround! I can confirm that I cannot reproduce the problem with go 1.18.3. |
I have a use case where:
This does not work. Consider the following set of events:
After this, if I make a new HTTP request in the http.Client, then the produced net.Conn is not reused. Instead a new dial is performed.
I also made the program below to illustrate the problem. The program dials two times where I would like it to dial just one time. To be precise, I get this output:
Where I would like this output:
The program is here:
The text was updated successfully, but these errors were encountered: