In a Go HTTP handler, what does http.MaxBytesReader do when a client sends a body larger than the limit?
answer
- wrap the body before anything reads it
- the limit is in bytes, not fields
- a typed error, not a silent stop
- the handler still writes the status code
- errors.As gives you the limit that was hit
basics
~20 shttp.MaxBytesReader wraps a request body so any read past the given byte limit fails with a *http.MaxBytesError instead of continuing. It caps what one request can allocate, but the handler must still write the 413 response itself.
solid answer
~50 s`http.MaxBytesReader(w, r.Body, n)` returns an `io.ReadCloser` that you assign back to `r.Body` before anything reads it. Reads work normally up to `n` bytes; the read that would go past `n` fails with an error of type `*http.MaxBytesError`, which carries the `Limit` field, so you can match it with `errors.As` and reply `http.StatusRequestEntityTooLarge`. It also tells the server that the request was too large, so the server stops trying to drain the rest of the body and reuse the connection. Two things it does not do: it does not write any response for you, and it does not limit anything but bytes — a 1 MB body is still 1 MB of untrusted structure to decode. It must wrap the body before `json.NewDecoder(r.Body).Decode` or any other read, because after the bytes are read the allocation has already happened.
code
go · 12 linesr.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1 MiB
var payload Event
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
var tooLarge *http.MaxBytesError
if errors.As(err, &tooLarge) {
http.Error(w, "body too large", http.StatusRequestEntityTooLarge)
return
}
http.Error(w, "malformed body", http.StatusBadRequest)
return
}go deeper
Be ready to write the two lines from memory: reassign r.Body to http.MaxBytesReader(w, r.Body, n) at the top of the handler, then return 413 when the read fails. Know that the limit counts bytes.
Explain why the wrapper needs the ResponseWriter, how errors.As pulls *http.MaxBytesError out of a decoder's wrapped error, and why the cap is useless once the body has been read.
Show where the cap belongs so no route can forget it — middleware or a handler wrapper — and be able to turn a chosen limit into a worst-case memory number for the number of requests you expect in flight.
Own the default. Decide what limit every public endpoint inherits, how a team with a legitimately larger payload gets an exception, and why the server keeps its own cap even when an edge proxy has one.
## The problem it solves An exported HTTP endpoint reads whatever the client sends. Nothing in `net/http` bounds the size of a request body by default: the server reads request *headers* up to `Server.MaxHeaderBytes` (1 MB by default, `http.DefaultMaxHeaderBytes`), but the body is whatever the handler chooses to read. If a handler does `json.NewDecoder(r.Body).Decode(&v)` or reads the body into a `[]byte`, an anonymous poster sending a multi-gigabyte body decides how much memory the process allocates. On a public webhook receiver, that is a one-request denial of service that needs no credentials. ## What the wrapper does `http.MaxBytesReader(w http.ResponseWriter, r io.ReadCloser, n int64) io.ReadCloser` returns a reader that passes through the first `n` bytes and then fails. Concretely: - Reads that stay within `n` behave exactly like the underlying body. - The read that would cross `n` returns an error of concrete type `*http.MaxBytesError`. That type has a `Limit int64` field holding the limit that was exceeded, so an error message can name the number the caller has to respect. Match it with `errors.As(err, &tooLarge)` where `tooLarge` is a `*http.MaxBytesError` — never by string comparison. - It takes the `ResponseWriter` as its first argument precisely so it can tell the server the request was over-sized. The server then stops trying to consume the remainder of the body before writing the reply and does not keep the connection alive for reuse — otherwise the server would sit there draining the very flood you just rejected. - The returned value is an `io.ReadCloser`, so it is a drop-in replacement for `r.Body`. ## How it is used The idiom is to reassign `r.Body` at the very top of the handler (or in middleware, or with `http.MaxBytesHandler`, which wraps a whole handler with a cap), then decode: ```go r.Body = http.MaxBytesReader(w, r.Body, 1<<20) ``` Order matters. The cap constrains *reads that happen after it*. Wrapping the body after something has already read it into memory accomplishes nothing at all — the allocation is already on the heap. ## What it does not do **It does not send a response.** Many handlers get this wrong and return 400 for an over-sized body, or return 500 because the error fell through to a generic branch. The handler must inspect the error and write `http.Error(w, ..., http.StatusRequestEntityTooLarge)` — status 413 — so the caller learns that the body, not its content, was the problem. **It does not limit structure.** A byte cap says nothing about how many objects that byte count expands into once decoded. A megabyte of deeply nested arrays decoded into an `any` becomes a very large number of small heap objects; a byte cap bounds the input, not the decoded graph. **It is not `Server.MaxHeaderBytes`.** That field bounds the request line and headers the server will accept before the handler ever runs; it has nothing to do with the body. **It is not `io.LimitReader`.** `io.LimitReader` reports a plain `io.EOF` at its limit, which a decoder cannot distinguish from a body that genuinely ended — the request comes back as a syntax error, not as "too large". `http.MaxBytesReader` exists to give an HTTP server a distinguishable, typed failure. ## Choosing the number The limit is a capacity decision, not a style choice: the worst-case memory a body cap allows is roughly the cap multiplied by the number of requests in flight, so 1 MB across 500 concurrent requests is half a gigabyte of body bytes before decoding costs are counted. Pick a number from the observed size distribution of real payloads with headroom, apply it per route where a route legitimately needs more, and document it so a 413 is diagnosable by the caller rather than mysterious.
- Does http.MaxBytesReader send the 413 response for you?No. It only makes the read fail. The handler must detect the error and write the status itself, typically `http.Error(w, "body too large", http.StatusRequestEntityTooLarge)`. If you let the error fall into a generic branch, the caller sees 400 or 500 and has no idea the body size was the problem.
- How do you tell an over-sized body apart from any other read error?Declare `var tooLarge *http.MaxBytesError` and call `errors.As(err, &tooLarge)`. Because the decoder wraps the read error, `errors.As` unwraps to find it. The `Limit` field then lets you put the actual byte limit in the response so the caller knows what to send instead.
- Where in the handler must the wrap happen?Before any read of the body. Assign `r.Body = http.MaxBytesReader(w, r.Body, n)` as the first statement, or push it into middleware or `http.MaxBytesHandler` so no route can forget. Wrapping after the body has been read is a no-op: the bytes are already in memory.
- Does capping the body also protect the server's header parsing?No. Request headers are bounded separately by `Server.MaxHeaderBytes`, which defaults to 1 MB and is enforced before your handler runs. A body cap and a header cap cover different parts of the request, and neither substitutes for the other.
It is a turnstile with a counter, not a bouncer: it stops letting bytes through and raises an alarm, but somebody still has to tell the visitor why they were turned away.
saying these in an interview costs you the question
- Thinks http.MaxBytesReader replies with 413 automatically
- Wraps r.Body after the body has already been read
- Believes it limits the number of decoded fields, not bytes
- Confuses it with Server.MaxHeaderBytes, which covers headers only
- Expects a silent truncation at the limit like io.LimitReader
- Matches the over-limit error by comparing message strings