From 25bb4dfecbe6d62a7803bf647b6f2b98baf1254f Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Sat, 4 Jul 2026 07:25:23 -0700 Subject: [PATCH 1/2] fix(registry): set default HTTP client timeout for registry client The default http.Client had no Timeout, so an unresponsive registry could hang helm registry operations indefinitely. Signed-off-by: Sebastien Tardif --- pkg/registry/client.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/registry/client.go b/pkg/registry/client.go index 4d7da78dc..4e9c8da08 100644 --- a/pkg/registry/client.go +++ b/pkg/registry/client.go @@ -30,6 +30,7 @@ import ( "os" "sort" "strings" + "time" "github.com/Masterminds/semver/v3" "github.com/opencontainers/image-spec/specs-go" @@ -97,6 +98,8 @@ func NewClient(options ...ClientOption) (*Client, error) { } if client.httpClient == nil { client.httpClient = &http.Client{ + // Bound registry operations so an unresponsive registry cannot hang forever. + Timeout: 30 * time.Second, Transport: NewTransport(client.debug), } } From 35fddfcede46c3ccebfba95ec7117182d869756d Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Sat, 4 Jul 2026 07:41:38 -0700 Subject: [PATCH 2/2] fix(registry): apply default timeout to custom clients with zero Timeout When NewClient is given a custom *http.Client with Timeout left at zero, apply the same 30s default used for the built-in client (clone so the caller-owned client is not mutated). Preserve explicit non-zero timeouts. Add unit tests for default, zero-timeout custom, and explicit override. Signed-off-by: Sebastien Tardif --- pkg/registry/client.go | 13 +++++++++-- pkg/registry/client_test.go | 44 +++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/pkg/registry/client.go b/pkg/registry/client.go index 4e9c8da08..045904c13 100644 --- a/pkg/registry/client.go +++ b/pkg/registry/client.go @@ -96,12 +96,21 @@ func NewClient(options ...ClientOption) (*Client, error) { if client.credentialsFile == "" { client.credentialsFile = helmpath.ConfigPath(CredentialsFileBasename) } + // Bound registry operations so an unresponsive registry cannot hang forever. + // Apply when no client was provided, or when a custom client left Timeout at the + // zero value (no timeout). Callers that need an unlimited client can set a large + // positive Timeout explicitly. + const defaultHTTPClientTimeout = 30 * time.Second if client.httpClient == nil { client.httpClient = &http.Client{ - // Bound registry operations so an unresponsive registry cannot hang forever. - Timeout: 30 * time.Second, + Timeout: defaultHTTPClientTimeout, Transport: NewTransport(client.debug), } + } else if client.httpClient.Timeout == 0 { + // Clone so we do not mutate a caller-owned *http.Client. + httpClient := *client.httpClient + httpClient.Timeout = defaultHTTPClientTimeout + client.httpClient = &httpClient } storeOptions := credentials.StoreOptions{ diff --git a/pkg/registry/client_test.go b/pkg/registry/client_test.go index 97b7c7c4c..88ea7eef0 100644 --- a/pkg/registry/client_test.go +++ b/pkg/registry/client_test.go @@ -23,6 +23,7 @@ import ( "path/filepath" "strings" "testing" + "time" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/stretchr/testify/assert" @@ -153,3 +154,46 @@ func TestWarnIfHostHasPath(t *testing.T) { }) } } + +func TestNewClient_DefaultHTTPClientTimeout(t *testing.T) { + t.Parallel() + + c, err := NewClient(ClientOptWriter(io.Discard)) + require.NoError(t, err) + require.NotNil(t, c.httpClient) + assert.Equal(t, 30*time.Second, c.httpClient.Timeout) +} + +func TestNewClient_AppliesDefaultTimeoutToCustomClientWithZeroTimeout(t *testing.T) { + t.Parallel() + + custom := &http.Client{ + Transport: http.DefaultTransport, + // Timeout left at zero (no timeout) + } + c, err := NewClient( + ClientOptWriter(io.Discard), + ClientOptHTTPClient(custom), + ) + require.NoError(t, err) + require.NotNil(t, c.httpClient) + assert.Equal(t, 30*time.Second, c.httpClient.Timeout) + // Caller-owned client must not be mutated. + assert.Equal(t, time.Duration(0), custom.Timeout) +} + +func TestNewClient_PreservesExplicitHTTPClientTimeout(t *testing.T) { + t.Parallel() + + custom := &http.Client{ + Timeout: 5 * time.Second, + Transport: http.DefaultTransport, + } + c, err := NewClient( + ClientOptWriter(io.Discard), + ClientOptHTTPClient(custom), + ) + require.NoError(t, err) + require.NotNil(t, c.httpClient) + assert.Equal(t, 5*time.Second, c.httpClient.Timeout) +}