Frontend Labs
⋯

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.


Generated from real-life-golang-guide.md · 8 Oct 2026 16:36