7. HTTP Servers & Clients

Harden services: enforce timeouts, reuse connections, stream safely, and shut down gracefully.

Question: Why is it critical to set timeouts on a production http.Server?

Answer: It is critical to set ReadTimeout, WriteTimeout, and IdleTimeout on a production server to protect it from slow or malicious clients. Without timeouts, a slow client could hold a connection open indefinitely, consuming resources and eventually leading to resource exhaustion (a "slowloris" attack).

Explanation:

  • ReadTimeout: Max time to read the entire request, including body.

  • WriteTimeout: Max time to write the entire response.

  • IdleTimeout: Max time to wait for the next request on a keep-alive connection.

Setting these timeouts ensures that server resources are recycled efficiently and the server remains resilient under load.

Question: How do you implement graceful shutdown for an http.Server?

Answer: Graceful shutdown is implemented by listening for an interrupt signal (like SIGINT), and then calling the server.Shutdown() method. This method gracefully stops the server from accepting new connections and waits for a specified duration for existing requests to complete before closing.

Explanation: Graceful shutdown is essential for zero-downtime deployments. It prevents requests from being abruptly terminated during a restart, ensuring a smooth user experience. You should use a context.WithTimeout with Shutdown to guarantee the server eventually exits, even if some connections hang.

func runServer(
    ctx context.Context, addr string, handler http.Handler,
) error {
    srv := &http.Server{
        Addr:         addr,
        Handler:      handler,
        ReadTimeout:  5 * time.Second,
        WriteTimeout: 10 * time.Second,
        IdleTimeout:  60 * time.Second,
    }
    errCh := make(chan error, 1)
    go func() { errCh <- srv.ListenAndServe() }()
    select {
    case <-ctx.Done():
        shutdownCtx, cancel := context.WithTimeout(
            context.Background(), 10*time.Second,
        )
        defer cancel()
        _ = srv.Shutdown(shutdownCtx)
        return ctx.Err()
    case err := <-errCh:
        if errors.Is(err, http.ErrServerClosed) { return nil }
        return err
    }
}

Question: Why should you reuse http.Client, and how can you customize its Transport?

Answer: You should reuse http.Client because it manages an underlying http.Transport which handles connection pooling (keep-alives) and caching. Creating a new client for each request is inefficient as it does not reuse TCP connections, leading to high latency and resource usage.

Explanation: The default client is a good starting point, but for production use, you should create a custom client with a tuned Transport and a request timeout. The Transport can be configured to set connection timeouts, limit the number of idle connections, and more.

var httpClient = &http.Client{
    Timeout: 10 * time.Second, // total deadline; still set transport timeouts
    Transport: &http.Transport{
        DialContext: (&net.Dialer{
            Timeout:   3 * time.Second,
            KeepAlive: 30 * time.Second,
        }).DialContext,
        TLSHandshakeTimeout:   3 * time.Second,
        ResponseHeaderTimeout: 5 * time.Second,
        ExpectContinueTimeout: 1 * time.Second,
        MaxIdleConns:          100,
        MaxIdleConnsPerHost:   10,
        IdleConnTimeout:       90 * time.Second,
    },
}

Question: How do you limit request body size and avoid DoS via large payloads?

Answer: Wrap the body with http.MaxBytesReader on the server and set client-side Request.Body = io.NopCloser(io.LimitReader(...)) when proxying.

Explanation: Always validate Content-Length expectations and handle ErrBodyReadAfterClose correctly. For JSON, use json.Decoder streaming with Decoder.DisallowUnknownFields() when appropriate.

Question: How do you stream JSON efficiently?

Answer: Use json.Decoder to stream decode from io.Reader and json.Encoder to stream encode to io.Writer.

Explanation: Streaming avoids loading entire payloads into memory and enables backpressure over network connections.

Question: How do you implement conditional requests with ETags?

Answer: Set ETag on responses and honor If-None-Match by returning 304 Not Modified on match.

Explanation: Saves bandwidth/CPU for cached clients.

Question: When is httptrace useful?

Answer: httptrace instruments client requests (DNS, connection, TLS, headers) to pinpoint latency sources.

Explanation: Attach a *httptrace.ClientTrace via request context.

Question: How do you gzip responses safely?

Answer: Negotiate Accept-Encoding, set Content-Encoding: gzip, and avoid recompressing already-compressed types.

Explanation: Close/flush writers and handle partial writes on errors.

Question: How do you structure middleware and health checks?

Answer: Compose middleware around your handler (logging, recovery, tracing). Expose /livez (process up) and /readyz (dependencies OK) endpoints.

Explanation: Health endpoints should be fast and unauthenticated. Readiness should fail when critical dependencies are unavailable to enable safe rollouts.