From 0ef4bb0220bdd94f19a16b3ad3a51ceed9c92114 Mon Sep 17 00:00:00 2001 From: Andrew Nesbitt Date: Tue, 12 May 2026 12:29:10 +0100 Subject: [PATCH] client: WithHTTPClient and WithTransport options MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two new Option constructors so callers can plug their own HTTP client or transport without mutating fields after NewClient returns. WithHTTPClient swaps in a caller-supplied *http.Client. Useful when the application already maintains a shared connection pool with mTLS config or auth-injecting transport that the existing WithTimeout / WithMaxRetries options can't express. WithTransport replaces the underlying http.RoundTripper while preserving the Client's other settings (timeout, cookie jar, ...). Useful for stacking middleware — auth headers, request logging, external retry, rate-limit — without rewriting the default Client. Both options compose with each other and with WithUserAgent, WithTimeout, WithMaxRetries, and WithRateLimiter. --- client/client.go | 24 +++++++++++++++++++++ client/options_test.go | 49 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 client/options_test.go diff --git a/client/client.go b/client/client.go index c4cb08d..cfd8ed5 100644 --- a/client/client.go +++ b/client/client.go @@ -217,3 +217,27 @@ func NewClient(opts ...Option) *Client { } return c } + +// WithHTTPClient swaps in a caller-supplied *http.Client. Use this +// when the application has its own connection pool, mTLS config, or +// auth-injecting transport that other Options like WithTimeout can't +// express. The supplied client's Timeout, Jar, and Transport are +// preserved as-is. +func WithHTTPClient(h *http.Client) Option { + return func(c *Client) { + c.HTTPClient = h + } +} + +// WithTransport replaces the underlying http.RoundTripper without +// touching the Client's other settings (timeout, jar, etc.). Useful +// for wrapping the transport in middleware (auth, logging, retry, +// rate-limit) while keeping the rest of the configured shape intact. +func WithTransport(rt http.RoundTripper) Option { + return func(c *Client) { + if c.HTTPClient == nil { + c.HTTPClient = &http.Client{Timeout: defaultTimeout} + } + c.HTTPClient.Transport = rt + } +} diff --git a/client/options_test.go b/client/options_test.go new file mode 100644 index 0000000..32af915 --- /dev/null +++ b/client/options_test.go @@ -0,0 +1,49 @@ +package client + +import ( + "net/http" + "testing" + "time" +) + +func TestWithHTTPClient(t *testing.T) { + custom := &http.Client{Timeout: 7 * time.Second} + c := NewClient(WithHTTPClient(custom)) + if c.HTTPClient != custom { + t.Errorf("HTTPClient = %p, want %p", c.HTTPClient, custom) + } + if c.HTTPClient.Timeout != 7*time.Second { + t.Errorf("supplied Timeout not preserved: got %v", c.HTTPClient.Timeout) + } +} + +type stubTransport struct{ called bool } + +func (s *stubTransport) RoundTrip(_ *http.Request) (*http.Response, error) { + s.called = true + return nil, http.ErrServerClosed +} + +func TestWithTransport(t *testing.T) { + rt := &stubTransport{} + c := NewClient(WithTransport(rt)) + if c.HTTPClient.Transport != rt { + t.Errorf("Transport not set; got %v", c.HTTPClient.Transport) + } + // Other Client defaults should remain. + if c.HTTPClient.Timeout == 0 { + t.Error("WithTransport should preserve default timeout") + } +} + +// WithTransport over a Client whose HTTPClient was nilled out (via an +// earlier custom option) should still attach without panicking. +func TestWithTransport_NilHTTPClient(t *testing.T) { + c := DefaultClient() + c.HTTPClient = nil + rt := &stubTransport{} + WithTransport(rt)(c) + if c.HTTPClient == nil || c.HTTPClient.Transport != rt { + t.Errorf("WithTransport should backfill HTTPClient; got %+v", c.HTTPClient) + } +}