Client options¶
Every option NewClient accepts, what it defaults to, and what happens when the value
is wrong or absent. The options are ordinary functional options — there are no
configuration keys, environment variables or config files behind them (see
what httpclient does not do).
Every option at a glance¶
| Option | Default when omitted | Passing it twice |
|---|---|---|
WithTimeout(d) |
30s | last call wins |
WithMaxRedirects(n) |
10 | last call wins |
WithSensitiveHeaders(names...) |
none — no extra headers stripped | names accumulate |
WithTLSConfig(cfg) |
go/tls DefaultConfig() |
last call wins |
WithCertPool(pool) |
system trust store | last call wins |
WithTransport(rt) |
the transport from NewTransport |
last call wins |
WithRetry(cfg) |
no retry | last call wins |
WithClientMiddleware(chain) |
no chain | last call wins |
The values behind the defaults are listed in defaults and limits.
WithTimeout — the deadline on the whole exchange¶
Sets http.Client.Timeout. Default: 30 seconds.
The timeout covers the entire exchange, not a single network operation: connection setup, the TLS handshake, every redirect hop, every retry attempt, the backoff waits between retries, and reading the response body. The clock starts when the request is issued and does not reset between hops.
A zero or negative duration disables the timeout entirely — net/http applies a deadline
only when Timeout is positive. NewClient(WithTimeout(0)) therefore produces a client
that will wait indefinitely for a response, which is rarely what is wanted; use a
context.WithTimeout on each request instead if you need per-request control.
The timeout interacts badly with generous retry settings. DefaultRetryConfig() backs off
up to 30 seconds between attempts, and the default client timeout is also 30 seconds, so a
retrying request can be cancelled mid-backoff and return
context deadline exceeded (Client.Timeout exceeded while awaiting headers) rather than
the retried result. When you enable retry, set a timeout that covers
MaxRetries × MaxBackoff plus the request time, or set no client timeout and bound each
request with a context instead.
WithMaxRedirects — how many hops a redirect chain may take¶
Caps the length of a redirect chain. Default: 10, matching the standard library's own built-in limit.
n bounds the number of requests in the chain, not the number of hops beyond the first:
the policy refuses a redirect once n requests have already been made. With the default
of 10, the client issues the original request and follows nine redirects; the tenth
redirect is refused.
When the limit is reached the call fails with
stopped after N redirects, wrapped in a *url.Error. The 3xx response is returned
alongside the error, but its body has already been closed by net/http, so the
response is useful only for its status code and headers. The URL field of the
*url.Error holds the Location value that was not followed, which is a relative path
when the server sent a relative Location.
Setting n to 0 — or to any negative number — refuses every redirect, because the
policy compares the number of prior requests against n before the first hop is taken.
This is the way to stop the client following redirects, but note that it is a refusal,
not a pass-through: the call returns an error and a body-closed response, and there is no
option that makes the client hand back the raw 3xx response the way
http.ErrUseLastResponse does for a hand-built client.
WithSensitiveHeaders — strip credential headers on a cross-origin redirect¶
Names request headers that carry credentials. Default: none — the option is opt-in, and without it redirect header handling is exactly the standard library's.
On a redirect that leaves the origin of the initial request, the named headers are
removed before the hop is followed. Origin means scheme, host and port compared exactly:
https://api.example.com and https://api.example.com:443 count as different origins, so
the credential is stripped. Same-origin redirects retain the headers.
net/http already strips Authorization, Www-Authenticate, Cookie, Cookie2,
Proxy-Authorization and Proxy-Authenticate when a redirect crosses hosts. This option
exists for the headers it does not know about — PRIVATE-TOKEN, X-Api-Key and similar —
which the standard library copies to every hop.
Header names are matched case-insensitively, in the canonical form net/http stores them
in, so WithSensitiveHeaders("X-API-KEY") strips a header set with
req.Header.Set("x-api-key", …).
Repeated calls accumulate rather than replace: WithSensitiveHeaders("A") followed by
WithSensitiveHeaders("B") strips both.
Two things it does not cover:
- The first request. Stripping happens in the redirect policy, which only runs on a redirect. A header you set is always sent to the address you asked for.
- Credentials injected by middleware.
WithBearerTokenandWithBasicAuthfromgo/transitset their header inside the transport, below the redirect policy, so they are re-applied on every hop. They defend themselves by pinning to a host instead — see keep a credential off a redirect.
WithTLSConfig — replace the TLS configuration wholesale¶
Replaces the hardened TLS configuration. Default: DefaultConfig() from
go/tls.
The replacement is total: minimum version, cipher suites, curve preferences and root pool
all come from the config you supply. Nothing is merged, so a &tls.Config{} with a single
field set is a client with the Go defaults for everything else, not a hardened client with
one field changed. To adjust one setting and keep the rest, start from gtls.DefaultConfig()
and mutate the copy it returns.
Passing nil is not an error and does not disable TLS — the transport falls back to the
hardened default, because NewTransport substitutes it for a nil config.
Because WithCertPool writes into whichever config is current, the order of the two
options matters — see order-sensitive options below.
WithCertPool — trust a private CA¶
Sets RootCAs on the hardened TLS configuration, keeping its cipher suites, minimum
version and curve preferences. Default: no pool, which means the host's system trust
store.
A non-nil pool replaces the system trust store; it does not add to it. A client given
a pool containing only your internal CA will fail every call to a publicly-trusted host
with x509: certificate signed by unknown authority. To trust both, seed the pool from
x509.SystemCertPool() before adding your CA — see
trust a private CA.
Passing nil sets RootCAs back to nil, which is the system trust store again.
Build the pool with gtls.CertPool(files...) from go/tls, which reads PEM files and
errors if a file contains no certificates. Note that gtls.CertPool() with no arguments
returns an empty pool, not the system roots — a client given that pool trusts nothing.
WithTransport — supply your own http.RoundTripper¶
Replaces the transport entirely. Default: the transport NewTransport builds.
Everything NewTransport configures is discarded with it: the hardened TLS config,
Proxy: http.ProxyFromEnvironment, the connection-pool limits and the dial, handshake and
response-header timeouts. WithTLSConfig and WithCertPool are silently ignored when a
transport is supplied — they configure a transport that is never built. No warning is
logged.
What survives is everything the client layer owns: the overall timeout and the redirect
policy, including the sensitive-header stripping. WithRetry and WithClientMiddleware
still wrap the transport you supplied.
Typical uses are a test double, an already-instrumented transport, or a transport built by
NewTransport(cfg) and then adjusted:
tr := httpclient.NewTransport(nil) // hardened defaults
tr.MaxIdleConnsPerHost = 100
client := httpclient.NewClient(httpclient.WithTransport(tr))
WithRetry — retry transient failures¶
Installs the retry transport from
go/transit, closest to the wire. Default: no
retry at all — an omitted WithRetry means a single attempt.
The configuration type belongs to go/transit, and so does the behaviour: exponential
backoff with full jitter, Retry-After honoured and clamped to MaxBackoff, and invalid
values normalised rather than rejected (a negative MaxRetries becomes zero, non-positive
backoffs fall back to transit's defaults, and a MaxBackoff below InitialBackoff is
raised to match it). DefaultRetryConfig()'s values are listed in
defaults and limits.
Two limits catch people out, both of them transit's rules rather than this module's:
- Only idempotent methods are retried by default. GET, HEAD, OPTIONS, PUT and DELETE
are eligible; a POST that comes back 503 is returned as-is, with one attempt made. Set
RetryAllMethodsor list methods inRetryableMethodsto change that, and only when every request is safe to replay. - A request body must be rewindable. Retry is skipped when the request has a body and
no
GetBodyto recreate it.http.NewRequestsetsGetBodyfor abytes.Buffer,bytes.Readerorstrings.Reader; an arbitraryio.Readergets none, and that request is never retried.
A request that provably never reached the origin — a refused connection or a failed dial — is retried regardless of method.
WithClientMiddleware — wrap the transport with a transit chain¶
Applies a go/transit client middleware chain to the transport. Default: no chain.
The chain wraps the transport after retry, so retry operates on the raw transport and a chain member sees one logical call rather than each attempt. Within the chain the first middleware is the outermost wrapper. A client built with
httpclient.WithRetry(transithttp.DefaultRetryConfig()),
httpclient.WithClientMiddleware(transithttp.NewClientChain(
transithttp.WithCircuitBreaker(log, transithttp.DefaultCircuitBreakerConfig()),
transithttp.WithBearerToken(token, "api.example.com"),
transithttp.WithRequestLogging(log),
)),
layers as breaker → auth → logging → retry → transport. See
compose client middleware for what belongs where and why.
Any func(http.RoundTripper) http.RoundTripper is a valid chain member, so a
round-tripper this module knows nothing about — an OpenTelemetry transport, a recorder —
can be added without giving up the hardened transport the way WithTransport would.
Options whose order matters¶
Options are applied left to right, so a later option overwrites an earlier one that writes the same field. Two combinations are worth stating explicitly.
WithCertPool before WithTLSConfig loses the pool. WithTLSConfig replaces the
whole configuration, including the RootCAs the earlier option set. Put WithCertPool
last, or set RootCAs on the config you pass.
WithTLSConfig before WithCertPool writes into the config you supplied.
WithCertPool assigns RootCAs on the current *tls.Config in place, so the
*tls.Config value you passed in is modified. If you reuse that config for another client
or another transport, it now carries the pool too. Pass a fresh config per client, or
apply the pool to the config yourself before handing it over.
What NewTransport returns¶
Builds the same *http.Transport NewClient uses, so it can be adjusted or shared. A nil
tlsCfg means the hardened default from go/tls. It is a plain constructor: it reads no
options, and calling it twice returns two independent transports with two independent
connection pools. Every field it sets is listed in
defaults and limits.