Hardened defaults & the factory model¶
httpclient exists to answer one question well: what should a Go HTTP client look like
before you've configured anything? Its answer is "secure, bounded, and honest about
redirects", and it draws a deliberate line between the client factory (this module)
and the middleware (go/transit).
What NewClient sets for you¶
Calling NewClient() with no options still gives you a client that:
- Enforces a TLS 1.2 floor with curated cipher suites and curve preferences, from
go/tls's
DefaultConfig. Downgrade to an obsolete protocol is not reachable by accident. - Bounds its connection pool and timeouts —
MaxIdleConns, per-host limits, idle / TLS-handshake / response-header / dial timeouts — so a slow or hostile peer cannot exhaust resources or hang a request forever. - Refuses HTTPS→HTTP downgrades and caps redirects. The
CheckRedirectpolicy stops after the configured maximum and rejects any redirect that would drop fromhttpstohttp, closing a common credential/data-leak vector.
You opt out of these (via WithTLSConfig, WithTransport, WithMaxRedirects), never
in. The secure posture is the path of least resistance.
Why the factory is separate from the middleware¶
The retry transport, circuit breaker, credential injection and request logging are not
in this module — they live in go/transit, and
NewClient merely wires them in through WithRetry / WithClientMiddleware. That split
is intentional:
- A transport concern is identical everywhere; a client is a policy. Retry-on-503 or host-pinned auth behave the same for every caller, so they belong in a shared, transport-neutral module. Connection limits, TLS trust and timeouts are choices a service makes, so they belong in a client factory the service owns.
- The middleware is reused by the server too.
go/transitis consumed by both clients and servers; keeping it separate means one tested implementation of each concern, not a client copy and a server copy. - Light graphs. Because the factory only pulls
go/tls+go/transit, a client-only consumer never links the server stack (controls, authn, gateway) or the gRPC SDK. Adepfootprint_test.goguard enforces this.
The redirect policy in detail¶
redirectPolicy(max) returns a func(*http.Request, []*http.Request) error that
net/http calls before following each redirect. It fails when the number of prior hops
reaches max, and — independently — when the original request was https and the
next hop is http. Because the check reads via[0] (the first request) rather than the
immediately-preceding hop, a chain cannot launder a downgrade through an intermediate
same-scheme redirect.
Relationship to go/transit¶
Think of it as two layers with one seam:
httpclient.NewClient → *http.Client
│ hardened transport (TLS, limits, timeouts, redirect policy)
└─ wraps → go/transit round-trippers (retry, breaker, auth, logging)
The factory owns the outer shell and the transport; transit owns everything that wraps a request on its way out. The transit middleware model covers the wrapping order.