Skip to content

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 timeoutsMaxIdleConns, 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 CheckRedirect policy stops after the configured maximum and rejects any redirect that would drop from https to http, 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/transit is 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. A depfootprint_test.go guard 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.