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 itsTransport
?
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.