From 2ca0b81292c087ac2a2eef6360c48c63a3e96263 Mon Sep 17 00:00:00 2001 From: thelovekesh Date: Mon, 1 Jun 2026 15:30:24 +0530 Subject: [PATCH 01/14] Update put package API --- pkg/pm/registry/client.go | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/pkg/pm/registry/client.go b/pkg/pm/registry/client.go index 9081c7e..7e0193b 100644 --- a/pkg/pm/registry/client.go +++ b/pkg/pm/registry/client.go @@ -1,9 +1,11 @@ package registry import ( + "bytes" "context" - "encoding/base64" + "encoding/binary" "encoding/json" + "fmt" "io" "net/http" @@ -13,7 +15,7 @@ import ( ) const ( - wpmManifestHeader = "X-WPM-Manifest" + maxManifestSize = 512 * 1024 // 512KB contentTypeOctetStream = "application/octet-stream" wpmContentTypeManifestV1 = "application/vnd.wpm.install-v1+json" ) @@ -62,14 +64,27 @@ func (c *client) PutPackage(ctx context.Context, data *manifest.Package, tarball return err } + manifestLen := len(manifestBytes) + if manifestLen > maxManifestSize { + return fmt.Errorf("manifest size exceeds %d bytes, refusing to continue", maxManifestSize) + } + + lengthBytes := make([]byte, 4) + binary.BigEndian.PutUint32(lengthBytes, uint32(manifestLen)) + + bodyReader := io.MultiReader( + bytes.NewReader(lengthBytes), + bytes.NewReader(manifestBytes), + tarball, + ) + return c.restClient.DoWithContext( ctx, http.MethodPut, - "/", - tarball, + "/"+data.Name+"/"+data.Version, + bodyReader, nil, api.WithHeader(api.HeaderContentType, contentTypeOctetStream), - api.WithHeader(wpmManifestHeader, base64.StdEncoding.EncodeToString(manifestBytes)), ) } From ac005fc5cad362dcc530ef79c62dc6527b536925 Mon Sep 17 00:00:00 2001 From: thelovekesh Date: Mon, 1 Jun 2026 15:35:47 +0530 Subject: [PATCH 02/14] Bump readme size to 100KB --- cli/command/publish/publish.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/command/publish/publish.go b/cli/command/publish/publish.go index a002c5d..f73329b 100644 --- a/cli/command/publish/publish.go +++ b/cli/command/publish/publish.go @@ -31,7 +31,7 @@ import ( ) const ( - maxReadmeSize = 50 * 1024 // 50KB + maxReadmeSize = 100 * 1024 // 100KB maxPackedSize int64 = 128 * 1024 * 1024 // 128MB ) @@ -118,7 +118,7 @@ func getReadme(dirPath string) (string, error) { _ = f.Close() }() - // Limit readme size to maxReadmeSize i.e. 50KB + // Limit readme size to maxReadmeSize. data, err := io.ReadAll(io.LimitReader(f, maxReadmeSize)) if err != nil { return "", err From ba6f11fe075abc86374772c194bf34c135edb02d Mon Sep 17 00:00:00 2001 From: thelovekesh Date: Mon, 1 Jun 2026 20:28:33 +0530 Subject: [PATCH 03/14] Add request opt to set content length --- pkg/api/rest_client.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/api/rest_client.go b/pkg/api/rest_client.go index 70cbed2..6782bae 100644 --- a/pkg/api/rest_client.go +++ b/pkg/api/rest_client.go @@ -49,6 +49,12 @@ func WithHeader(key, value string) RequestOption { } } +func WithContentLength(length int64) RequestOption { + return func(req *http.Request) { + req.ContentLength = length + } +} + func (c *RESTClient) DoWithContext(ctx context.Context, method, path string, body io.Reader, response any, opts ...RequestOption) error { url := restURL(c.host, path) req, err := http.NewRequestWithContext(ctx, method, url, body) From e4a79286c5ae7316041364525b86cda34dcd8ff2 Mon Sep 17 00:00:00 2001 From: thelovekesh Date: Mon, 1 Jun 2026 20:51:12 +0530 Subject: [PATCH 04/14] Add content length in package upload request --- pkg/pm/registry/client.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/pm/registry/client.go b/pkg/pm/registry/client.go index 7e0193b..32a0507 100644 --- a/pkg/pm/registry/client.go +++ b/pkg/pm/registry/client.go @@ -78,6 +78,8 @@ func (c *client) PutPackage(ctx context.Context, data *manifest.Package, tarball tarball, ) + totalContentLength := int64(4+manifestLen) + data.Dist.PackedSize + return c.restClient.DoWithContext( ctx, http.MethodPut, @@ -85,6 +87,7 @@ func (c *client) PutPackage(ctx context.Context, data *manifest.Package, tarball bodyReader, nil, api.WithHeader(api.HeaderContentType, contentTypeOctetStream), + api.WithContentLength(totalContentLength), ) } From 1ee7760e517bb0f23d0b442b16db49a0f398c5bf Mon Sep 17 00:00:00 2001 From: thelovekesh Date: Mon, 1 Jun 2026 21:01:43 +0530 Subject: [PATCH 05/14] Fix handling of schema in registry host --- pkg/api/rest_client.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/api/rest_client.go b/pkg/api/rest_client.go index 6782bae..6d6d66a 100644 --- a/pkg/api/rest_client.go +++ b/pkg/api/rest_client.go @@ -146,6 +146,9 @@ func restURL(hostname, pathOrURL string) string { if !strings.HasPrefix(pathOrURL, "/") { pathOrURL = "/" + pathOrURL } + if strings.HasPrefix(hostname, "https://") || strings.HasPrefix(hostname, "http://") { + return strings.TrimRight(hostname, "/") + pathOrURL + } return restPrefix(hostname) + pathOrURL } From cd44b167b38b3153c42ddbb9e3072a5a0b7dad79 Mon Sep 17 00:00:00 2001 From: thelovekesh Date: Mon, 1 Jun 2026 21:16:34 +0530 Subject: [PATCH 06/14] Reduce max manifest size to 256KB --- pkg/pm/registry/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/pm/registry/client.go b/pkg/pm/registry/client.go index 32a0507..25d9815 100644 --- a/pkg/pm/registry/client.go +++ b/pkg/pm/registry/client.go @@ -15,7 +15,7 @@ import ( ) const ( - maxManifestSize = 512 * 1024 // 512KB + maxManifestSize = 256 * 1024 // 256KB contentTypeOctetStream = "application/octet-stream" wpmContentTypeManifestV1 = "application/vnd.wpm.install-v1+json" ) From 3a61ecc67879a231ca6023d856b2ce4dfb7079ea Mon Sep 17 00:00:00 2001 From: thelovekesh Date: Mon, 1 Jun 2026 21:24:14 +0530 Subject: [PATCH 07/14] Add Expect header const --- pkg/api/http_client.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/api/http_client.go b/pkg/api/http_client.go index 1d09eb6..38d7248 100644 --- a/pkg/api/http_client.go +++ b/pkg/api/http_client.go @@ -34,6 +34,7 @@ const ( HeaderAccept = "Accept" HeaderAuthorization = "Authorization" HeaderUserAgent = "User-Agent" + HeaderExpect = "Expect" // header values CacheHit = "HIT" From a26c66ff99395f85fa39f3399da95de87fc2a7e7 Mon Sep 17 00:00:00 2001 From: thelovekesh Date: Mon, 1 Jun 2026 21:24:30 +0530 Subject: [PATCH 08/14] Add `100 Continue` to publish request --- pkg/pm/registry/client.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/pm/registry/client.go b/pkg/pm/registry/client.go index 25d9815..a2f55a6 100644 --- a/pkg/pm/registry/client.go +++ b/pkg/pm/registry/client.go @@ -15,6 +15,7 @@ import ( ) const ( + continue100 = "100-continue" maxManifestSize = 256 * 1024 // 256KB contentTypeOctetStream = "application/octet-stream" wpmContentTypeManifestV1 = "application/vnd.wpm.install-v1+json" @@ -86,6 +87,7 @@ func (c *client) PutPackage(ctx context.Context, data *manifest.Package, tarball "/"+data.Name+"/"+data.Version, bodyReader, nil, + api.WithHeader(api.HeaderExpect, continue100), api.WithHeader(api.HeaderContentType, contentTypeOctetStream), api.WithContentLength(totalContentLength), ) From 1d4ecaa45f021f285c786c08558038e3e052943a Mon Sep 17 00:00:00 2001 From: thelovekesh Date: Wed, 3 Jun 2026 14:22:16 +0530 Subject: [PATCH 09/14] Remove redundant helpers --- pkg/api/client_options.go | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/pkg/api/client_options.go b/pkg/api/client_options.go index 44ee27a..093a006 100644 --- a/pkg/api/client_options.go +++ b/pkg/api/client_options.go @@ -1,7 +1,6 @@ package api import ( - "errors" "io" "time" ) @@ -42,23 +41,3 @@ type ClientOptions struct { // CacheDir specifies a directory to use for caching GET requests. CacheDir string } - -func optionsNeedResolution(opts ClientOptions) bool { - if opts.Host == "" { - return true - } - - if opts.AuthToken == "" { - return true - } - - return false -} - -func resolveOptions(opts ClientOptions) (ClientOptions, error) { - if opts.Host == "" { - return ClientOptions{}, errors.New("host not found") - } - - return opts, nil -} From 1d17c4113a2e6e556160310f5c0e6b2030598c73 Mon Sep 17 00:00:00 2001 From: thelovekesh Date: Wed, 3 Jun 2026 14:22:31 +0530 Subject: [PATCH 10/14] Show errors verbatim --- pkg/api/errors.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/api/errors.go b/pkg/api/errors.go index 27dcb13..ad1701f 100644 --- a/pkg/api/errors.go +++ b/pkg/api/errors.go @@ -24,7 +24,7 @@ func (err *HTTPError) Error() string { if err.Message == "" { return "wpm registry error: " + strings.ToLower(http.StatusText(err.StatusCode)) } - return "wpm registry error: " + strings.ToLower(err.Message) + return "wpm registry error: " + err.Message } // HandleHTTPError parses a http.Response into a HTTPError. The response body From a6ad304ec2ec4ec988a745b68d8ac46274c437ff Mon Sep 17 00:00:00 2001 From: thelovekesh Date: Wed, 3 Jun 2026 15:16:44 +0530 Subject: [PATCH 11/14] Fix host and paths handling in http client --- pkg/api/http_client.go | 135 +++++++++++++++++++++++++++++++++-------- pkg/api/rest_client.go | 100 +++++++++++++++--------------- 2 files changed, 158 insertions(+), 77 deletions(-) diff --git a/pkg/api/http_client.go b/pkg/api/http_client.go index 38d7248..33345e4 100644 --- a/pkg/api/http_client.go +++ b/pkg/api/http_client.go @@ -1,9 +1,12 @@ package api import ( + "errors" "fmt" "io" + "net" "net/http" + "net/url" "regexp" "strings" "sync" @@ -29,9 +32,11 @@ const ( HeaderIfModifiedSince = "If-Modified-Since" HeaderCacheRevalidate = "X-Cache-Revalidate" HeaderContentEncoding = "Content-Encoding" + HeaderContentLength = "Content-Length" HeaderContentType = "Content-Type" HeaderCacheControl = "Cache-Control" HeaderAccept = "Accept" + HeaderAcceptEncoding = "Accept-Encoding" HeaderAuthorization = "Authorization" HeaderUserAgent = "User-Agent" HeaderExpect = "Expect" @@ -39,6 +44,8 @@ const ( // header values CacheHit = "HIT" CacheMiss = "MISS" + + encodingZstd = "zstd" ) var ( @@ -55,12 +62,15 @@ var ( ) func NewHTTPClient(opts ClientOptions) (*http.Client, error) { - if optionsNeedResolution(opts) { - var err error - opts, err = resolveOptions(opts) - if err != nil { - return nil, err - } + base, err := normalizeBaseURL(opts.Host) + if err != nil { + return nil, err + } + host := base.Hostname() + + allowToken := base.Scheme == "https" || isLoopbackHost(host) + if opts.AuthToken != "" && !allowToken { + return nil, fmt.Errorf("refusing to send auth token over http to %q: use https", base.Host) } // Sweep stale cache tmp files left behind by aborted requests. Runs in @@ -73,11 +83,19 @@ func NewHTTPClient(opts ClientOptions) (*http.Client, error) { transport := &Transport{ Base: &http.Transport{ - MaxIdleConns: 100, - MaxIdleConnsPerHost: 100, - IdleConnTimeout: 90 * time.Second, - ForceAttemptHTTP2: true, - DisableCompression: true, + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).DialContext, + MaxIdleConns: 100, + MaxIdleConnsPerHost: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ResponseHeaderTimeout: 30 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + ForceAttemptHTTP2: true, + DisableCompression: true, }, cacheDir: opts.CacheDir, } @@ -92,7 +110,7 @@ func NewHTTPClient(opts ClientOptions) (*http.Client, error) { var rt http.RoundTripper = transport - rt = newHeaderRoundTripper(opts.Host, opts.AuthToken, opts.Headers, rt) + rt = newHeaderRoundTripper(host, allowToken, opts.AuthToken, opts.Headers, rt) rt = newDecompressingRoundTripper(rt) rt = newSanitizerRoundTripper(rt) @@ -116,13 +134,65 @@ func NewHTTPClient(opts ClientOptions) (*http.Client, error) { rt = logger.RoundTripper(rt) } - return &http.Client{Transport: rt, Timeout: opts.Timeout}, nil + return &http.Client{ + Transport: rt, + Timeout: opts.Timeout, + CheckRedirect: func(req *http.Request, _ []*http.Request) error { + if !isSameDomain(req.URL.Hostname(), host) { + return fmt.Errorf("refusing redirect to %q outside registry host %q", req.URL.Host, host) + } + return nil + }, + }, nil } func inspectableMIMEType(t string) bool { return jsonTypeRE.MatchString(t) } +// normalizeBaseURL canonicalizes a configured registry host into a base URL +// with an explicit scheme. A bare host (no scheme) defaults to https so we +// never silently fall back to cleartext. +func normalizeBaseURL(host string) (*url.URL, error) { + host = strings.TrimSpace(host) + if host == "" { + return nil, errors.New("registry host not configured") + } + + lc := strings.ToLower(host) + if !strings.HasPrefix(lc, "http://") && !strings.HasPrefix(lc, "https://") { + host = "https://" + host + } + + u, err := url.Parse(host) + if err != nil { + return nil, fmt.Errorf("invalid registry host %q: %w", host, err) + } + if u.Hostname() == "" { + return nil, fmt.Errorf("invalid registry host %q: missing hostname", host) + } + if u.Scheme != "http" && u.Scheme != "https" { + return nil, fmt.Errorf("unsupported registry scheme %q, only http and https are supported", u.Scheme) + } + + u.Fragment = "" + u.RawQuery = "" + u.Path = strings.TrimRight(u.Path, "/") + return u, nil +} + +// isLoopbackHost reports whether host refers to the local machine, where +// cleartext traffic never touches the network. +func isLoopbackHost(host string) bool { + if strings.EqualFold(host, "localhost") { + return true + } + if ip := net.ParseIP(host); ip != nil { + return ip.IsLoopback() + } + return false +} + func isSameDomain(requestHost, domain string) bool { requestHost = strings.ToLower(requestHost) domain = strings.ToLower(domain) @@ -130,9 +200,10 @@ func isSameDomain(requestHost, domain string) bool { } type headerRoundTripper struct { - headers map[string]string - host string - rt http.RoundTripper + headers map[string]string + host string + allowToken bool + rt http.RoundTripper } func resolveHeaders(headers map[string]string) { @@ -147,29 +218,43 @@ func resolveHeaders(headers map[string]string) { } } -func newHeaderRoundTripper(host, authToken string, headers map[string]string, rt http.RoundTripper) http.RoundTripper { +func newHeaderRoundTripper(host string, allowToken bool, authToken string, headers map[string]string, rt http.RoundTripper) http.RoundTripper { if _, ok := headers[HeaderAuthorization]; !ok && authToken != "" { headers[HeaderAuthorization] = "Bearer " + authToken } if len(headers) == 0 { - return headerRoundTripper{host: host, headers: nil, rt: rt} + headers = nil } - return headerRoundTripper{host: host, headers: headers, rt: rt} + return headerRoundTripper{host: host, allowToken: allowToken, headers: headers, rt: rt} } func (hrt headerRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { reqCopy := req.Clone(req.Context()) - reqCopy.Header.Set("Accept-Encoding", "zstd") + reqCopy.Header.Set(HeaderAcceptEncoding, encodingZstd) for k, v := range hrt.headers { - if k == HeaderAuthorization && !isSameDomain(reqCopy.URL.Hostname(), hrt.host) { - continue + if k == HeaderAuthorization { + continue // handled separately below } if reqCopy.Header.Get(k) == "" { reqCopy.Header.Set(k, v) } } + // The auth token only travels to the configured registry host over an + // allowed transport. On anything else (cross-host redirect, plaintext to a + // remote host) strip it, including any token a caller set per-request, so + // the credential cannot leak. + if hrt.allowToken && isSameDomain(reqCopy.URL.Hostname(), hrt.host) { + if reqCopy.Header.Get(HeaderAuthorization) == "" { + if token := hrt.headers[HeaderAuthorization]; token != "" { + reqCopy.Header.Set(HeaderAuthorization, token) + } + } + } else { + reqCopy.Header.Del(HeaderAuthorization) + } + return hrt.rt.RoundTrip(reqCopy) } @@ -215,7 +300,7 @@ func (d decompressingRoundTripper) RoundTrip(req *http.Request) (*http.Response, return nil, err } - if resp.Header.Get("Content-Encoding") == "zstd" { + if resp.Header.Get(HeaderContentEncoding) == encodingZstd { decoder := zstdDecoderPool.Get().(*zstd.Decoder) if err := decoder.Reset(resp.Body); err != nil { _ = resp.Body.Close() @@ -229,8 +314,8 @@ func (d decompressingRoundTripper) RoundTrip(req *http.Request) (*http.Response, Decoder: decoder, OriginalBody: resp.Body, } - resp.Header.Del("Content-Encoding") - resp.Header.Del("Content-Length") + resp.Header.Del(HeaderContentLength) + resp.Header.Del(HeaderContentEncoding) resp.ContentLength = -1 } diff --git a/pkg/api/rest_client.go b/pkg/api/rest_client.go index 6d6d66a..6e1b3f5 100644 --- a/pkg/api/rest_client.go +++ b/pkg/api/rest_client.go @@ -3,29 +3,24 @@ package api import ( "context" "encoding/json" + "fmt" "io" "net/http" + "net/url" "strings" - - "go.wpm.so/cli/pkg/unsafeconv" ) -type RESTClient struct { - client *http.Client - host string -} +const maxResponseBodySize = 4 << 20 // 4 MiB -func DefaultRESTClient() (*RESTClient, error) { - return NewRESTClient(ClientOptions{}) +type RESTClient struct { + client *http.Client + baseURL *url.URL } func NewRESTClient(opts ClientOptions) (*RESTClient, error) { - if optionsNeedResolution(opts) { - var err error - opts, err = resolveOptions(opts) - if err != nil { - return nil, err - } + base, err := normalizeBaseURL(opts.Host) + if err != nil { + return nil, err } client, err := NewHTTPClient(opts) @@ -33,11 +28,9 @@ func NewRESTClient(opts ClientOptions) (*RESTClient, error) { return nil, err } - host := strings.TrimRight(opts.Host, "/") - return &RESTClient{ - client: client, - host: host, + baseURL: base, + client: client, }, nil } @@ -56,16 +49,11 @@ func WithContentLength(length int64) RequestOption { } func (c *RESTClient) DoWithContext(ctx context.Context, method, path string, body io.Reader, response any, opts ...RequestOption) error { - url := restURL(c.host, path) - req, err := http.NewRequestWithContext(ctx, method, url, body) + req, err := c.newRequest(ctx, method, path, body, opts...) if err != nil { return err } - for _, opt := range opts { - opt(req) - } - resp, err := c.client.Do(req) if err != nil { return err @@ -85,35 +73,30 @@ func (c *RESTClient) DoWithContext(ctx context.Context, method, path string, bod switch v := response.(type) { case *string: - var b []byte - b, err = io.ReadAll(resp.Body) - if err == nil { - *v = unsafeconv.UnsafeBytesToString(b) + b, rErr := readAtMost(resp.Body, maxResponseBodySize) + if rErr != nil { + return rErr } - return err + *v = string(b) + return nil case *[]byte: - var b []byte - b, err = io.ReadAll(resp.Body) - if err == nil { - *v = b + b, rErr := readAtMost(resp.Body, maxResponseBodySize) + if rErr != nil { + return rErr } - return err + *v = b + return nil default: - return json.NewDecoder(resp.Body).Decode(v) + return json.NewDecoder(io.LimitReader(resp.Body, maxResponseBodySize)).Decode(v) } } func (c *RESTClient) RequestStream(ctx context.Context, method, path string, body io.Reader, opts ...RequestOption) (io.ReadCloser, error) { - url := restURL(c.host, path) - req, err := http.NewRequestWithContext(ctx, method, url, body) + req, err := c.newRequest(ctx, method, path, body, opts...) if err != nil { return nil, err } - for _, opt := range opts { - opt(req) - } - resp, err := c.client.Do(req) if err != nil { return nil, err @@ -139,19 +122,32 @@ func drainBody(body io.Reader) { _, _ = io.Copy(io.Discard, io.LimitReader(body, 64<<10)) } -func restURL(hostname, pathOrURL string) string { - if strings.HasPrefix(pathOrURL, "https://") || strings.HasPrefix(pathOrURL, "http://") { - return pathOrURL - } - if !strings.HasPrefix(pathOrURL, "/") { - pathOrURL = "/" + pathOrURL +// readAtMost reads limited bytes and returns an error if the limit +// is exceeded, to prevent OOM from hostile or buggy servers. +func readAtMost(r io.Reader, limit int64) ([]byte, error) { + b, err := io.ReadAll(io.LimitReader(r, limit+1)) + if err != nil { + return nil, err } - if strings.HasPrefix(hostname, "https://") || strings.HasPrefix(hostname, "http://") { - return strings.TrimRight(hostname, "/") + pathOrURL + if int64(len(b)) > limit { + return nil, fmt.Errorf("response body exceeds %d byte limit", limit) } - return restPrefix(hostname) + pathOrURL + return b, nil } -func restPrefix(hostname string) string { - return "https://" + hostname +func (c *RESTClient) newRequest(ctx context.Context, method, path string, body io.Reader, opts ...RequestOption) (*http.Request, error) { + if strings.Contains(path, "://") { + return nil, fmt.Errorf("path %q must be relative, not an absolute URL", path) + } + + req, err := http.NewRequestWithContext(ctx, method, c.baseURL.JoinPath(path).String(), body) + if err != nil { + return nil, err + } + + for _, opt := range opts { + opt(req) + } + + return req, nil } From f1997fa54d0c2a20977632359a555c465697b659 Mon Sep 17 00:00:00 2001 From: thelovekesh Date: Wed, 3 Jun 2026 15:23:48 +0530 Subject: [PATCH 12/14] Update cloning requests with shallow copy --- pkg/api/cache.go | 4 +++- pkg/api/http_client.go | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pkg/api/cache.go b/pkg/api/cache.go index e43dd7d..b6ad9ff 100644 --- a/pkg/api/cache.go +++ b/pkg/api/cache.go @@ -69,7 +69,9 @@ func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { return t.base().RoundTrip(req) } - outReq := req.Clone(req.Context()) + reqCopy := *req + reqCopy.Header = req.Header.Clone() + outReq := &reqCopy cache := outReq.Header.Get(HeaderSaveCache) == "true" force := outReq.Header.Get(HeaderCacheRevalidate) == "true" diff --git a/pkg/api/http_client.go b/pkg/api/http_client.go index 33345e4..6ecfe99 100644 --- a/pkg/api/http_client.go +++ b/pkg/api/http_client.go @@ -229,7 +229,8 @@ func newHeaderRoundTripper(host string, allowToken bool, authToken string, heade } func (hrt headerRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { - reqCopy := req.Clone(req.Context()) + reqCopy := *req + reqCopy.Header = req.Header.Clone() reqCopy.Header.Set(HeaderAcceptEncoding, encodingZstd) for k, v := range hrt.headers { @@ -255,7 +256,7 @@ func (hrt headerRoundTripper) RoundTrip(req *http.Request) (*http.Response, erro reqCopy.Header.Del(HeaderAuthorization) } - return hrt.rt.RoundTrip(reqCopy) + return hrt.rt.RoundTrip(&reqCopy) } type sanitizerRoundTripper struct { From 81fd8ce4cb1eb3aac8fbf8be4de241aa273613dd Mon Sep 17 00:00:00 2001 From: thelovekesh Date: Wed, 3 Jun 2026 15:42:47 +0530 Subject: [PATCH 13/14] Update position of logger rt --- pkg/api/http_client.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pkg/api/http_client.go b/pkg/api/http_client.go index 6ecfe99..1eaee8b 100644 --- a/pkg/api/http_client.go +++ b/pkg/api/http_client.go @@ -109,11 +109,8 @@ func NewHTTPClient(opts ClientOptions) (*http.Client, error) { } var rt http.RoundTripper = transport - - rt = newHeaderRoundTripper(host, allowToken, opts.AuthToken, opts.Headers, rt) rt = newDecompressingRoundTripper(rt) rt = newSanitizerRoundTripper(rt) - if opts.Log != nil && zerolog.GlobalLevel() == zerolog.DebugLevel { opts.LogVerboseHTTP = true logger := &httpretty.Logger{ @@ -133,6 +130,7 @@ func NewHTTPClient(opts ClientOptions) (*http.Client, error) { }) rt = logger.RoundTripper(rt) } + rt = newHeaderRoundTripper(host, allowToken, opts.AuthToken, opts.Headers, rt) return &http.Client{ Transport: rt, From d21aeb25e96a2faaedd69f4314617311a6b58c59 Mon Sep 17 00:00:00 2001 From: thelovekesh Date: Wed, 3 Jun 2026 16:01:53 +0530 Subject: [PATCH 14/14] Revert req shallow copy to use req.Clone --- pkg/api/cache.go | 4 +--- pkg/api/http_client.go | 5 ++--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/pkg/api/cache.go b/pkg/api/cache.go index b6ad9ff..e43dd7d 100644 --- a/pkg/api/cache.go +++ b/pkg/api/cache.go @@ -69,9 +69,7 @@ func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { return t.base().RoundTrip(req) } - reqCopy := *req - reqCopy.Header = req.Header.Clone() - outReq := &reqCopy + outReq := req.Clone(req.Context()) cache := outReq.Header.Get(HeaderSaveCache) == "true" force := outReq.Header.Get(HeaderCacheRevalidate) == "true" diff --git a/pkg/api/http_client.go b/pkg/api/http_client.go index 1eaee8b..39efa82 100644 --- a/pkg/api/http_client.go +++ b/pkg/api/http_client.go @@ -227,8 +227,7 @@ func newHeaderRoundTripper(host string, allowToken bool, authToken string, heade } func (hrt headerRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { - reqCopy := *req - reqCopy.Header = req.Header.Clone() + reqCopy := req.Clone(req.Context()) reqCopy.Header.Set(HeaderAcceptEncoding, encodingZstd) for k, v := range hrt.headers { @@ -254,7 +253,7 @@ func (hrt headerRoundTripper) RoundTrip(req *http.Request) (*http.Response, erro reqCopy.Header.Del(HeaderAuthorization) } - return hrt.rt.RoundTrip(&reqCopy) + return hrt.rt.RoundTrip(reqCopy) } type sanitizerRoundTripper struct {