# Go - net/http Transport Behavior
`net/http` hides transport defaults that cause failures with no error at the call site. The idle-connection pool, the request-body lifecycle, and the connection-hijack path each contain a trap that stays silent until a payload is corrupt, a request hangs, or a span never ends.
## Key points
- `http.Transport.MaxIdleConnsPerHost` defaults to 2, regardless of `MaxIdleConns`. For a Unix-domain socket, all requests share one synthetic hostname, so the per-host cap is the effective pool limit. Set it equal to `MaxIdleConns`.
- `http.ConnState` callbacks fire synchronously on the server's connection lifecycle. This makes them reliable for pool-behavior tests that use no sleeps and no polling.
- The Go stdlib does not retry a POST after it writes bytes, regardless of an `Idempotency-Key`. `GetBody` allows request rewinding, but you still need an explicit application-level retry loop for a `broken pipe` after a partial send.
- `http.NewRequest` auto-populates `GetBody` only for known body types such as `*bytes.Buffer`. A custom body type silently leaves `GetBody` nil, which disables transparent retry and redirect replay. Guard the nil case explicitly.
- `io.LimitReader` truncates silently and reports no error. Body-capturing middleware that puts a capped copy back as the real `req.Body` silently corrupts every larger payload. Never replace flowing bytes with a capped copy.
- Writing to a Unix-domain socket after the peer closes yields `ENOTCONN` on Darwin and `EPIPE`/`ECONNRESET` on Linux, so stale-connection flakes concentrate on macOS. Windows parity needs a message-contains fallback, because WSA error chains do not always unwrap under `errors.Is`. Raising the stale-connection retry attempts from 3 to 6 absorbs bursts of stale pooled connections.
- Go 1.27 adds a post-close body drain (golang/go#77370). The drain turns the old two-waiters-one-send body race into a permanent deadlock on the 1.26/1.27 boundary: `Read` releases the mutex across the inner body `Read`, a `Close` in that window takes the early-close path and parks on the done channel, and the successful drain keeps the read loop alive, so the deferred close never runs.
- To reproduce the drain race, land the chunked terminator inside the 50 ms `maxPostCloseReadTime` budget. A longer hold times out the drain, kills the connection, and frees the waiters through the close, so no deadlock occurs.
- On a `101 Switching Protocols` hijack, `ReadResponse` returns `http.NoBody`, and the response-close path runs only on the non-101 branch. A client span on a hijacked request therefore never ends. Every protocol-switch hijack leaks an unended span.
- `strconv.FormatUint(n, 16)` is variable-length and unpadded. A claim of a fixed-width 128-bit hex identifier needs zero-padded `%016x` formatting, or the code emits `"0"` for zero and the comment lies about the invariant.
## How to apply
1. For a transport that talks over a Unix-domain socket, set `MaxIdleConnsPerHost` equal to `MaxIdleConns`.
2. Check `req.GetBody != nil` before you rely on transparent retry or redirect replay.
3. Write your own retry loop for a POST that can break after a partial send. Rewind the body through `req.GetBody`.
4. Keep body-capturing middleware read-only. Never write a capped copy back into `req.Body`.
5. Match socket errors per platform: `ENOTCONN` on Darwin, `EPIPE` and `ECONNRESET` on Linux, and a `strings.Contains(err.Error(), "socket is not connected")` fallback on Windows.
6. Budget stale-connection retry attempts above the pool's burst size. Six attempts absorbed the bursts.
7. Record connection-pool behavior with `http.ConnState`. Do not add sleeps or polling.
8. Deliver the chunked terminator within 50 ms when you reproduce the drain race.
9. Format fixed-width hex identifiers with `fmt.Sprintf("%016x%016x", hi, lo)`.
10. Expect an unended client span on every `101 Switching Protocols` hijack. End the span on the hijack branch.
## Observations
- [pitfall] `MaxIdleConnsPerHost` defaults to 2, so a Unix-domain-socket pool starves unless you set it equal to `MaxIdleConns`. #connection-pool (source: agent sessions, 2026-03)
- [practice] `http.ConnState` callbacks fire synchronously, so pool-behavior tests need no sleeps. #testing (source: agent sessions, 2026-03)
- [pitfall] The stdlib never retries a POST after a partial send, regardless of an `Idempotency-Key`. #retries (source: agent sessions, 2026-05)
- [pitfall] `strconv.FormatUint` emits variable-length hex, so a zero value breaks a fixed-width identifier. #formatting (source: agent sessions, 2026-06)
- [pitfall] A custom body type leaves `GetBody` nil, which disables transparent retry and redirect replay. #retries (source: agent sessions, 2026-06)
- [pitfall] `io.LimitReader` truncates without an error, so a capped copy that replaces `req.Body` corrupts larger payloads. #middleware (source: agent sessions, 2026-06)
- [portability] `ENOTCONN` on Darwin marks a stale Unix-domain socket. Windows needs a message-contains fallback because WSA chains do not unwrap under `errors.Is`. #cross-platform (source: agent sessions, 2026-07)
- [bug] Go 1.27 turns the post-close body-drain race into a permanent deadlock on the 1.26/1.27 boundary. #stdlib (source: agent sessions, 2026-09)
- [practice] Hold the chunked terminator within the 50 ms drain budget to reproduce the deadlock. #testing (source: agent sessions, 2026-09)
- [pitfall] A protocol-switch hijack leaves the client span unended. #observability (source: agent sessions, 2026-10)
## Relations
- relates_to [[resources/documentation/Go/Go - Internals]] (connection pooling and the body lifecycle follow net/http internals)
- relates_to [[resources/documentation/Go/Go - Testing Patterns and Tests That Certify Nothing]] (ConnState records pool behavior without sleeps, and race reproduction needs a timing budget)
- relates_to [[resources/documentation/Go/Go - Portability and Crash Handling]] (stale-socket errors differ per platform)
- relates_to [[resources/documentation/Go/Instrumentation/Go Instrumentation - OpenTelemetry SDK and Bridge Pitfalls]] (a hijacked request leaks an unended client span)
## Sources
Personal agent-session notes, March 2026 to October 2026