diff --git a/util/helm/client.go b/util/helm/client.go index eb8b298f84..f84909090f 100644 --- a/util/helm/client.go +++ b/util/helm/client.go @@ -373,6 +373,7 @@ func (c *nativeHelmChart) loadRepoIndex(ctx context.Context, maxIndexSize int64) Proxy: proxy.GetCallback(c.proxy, c.noProxy), TLSClientConfig: tlsConf, DisableKeepAlives: true, + ForceAttemptHTTP2: true, } client := http.Client{Transport: tr} resp, err := client.Do(req) @@ -492,6 +493,7 @@ func (c *nativeHelmChart) GetTags(chart string, noCache bool) ([]string, error) Proxy: proxy.GetCallback(c.proxy, c.noProxy), TLSClientConfig: tlsConf, DisableKeepAlives: true, + ForceAttemptHTTP2: true, } // Wrap transport to add User-Agent header to all requests diff --git a/util/helm/client_test.go b/util/helm/client_test.go index 567f6a4625..d84a9831e1 100644 --- a/util/helm/client_test.go +++ b/util/helm/client_test.go @@ -2,6 +2,7 @@ package helm import ( "bytes" + "crypto/tls" "encoding/json" "fmt" "math" @@ -10,6 +11,7 @@ import ( "net/url" "os" "path/filepath" + "slices" "strings" "testing" @@ -574,6 +576,68 @@ func TestGetTagsCaching(t *testing.T) { }) } +func TestGetTagsUsesHTTP2(t *testing.T) { + t.Run("should negotiate HTTP/2 when TLS is configured", func(t *testing.T) { + var requestProtos []string + server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requestProtos = append(requestProtos, r.Proto) + t.Logf("called %s with proto %s", r.URL.Path, r.Proto) + + responseTags := fakeTagsList{ + Tags: []string{"1.0.0"}, + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + require.NoError(t, json.NewEncoder(w).Encode(responseTags)) + })) + // httptest.NewTLSServer only advertises http/1.1 in ALPN, so we must + // configure the server to also offer h2 for HTTP/2 negotiation to work. + server.TLS = &tls.Config{NextProtos: []string{"h2", "http/1.1"}} + server.StartTLS() + t.Cleanup(server.Close) + + client := NewClient(server.URL, HelmCreds{InsecureSkipVerify: true}, true, "", "") + + tags, err := client.GetTags("mychart", true) + require.NoError(t, err) + assert.Equal(t, []string{"1.0.0"}, tags) + + // Verify that at least one request used HTTP/2. When ForceAttemptHTTP2 is + // not set on the Transport, Go's TLS stack won't negotiate h2 even though + // the server supports it, because a custom TLSClientConfig disables the + // automatic HTTP/2 setup. + require.NotEmpty(t, requestProtos, "expected at least one request to the server") + hasHTTP2 := slices.Contains(requestProtos, "HTTP/2.0") + assert.True(t, hasHTTP2, "expected at least one HTTP/2 request, but got protocols: %v", requestProtos) + }) +} + +func TestLoadRepoIndexUsesHTTP2(t *testing.T) { + t.Run("should negotiate HTTP/2 when fetching index", func(t *testing.T) { + var requestProto string + server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requestProto = r.Proto + t.Logf("called %s with proto %s", r.URL.Path, r.Proto) + + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`apiVersion: v1 +entries: {} +`)) + })) + server.TLS = &tls.Config{NextProtos: []string{"h2", "http/1.1"}} + server.StartTLS() + t.Cleanup(server.Close) + + client := NewClient(server.URL, HelmCreds{InsecureSkipVerify: true}, false, "", "") + + _, err := client.GetIndex(false, 10000) + require.NoError(t, err) + + assert.Equal(t, "HTTP/2.0", requestProto, "expected HTTP/2 request for index fetch, but got %s", requestProto) + }) +} + func TestUserAgentIsSet(t *testing.T) { t.Run("Default User-Agent for traditional Helm repo", func(t *testing.T) { // Create a test server that captures the User-Agent header diff --git a/util/oci/client.go b/util/oci/client.go index 256aab4a19..37c029d330 100644 --- a/util/oci/client.go +++ b/util/oci/client.go @@ -143,6 +143,7 @@ func NewClientWithLock(repoURL string, creds Creds, repoLock sync.KeyLock, proxy Proxy: proxy.GetCallback(proxyURL, noProxy), TLSClientConfig: tlsConf, DisableKeepAlives: true, + ForceAttemptHTTP2: true, }, /* CheckRedirect: func(req *http.Request, via []*http.Request) error { diff --git a/util/oci/client_test.go b/util/oci/client_test.go index 0982cb99fd..98f523fdfd 100644 --- a/util/oci/client_test.go +++ b/util/oci/client_test.go @@ -5,16 +5,22 @@ import ( "bytes" "compress/gzip" "context" + "crypto/tls" "encoding/json" "errors" "io" + "net/http" + "net/http/httptest" + "net/url" "os" "path/filepath" + "slices" "testing" "github.com/opencontainers/go-digest" "github.com/opencontainers/image-spec/specs-go" imagev1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "oras.land/oras-go/v2" "oras.land/oras-go/v2/content" @@ -761,6 +767,38 @@ func Test_nativeOCIClient_ResolveRevision(t *testing.T) { } } +func TestNewClientUsesHTTP2(t *testing.T) { + t.Run("should negotiate HTTP/2 when TLS is configured", func(t *testing.T) { + var requestProtos []string + server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requestProtos = append(requestProtos, r.Proto) + t.Logf("called %s with proto %s", r.URL.Path, r.Proto) + w.WriteHeader(http.StatusOK) + })) + // httptest.NewTLSServer only advertises http/1.1 in ALPN, so we must + // configure the server to also offer h2 for HTTP/2 negotiation to work. + server.TLS = &tls.Config{NextProtos: []string{"h2", "http/1.1"}} + server.StartTLS() + t.Cleanup(server.Close) + + serverURL, err := url.Parse(server.URL) + require.NoError(t, err) + + // NewClient expects oci://host/path format. + repoURL := "oci://" + serverURL.Host + "/myorg/myrepo" + client, err := NewClient(repoURL, Creds{InsecureSkipVerify: true}, "", "", nil, + WithEventHandlers(fakeEventHandlers(t, serverURL.Host+"/myorg/myrepo"))) + require.NoError(t, err) + + // TestRepo pings the registry's /v2/ endpoint, exercising the transport. + _, _ = client.TestRepo(t.Context()) + + require.NotEmpty(t, requestProtos, "expected at least one request to the server") + hasHTTP2 := slices.Contains(requestProtos, "HTTP/2.0") + assert.True(t, hasHTTP2, "expected at least one HTTP/2 request, but got protocols: %v", requestProtos) + }) +} + func fakeEventHandlers(t *testing.T, repoURL string) EventHandlers { t.Helper() return EventHandlers{ @@ -772,6 +810,9 @@ func fakeEventHandlers(t *testing.T, repoURL string) EventHandlers { OnGetTagsFail: func(repo string) func() { return func() { require.Equal(t, repoURL, repo) } }, + OnTestRepoFail: func(repo string) func() { + return func() { require.Equal(t, repoURL, repo) } + }, OnExtractFail: func(repo string) func(revision string) { return func(_ string) { require.Equal(t, repoURL, repo) } },