Skip to content

fix(client): close response bodies in sendRequest to stop connection-pool exhaustion - #61

Draft
spbsoluble wants to merge 1 commit into
v3from
fix/close-response-bodies
Draft

spbsoluble wants to merge 1 commit into
v3from
fix/close-response-bodies

Conversation

@spbsoluble

@spbsoluble spbsoluble commented Nov 5, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

Client.sendRequest returns successful (200/204) responses with resp.Body still open, and most callers decode the body without closing it. The non-success branches also return an error without ever closing the body. Each unclosed, not-fully-read body pins its connection in the transport's pool.

The auth-client transports that this client builds on cap connections per host (MaxConnsPerHost, 10 in keyfactor-auth-client-go v1.6.1-rc.0), and the client is cached once per process. After the cap is reached by leaked bodies, the next request blocks indefinitely in net/http.(*Transport).getConn, so a long-running consumer (for example a Terraform provider run) hangs instead of failing.

Fix

  • Success path: sendRequest reads the whole body, closes the original (returning the connection to the pool), and sets resp.Body to an in-memory reader whose Close is a no-op. Callers may still call Close; they just no longer have to.
  • Error paths: sendRequest owns the body, so it drains (bounded to 64 KiB) and closes it before returning.

Tradeoff

One extra in-memory copy of each response for the duration of the call. Existing callers already decode the full body into memory, so peak usage rises by at most one raw copy of the response. If very large streaming downloads are added later, they would need a separate path.

Tests

New v3/api/response_body_leak_test.go uses a TLS httptest server and a transport with MaxConnsPerHost: 2, then issues more requests than the cap. It covers sendRequest for 200 (unread, chunked, fully decoded), 204, and 400/401/404/500 responses, plus DownloadCertificate. Four cases wedge before the fix; all pass after.

cd v3 && GOWORK=off go test ./... passes.

Possible follow-up

A per-request/configurable client timeout so that any future pool exhaustion surfaces as an error instead of an indefinite wait.

…pool exhaustion

sendRequest returned successful responses with the body open and most
callers never closed it, and the error paths never closed it either.
With a per-host connection cap on the shared transport, each leaked body
pinned a pooled connection, and once the cap was reached the next request
blocked forever in net/http.(*Transport).getConn.

On success the body is now buffered, the original closed, and an
in-memory reader returned (Close is a no-op, so callers may still call
it). Error paths drain a bounded amount and close the body.

Adds a regression test using a TLS httptest server and a transport with
MaxConnsPerHost: 2; four cases wedge before the fix and all pass after.
@spbsoluble

Copy link
Copy Markdown
Collaborator Author

Cross-reference: terraform-provider-keyfactor#219 / #223 depend on stable long-running client behavior. This fix needs a release, followed by a version bump in the provider.

Validation note: validated end-to-end: with the patched client a provider run that previously stalled and took ~2h43m completed in ~18 min.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant