From 24a980a0983d06898100fac02311e73e5ad4d2ed Mon Sep 17 00:00:00 2001 From: Rafal Prasal Date: Mon, 27 Jul 2026 13:37:51 +0200 Subject: [PATCH] feature/http-gzip-index.yaml Signed-off-by: Rafal Prasal --- pkg/getter/httpgetter.go | 40 ++++++++++++++++++++++++++++++++- pkg/repo/v1/chartrepo_test.go | 42 +++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/pkg/getter/httpgetter.go b/pkg/getter/httpgetter.go index a72a9337a..351fa6086 100644 --- a/pkg/getter/httpgetter.go +++ b/pkg/getter/httpgetter.go @@ -17,6 +17,7 @@ package getter import ( "bytes" + "compress/gzip" "context" "crypto/tls" "fmt" @@ -24,6 +25,7 @@ import ( "log/slog" "net/http" "net/url" + "strings" "sync" "helm.sh/helm/v4/internal/tlsutil" @@ -55,9 +57,14 @@ func (g *HTTPGetter) get(href string, opts getterOptions) (*bytes.Buffer, error) return nil, err } + isRepositoryIndexRequest := isRepositoryIndexRequestURL(href) + if opts.acceptHeader != "" { req.Header.Set("Accept", opts.acceptHeader) } + if isRepositoryIndexRequest { + req.Header.Set("Accept-Encoding", "gzip") + } req.Header.Set("User-Agent", version.GetUserAgent()) if opts.userAgent != "" { @@ -100,11 +107,42 @@ func (g *HTTPGetter) get(href string, opts getterOptions) (*bytes.Buffer, error) return nil, fmt.Errorf("failed to fetch %s : %s", href, resp.Status) } + reader := io.Reader(resp.Body) + if isRepositoryIndexRequest && isGzipEncoded(resp.Header) { + gzipReader, err := gzip.NewReader(resp.Body) + if err != nil { + return nil, err + } + defer gzipReader.Close() + reader = gzipReader + } + buf := bytes.NewBuffer(nil) - _, err = io.Copy(buf, resp.Body) + _, err = io.Copy(buf, reader) return buf, err } +func isRepositoryIndexRequestURL(href string) bool { + u, err := url.Parse(href) + if err != nil { + return false + } + + return u.Path == "index.yaml" || strings.HasSuffix(u.Path, "/index.yaml") +} + +func isGzipEncoded(header http.Header) bool { + for _, value := range header.Values("Content-Encoding") { + for _, encoding := range strings.Split(value, ",") { + if strings.EqualFold(strings.TrimSpace(encoding), "gzip") { + return true + } + } + } + + return false +} + // NewHTTPGetter constructs a valid http/https client as a Getter func NewHTTPGetter(options ...Option) (Getter, error) { var client HTTPGetter diff --git a/pkg/repo/v1/chartrepo_test.go b/pkg/repo/v1/chartrepo_test.go index f0e5839ac..e658a43db 100644 --- a/pkg/repo/v1/chartrepo_test.go +++ b/pkg/repo/v1/chartrepo_test.go @@ -18,6 +18,7 @@ package repo import ( "bytes" + "compress/gzip" "net/http" "net/http/httptest" "os" @@ -124,6 +125,47 @@ func TestConcurrencyDownloadIndex(t *testing.T) { wg.Wait() } +func TestDownloadIndexFileWithGzipContentEncoding(t *testing.T) { + fileBytes, err := os.ReadFile("testdata/local-index.yaml") + require.NoError(t, err) + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "gzip", r.Header.Get("Accept-Encoding")) + + w.Header().Set("Content-Encoding", "gzip") + gzipWriter := gzip.NewWriter(w) + defer gzipWriter.Close() + + _, err := gzipWriter.Write(fileBytes) + require.NoError(t, err) + }) + + srv, err := startLocalServerForTests(handler) + require.NoError(t, err) + defer srv.Close() + + r, err := NewChartRepository(&Entry{ + Name: testRepo, + URL: srv.URL, + }, getter.All(&cli.EnvSettings{})) + require.NoError(t, err) + + idx, err := r.DownloadIndexFile() + require.NoError(t, err) + + i, err := LoadIndexFile(idx) + require.NoError(t, err) + verifyLocalIndex(t, i) + + writtenIndex, err := os.ReadFile(idx) + require.NoError(t, err) + assert.Equal(t, fileBytes, writtenIndex) + assert.NotContains(t, string(writtenIndex), "\x1f\x8b") + assert.NotEmpty(t, writtenIndex) + assert.Equal(t, filepath.Base(idx), helmpath.CacheIndexFile(r.Config.Name)) + assert.Equal(t, filepath.Dir(idx), r.CachePath) +} + // startLocalServerForTests Start the local helm server func startLocalServerForTests(handler http.Handler) (*httptest.Server, error) { if handler == nil {