Standard library patterns · 3 MIN READ

Outgoing HTTP: reuse the client and bound the response, not just the dial

Own the client lifetime, close bodies and classify HTTP status separately from transport failure.

A successful Client.Do means an HTTP response was received, not that the application operation succeeded. An error page can still be a valid response, and a server can keep streaming bytes after the headers arrive. Reuse the client and transport, pass an operation context and bound both elapsed time and bytes consumed. Closing the body is necessary even when status handling returns early.

stdlib · A MAP OF THE IDEA

  1. Reuse owned client
  2. Bound request lifetime
  3. Check status + body limit
  4. Close and classify outcome

Useful when

  • Own the client lifetime, close bodies and classify HTTP status separately from transport failure.
  • Makes the owner and the failure boundary explicit.

Watch out

  • Creating a new transport per request loses useful pooling and may waste sockets.
  • Run native service and failure tests in your own development environment.

Implementation & operation cost: Additional state and tests must be justified by the demonstrated failure; this lesson is not a requirement for every Go program.

01

The contract and the failure

A successful Client.Do means an HTTP response was received, not that the application operation succeeded. An error page can still be a valid response, and a server can keep streaming bytes after the headers arrive. Reuse the client and transport, pass an operation context and bound both elapsed time and bytes consumed. Closing the body is necessary even when status handling returns early.

02

Work through the example

Validate the configured positive byte limit and keep its arithmetic within range in a real API. Check status before parsing a success DTO, classify retryable failures and make unsafe writes retry only under an idempotency policy. Bound decompressed response bytes too. Reusing a transport enables connection pooling; reading to EOF can help reuse, but do not drain an untrusted infinite body merely to save a connection.

go
package main

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"time"
)

func get(ctx context.Context, c *http.Client, url string, limit int64) ([]byte, error) {
	req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
	if err != nil {
		return nil, err
	}
	resp, err := c.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return nil, fmt.Errorf("upstream status %d", resp.StatusCode)
	}
	body, err := io.ReadAll(io.LimitReader(resp.Body, limit+1))
	if err != nil {
		return nil, err
	}
	if int64(len(body)) > limit {
		return nil, fmt.Errorf("response exceeds %d bytes", limit)
	}
	return body, nil
}
func main() {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte("four")) }))
	defer server.Close()
	client := &http.Client{Timeout: 2 * time.Second}
	_, err := get(context.Background(), client, server.URL, 3)
	fmt.Println(err)
}
Read the code

The body is limited to the allowed bytes plus one detection byte, so a truncated success cannot be mistaken for a complete valid response. Client.Timeout covers the overall request including body reading; the context can impose an earlier bound. The sample uses only a local test server and returns the explicit oversize error.

03

Production decisions

Creating a new transport per request loses useful pooling and may waste sockets. Ignoring Close or using an unbounded ReadAll can exhaust memory. A timeout after sending a write leaves an unknown business outcome, even if no response body was received. Test slow headers, slow body, oversize decompressed data, non-2xx responses, redirects and cancellation in your native service.

04

Explain it in an interview

Does a nil error from http.Client.Do guarantee HTTP success and a small complete body? No. Check status, bound time and response bytes, validate parsing and always close the body; retry semantics depend on the operation. Describe the state before the operation, its owner, the failure between steps and the evidence you would test. Distinguish what this example demonstrates from the infrastructure a production service still needs.

TAKE THIS WITH YOU

Own the client lifetime, close bodies and classify HTTP status separately from transport failure. No. Check status, bound time and response bytes, validate parsing and always close the body; retry semantics depend on the operation.

Check your understanding

Does a nil error from http.Client.Do guarantee HTTP success and a small complete body?

Go to the source

Progress stays in this browser. No account needed.