go 93 lines · 3 tabs

Enforce a Maximum Request Body Size in Go HTTP Handlers with MaxBytesReader

Shared by codesnips Sep 2026
3 tabs
package httpx

import (
	"net/http"
	"strconv"
)

// MaxBytes returns middleware that caps the request body at limit bytes.
func MaxBytes(limit int64) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			if cl := r.Header.Get("Content-Length"); cl != "" {
				if n, err := strconv.ParseInt(cl, 10, 64); err == nil && n > limit {
					http.Error(w, "request body too large", http.StatusRequestEntityTooLarge)
					return
				}
			}

			// Enforce the real limit even without a trustworthy Content-Length.
			r.Body = http.MaxBytesReader(w, r.Body, limit)
			next.ServeHTTP(w, r)
		})
	}
}
3 files · go Explain with highlit

Accepting request bodies without a size cap is a classic denial-of-service vector: a single client can stream gigabytes into memory or disk and exhaust the process. This snippet shows the idiomatic Go defense, http.MaxBytesReader, wired into reusable middleware and then consumed by a JSON handler that reports oversized payloads cleanly.

In maxbytes.go, the MaxBytes middleware wraps a limit around every request. It first performs a cheap short-circuit on the Content-Length header — if a client honestly advertises a body larger than the limit, the request is rejected with 413 Request Entity Too Large before any bytes are read. That header cannot be trusted on its own, so the real enforcement is http.MaxBytesReader(w, r.Body, limit), which replaces r.Body with a reader that returns an error once more than limit bytes have been consumed. Because it is a decorator over the standard library, it composes with any http.Handler and works for chunked transfers where Content-Length is absent.

The subtlety MaxBytesReader handles is that it also calls the underlying ResponseWriter to close the connection, preventing a client from continuing to push data after the limit is hit. The error it produces is the sentinel *http.MaxBytesError, which downstream code inspects rather than treating every read failure as a client's fault.

In upload_handler.go, UploadHandler reads the now-capped body. It uses errors.As to distinguish a *http.MaxBytesError from generic I/O or JSON errors, returning 413 for the former and 400 for malformed JSON. This separation matters: an oversized body is a policy violation, while a truncated JSON document is a syntax problem, and clients need different signals. The handler also disables DisallowUnknownFields deliberately kept strict to reject junk.

In server.go, the middleware is applied once with a 1 MiB cap via MaxBytes(1 << 20), demonstrating that the limit is a single tunable knob at the edge. A key pitfall worth noting: MaxBytesReader only guards a single body, so per-route overrides (a 10 MiB image endpoint, say) should re-wrap with a larger value rather than relying on the global default. The pattern keeps memory bounded, fails fast, and returns precise status codes.


Related snips

Share this code

Here's the card — post it anywhere.

Enforce a Maximum Request Body Size in Go HTTP Handlers with MaxBytesReader — share card
Link copied