Wrap the request body with http.MaxBytesReader before you read or decode it. Go does not cap request bodies by default, and Server.MaxHeaderBytes limits headers only. MaxBytesReader stops a handler reading past your chosen size, which protects memory and CPU from oversized or malicious uploads.
Definition
http.MaxBytesReader takes the response writer, a reader (normally r.Body) and a byte limit, and returns a reader that fails once the limit is passed. The Go documentation puts its purpose in one sentence: it "prevents clients from accidentally or maliciously sending a large request and wasting server resources."
Header size is a separate control. The source for Server.MaxHeaderBytes says it limits the bytes read parsing header keys and values, including the request line, and that it "does not limit the size of the request body".
The pattern
func create(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1 MiB
var in CreateInput
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
if err := dec.Decode(&in); err != nil {
var tooBig *http.MaxBytesError
if errors.As(err, &tooBig) {
http.Error(w, "request too large", http.StatusRequestEntityTooLarge)
return
}
http.Error(w, "bad request", http.StatusBadRequest)
return
}
// use in
}
Return 413 for a body that is too large and 400 for malformed input, so clients and logs can tell them apart. Keep the error text generic.
Choosing a limit
Pick the limit per route, not one global number. A login form needs a few kilobytes. A JSON create call may need tens of kilobytes. A file upload may need megabytes and should stream to storage instead of loading into memory.
| Route type | Typical approach |
|---|---|
| Login, tokens, small forms | Very small limit, a few KB |
| JSON API writes | Limit from your largest valid payload |
| File upload | Larger limit, stream to disk or object storage, never read it all into memory |
| Webhooks | Limit to the provider's documented maximum payload |
A middleware that applies a default and lets specific routes raise it works well.
Two common mistakes
The first is applying the limit after the body has already been read, for example after calling ParseForm. The limit must wrap the body before any reader touches it. The second is trusting the Content-Length header. A client can declare a small length and send more, or send a chunked body with no length at all, which is why a wrapped reader that counts actual bytes is the dependable control.
Steps
- List every handler that reads r.Body, including ParseForm, ParseMultipartForm and JSON decoders.
- Add MaxBytesReader at the top of each one, or in middleware.
- Decide the limit from real payload sizes and add a margin.
- Map the size error to 413 and log it with the route and client, without logging the body.
- Add a test that sends a body one byte over the limit.
- Pair this with header timeouts. See which timeouts a Go HTTP server needs.
Try it yourself
Click through it at your own pace, or open it full screen. Look at the kinds of API weaknesses a scan reports, including missing input limits.
Quick check
Does MaxBytesReader limit multipart uploads?
It limits whatever you read through the wrapped body. Apply it before ParseMultipartForm, and set a sensible memory limit for that call as well.
Should I rely on the reverse proxy for body limits?
Use it as one layer, not the only one. Traffic that bypasses the proxy, such as internal calls, will not be limited.
Safeguard's dependency scanning and SBOM generation cover the Go modules your service builds from, with reachability analysis to separate reachable findings, and the free scan is at gold.safeguard.sh/scan.
Sources
Last reviewed: 19 October 2026.