Closed
Description
I have a use case where:
- I want to make many HTTP requests each with a short deadline
- It may take a long time to dial the HTTP server - longer than the deadline of each HTTP request.
This does not work. Consider the following set of events:
- I create an http.Client with a custom dialer
- I start an HTTP request in the http.Client
- http.Client invokes the custom dialer
- I cancel the HTTP request
- The dialer completes successfully producing a net.Conn
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:
Request 0: Starts
Dial 0: Sleeps for 2 seconds!
Request 0: Done with error Get "https://www.google.com/": context deadline exceeded
Request 1: Starts
Dial 1: Sleeps for 2 seconds!
Dial 0: Performs actual dial
Dial 0: Done with success
Dial 1: Performs actual dial
Dial 1: Done with success
Request 1: Got HTTP status 200
Where I would like this output:
Request 0: Starts
Dial 0: Sleeps for 2 seconds!
Request 0: Done with error Get "https://www.google.com/": context deadline exceeded
Request 1: Starts
Dial 0: Performs actual dial
Dial 0: Done with success
Request 1: Got HTTP status 200
The program is here:
package main
import (
"context"
"crypto/tls"
"fmt"
"io/ioutil"
"net"
"net/http"
"time"
)
func main() {
// 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{}
fmt.Printf("Dial %v: Sleeps for 2 seconds!\n", d)
time.Sleep(2 * time.Second)
fmt.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 {
fmt.Printf("Dial %v: Done with error %v\n", d, err)
} else {
fmt.Printf("Dial %v: Done with success\n", d)
}
return ret, err
}
// 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)
}
fmt.Printf("Request %v: Starts\n", r)
resp, err := client.Do(req)
if err != nil {
fmt.Printf("Request %v: Done with error %v\n", r, err)
} else {
ioutil.ReadAll(resp.Body)
resp.Body.Close()
fmt.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)
}