fix: force attempt http2 with custom tls config (#26975) (#26976)

Signed-off-by: Max Verbeek <m4xv3rb33k@gmail.com>
This commit is contained in:
Max Verbeek 2026-03-30 16:39:56 +02:00 committed by GitHub
parent 0191c1684d
commit 1042e12c6a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 108 additions and 0 deletions

View file

@ -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

View file

@ -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

View file

@ -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 {

View file

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