62. Production HTTP APIs: validation, timeouts, middleware, shutdown
62.1 Limit and validate request bodies
func decodeJSON[T any](w http.ResponseWriter, r *http.Request, dst *T) bool {
r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1 MiB
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
if err := dec.Decode(dst); err != nil {
http.Error(w, "invalid JSON", http.StatusBadRequest)
return false
}
return true
}
Production handlers should treat every boundary value as hostile: path params, query params, headers, cookies, JSON, multipart files, and remote addresses.
62.2 Map internal errors to stable HTTP responses
func writeError(w http.ResponseWriter, err error) {
switch {
case errors.Is(err, ErrNotFound):
http.Error(w, "not found", http.StatusNotFound)
case errors.Is(err, ErrInvalid):
http.Error(w, "bad request", http.StatusBadRequest)
default:
http.Error(w, "internal error", http.StatusInternalServerError)
}
}
Do not leak database errors, stack traces, SQL strings, token parsing details, or filesystem paths to clients. Log the internal detail; return a stable external contract.
62.3 Client and transport timeouts
transport := &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 20,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 5 * time.Second,
}
client := &http.Client{
Transport: transport,
Timeout: 10 * time.Second,
}
Use one long-lived client per destination/configuration. Creating a new client per request defeats connection pooling.
62.4 Graceful shutdown in real servers
srv := &http.Server{
Addr: ":8080",
Handler: handler,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 2 * time.Minute,
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}()
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Printf("graceful shutdown failed: %v", err)
}
Shutdown only works if handlers respect request contexts. If a handler starts background work, it must either finish quickly, detach intentionally, or be owned by a separate worker lifecycle.
Applied in the series: HTTPS guide, Chapter 19 measures the four server timeouts against Slowloris: 200 held connections were dropped within 10 s.