Skip to content

net.http: http.Client sometimes does not use a connection produced by a canceled dialer #53627

Closed
@cwmos

Description

@cwmos

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)
}

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions