client

package
v0.4.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 8, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package client provides an *http.Client with structured logging and tracing built in.

Every exchange is logged through xlog and wrapped in a span, so outgoing calls show up in traces alongside the operation that made them. What may be written to those logs is the caller's decision: see sanitize.Sanitizer.

The zero-configuration client is not redaction-safe. NewClient() with no options logs URLs and headers verbatim, so any service whose requests carry credentials should pass WithSanitizer.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func NewClient

func NewClient(opts ...Option) *http.Client

NewClient returns an *http.Client that logs and traces every exchange.

The returned client is an ordinary *http.Client: it can be handed to any library that accepts one, and it is safe for concurrent use.

Types

type Option

type Option func(*http.Client)

Option configures the client returned by NewClient.

func WithBodyLogging

func WithBodyLogging() Option

WithBodyLogging enables request and response body dumps at debug level, truncated to maxLoggedBodyBytes.

This is a local debugging tool and is deliberately awkward to reach for. Bodies are the one thing a sanitizer struggles to make safe: their shape is arbitrary, so a secret inside one is unrecognizable. Enabling this in code that ships means every payload the service exchanges lands in the log.

Consider forbidding it outside tests with a lint rule, e.g. golangci-lint's forbidigo.

func WithCallerAfterDo

func WithCallerAfterDo(f func(context.Context, *http.Response)) Option

WithCallerAfterDo registers a hook run after a successful round-trip. It is not called when the round-trip fails.

A hook that reads resp.Body must restore it, or the caller will read an empty body.

func WithCallerBeforeDo

func WithCallerBeforeDo(f func(context.Context, *http.Request)) Option

WithCallerBeforeDo registers a hook run just before the request goes out. Use it to attach credentials or correlation headers:

WithCallerBeforeDo(func(_ context.Context, r *http.Request) {
    r.Header.Set("Authorization", "Bearer "+token)
})

Hooks run before the request is logged, so anything they add is visible to the sanitizer — and therefore actually subject to redaction.

func WithCustomRedirectFlow

func WithCustomRedirectFlow(redirectFlow func(*http.Request, []*http.Request) error) Option

WithCustomRedirectFlow installs a custom http.Client.CheckRedirect policy.

func WithDialGuard added in v0.3.0

func WithDialGuard(guard dialguard.Guard) Option

WithDialGuard installs a policy consulted before every connection, refusing the dial when it returns an error. It is off by default: this client is also used to reach neighbors inside the perimeter and stubs on loopback, and a guard enabled by default would break those silently.

Use it when the destination comes from outside the service — a URL an administrator types into a form, a callback address in a payload. See WithoutInternalHosts for the ready-made policy, and dialguard for building one from the default list plus or minus your own networks.

The guard receives an already-resolved ip:port and runs on every connection attempt — redirects and retries included, and once per address the resolver returned. That is what makes it a defense against DNS rebinding, which a check made when a URL is stored cannot be.

Two properties matter when writing a policy of your own. A denial refuses one address rather than the dial: net.Dialer falls through to the next address the resolver returned, so a list covering one address family and not the other blocks nothing. And the guard runs per connection, so a connection it approved keeps serving later requests from the pool without being consulted again.

A nil guard is ignored, and in particular does not clear a guard an earlier option installed: a security control should not disappear because a config field was unset. Two non-nil guards do not compose — the last one wins.

func WithProxy added in v0.4.0

func WithProxy(proxy func(*http.Request) (*url.URL, error)) Option

WithProxy overrides how the client picks a proxy for a request, replacing the default http.ProxyFromEnvironment. Returning a nil *url.URL for a request sends it directly.

A nil function is ignored rather than installed, since that would disable proxying rather than configure it — pass a function returning (nil, nil) if going direct is what you want.

Note the same rule points the other way here than it does for WithDialGuard. There, ignoring nil preserves a security control; here it preserves http.ProxyFromEnvironment, which is the thing that defeats a dial guard. "nil is ignored" means "the earlier setting stands", not "fails safe".

Worth knowing when a dial guard is installed: a proxied request is not covered by it. The dialer connects to the proxy, so that is the only address the guard ever sees; the real destination is carried in the request itself — measured on go1.25.13, in the request line for http and in a CONNECT for https. Routing a guarded client through a proxy therefore disables the protection for every proxied destination, whatever the scheme.

Do not interpolate the proxy URL into an error this function returns. Such an error is surfaced by net/http and logged at error level without passing through the sanitizer, and a proxy URL routinely carries credentials — that is how HTTPS_PROXY auth is expressed. Name the host, not the URL.

func WithSanitizer

func WithSanitizer(s sanitize.Sanitizer) Option

WithSanitizer installs the redaction policy used for every logged URL, header set and body. Without it the client uses sanitize.NoopSanitizer and logs everything verbatim, secrets included.

A nil sanitizer is ignored rather than installed: a client that logs raw credentials because a constructor argument was nil is worse than one that keeps whatever policy it already had.

func WithTimeout

func WithTimeout(timeout time.Duration) Option

WithTimeout overrides the total request timeout. A non-positive value is ignored so a caller can pass an unset config field without accidentally disabling the default ceiling.

func WithTransport

func WithTransport(transport *http.Transport) Option

WithTransport replaces the underlying *http.Transport, keeping the logging round-tripper that wraps it. A nil value is ignored.

Note that options which reach into the logging round-tripper — WithCallerBeforeDo, WithCallerAfterDo, WithSanitizer, WithBodyLogging, WithDialGuard, WithoutInternalHosts, WithProxy — look up that wrapper on the client. Replacing the whole c.Transport from outside (rather than through this option) removes it, and those options then become silent no-ops.

For the two dial-guard options that no-op is a security consequence rather than a logging one, and it applies in either order: used before this option the guard is installed and then discarded with the transport that carried it, and used after, it no longer finds the wrapper. A client built this way looks guarded and is not.

func WithoutInternalHosts added in v0.3.0

func WithoutInternalHosts() Option

WithoutInternalHosts refuses connections to loopback, private, link-local and reserved addresses — including the IPv6 spellings that carry an IPv4 target inside them. It is the ready-made form of WithDialGuard; see dialguard for the full list.

Off by default, and worth turning on only where the destination is influenced from outside the service. Two limits are worth knowing before relying on it:

An HTTP proxy bypasses it. The default transport honors http.ProxyFromEnvironment, and with a proxy configured the dialer connects to the proxy while the real target travels inside a CONNECT, unseen by the guard. NO_PROXY makes this partial rather than all-or-nothing: destinations it exempts are dialed directly and are guarded.

WithTransport removes it, in either option order. Replacing the transport discards the dialer this guard lives on, and an option applied afterwards no longer finds the wrapper it needs — so the client looks configured and is not.

The error names an address, not a host: the guard sees only what the resolver returned, so a message telling an operator which URL was rejected has to be produced by the caller.

func WithoutRedirect

func WithoutRedirect() Option

WithoutRedirect stops the client from following redirects, returning the redirect response itself instead.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL