#81646·Go

x/net/http2: New wrapped http2 implementation (go1.27+) missing single flight mechanism

Author: FanglidingCreated Sep 22, 2026Updated Sep 28, 2026
LabelsNeedsFix

Go version

go 1.27 linux/amd64

Output of go env in your module/workspace:

bash
AR='ar'
CC='gcc'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_ENABLED='1'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='g++'
GCCGO='gccgo'
GO111MODULE=''
GOAMD64='v1'
GOARCH='amd64'
GOAUTH='netrc'
GOBIN=''
GOCACHE='/home/codespace/.cache/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/home/codespace/.config/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build2735046389=/tmp/go-build -gno-record-gcc-switches'
GOHOSTARCH='amd64'
GOHOSTOS='linux'
GOINSECURE=''
GOMOD='/workspaces/Xray-core/go.mod'
GOMODCACHE='/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='linux'
GOPATH='/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/usr/local/go'
GOSUMDB='sum.golang.org'
GOTELEMETRY='local'
GOTELEMETRYDIR='/home/codespace/.config/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/usr/local/go/pkg/tool/linux_amd64'
GOVCS=''
GOVERSION='go1.26.1'
GOWORK=''
PKG_CONFIG='pkg-config'

What did you do?

run this code to count dial calls in http2

package main

import (
	"context"
	"crypto/tls"
	"fmt"
	"io"
	"net"
	"net/http"
	"sync"
	"sync/atomic"

	"golang.org/x/net/http2"
)

func main() {
	totalRequests := 100
	targetURL := "https://cp.cloudflare.com/cdn-cgi/trace"

	var dialCount atomic.Int64

	client := &http.Client{
		Transport: &http2.Transport{
			DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (c net.Conn, err error) {
				dialCount.Add(1)
				dialer := &tls.Dialer{Config: cfg}
				return dialer.DialContext(ctx, network, addr)
			},
		},
	}

	var wg sync.WaitGroup
	start := make(chan struct{})

	for range totalRequests {
		wg.Add(1)
		go func() {
			defer wg.Done()
			<-start
			resp, err := client.Get(targetURL)
			if err == nil {
				io.Copy(io.Discard, resp.Body)
				resp.Body.Close()
			}
		}()
	}

	close(start)
	wg.Wait()

	fmt.Printf("Total Dial Calls: %d\n", dialCount.Load())
}

What did you see happen?

It made 100 tcp dial calls for every http request

Total Dial Calls: 100

What did you expect to see?

In old http2 implementation, instantly sending multiple requests will only make one TCP dial, this is the expected behavior of http2 run code above with this

go run -tags http2legacy .

And it will return

Total Dial Calls: 1

I understand that HTTP package needs to dial multiple connections because the server HTTP version is unknown, but we have prior knowledge in net/x/http2, and perhaps we can add a config to enable the single flight mechanism in http package (or automatically use when alpn only contains h2?)