skip to content

Decoding Untrusted Input

Parsing bytes a stranger sent: capping the body with http.MaxBytesReader, DisallowUnknownFields, and the checks a decoded struct still needs. Interviewers want decode treated as a boundary.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

explore

questions

14

In a Go HTTP handler, what does http.MaxBytesReader do when a client sends a body larger than the limit?

level: juniorimportance: must knowfreq 58%

answer

  1. wrap the body before anything reads it
  2. the limit is in bytes, not fields
  3. a typed error, not a silent stop
  4. the handler still writes the status code
  5. errors.As gives you the limit that was hit

basics

~20 s

http.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 lines
go
r.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

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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
open as a page

Why does json.Unmarshal leave a struct field at zero when a key in the JSON is misspelled, and return no error?

level: juniorimportance: must knowfreq 60%

basics

~20 s

encoding/json skips any JSON object key that matches no field of the destination struct. A misspelled key is discarded, the field keeps its zero value, and Unmarshal returns nil. Silence is the documented default, not a bug.

open as a page

Why does json.Unmarshal succeed when a required field is missing from the JSON input?

level: juniorimportance: must knowfreq 74%

basics

~20 s

encoding/json has no required-field concept. A key absent from the input is simply never assigned, so the struct field keeps its zero value and Unmarshal returns nil. Requiredness is a rule your own code must check after decoding.

open as a page

Why is checking r.ContentLength not a safe size cap on an untrusted HTTP request body in Go?

level: middleimportance: should knowfreq 45%

basics

~20 s

r.ContentLength only reports what the client declared. A chunked request declares nothing and arrives as -1, so a length check silently passes exactly the requests whose size is unknown. Only a cap on bytes actually read bounds the allocation.

open as a page

What does json.Decoder.DisallowUnknownFields change about a Decode call that reads an unexpected key?

level: middleimportance: should knowfreq 50%

basics

~20 s

It makes a key that matches no field of the destination struct an error instead of a silent skip. Decode then returns an error naming the first such key, for example: json: unknown field "maxConnnections".

open as a page

Should a request type validate inside its UnmarshalJSON method or in a separate Validate method?

level: middleimportance: should knowfreq 48%

basics

~20 s

Prefer a separate Validate method. Validation inside UnmarshalJSON runs only when the value arrives as JSON, blurs malformed syntax with invalid content, and can leave the receiver half-populated. A Validate method covers every construction path and is trivial to test.

open as a page

A webhook receiver caps bodies at 1 MB, yet its heap profile spikes on deeply nested JSON. Why?

level: seniorimportance: should knowfreq 36%

basics

~20 s

A byte cap bounds input, not the decoded graph. Nested arrays decoded into an any allocate a fresh slice and interface value per level, so a megabyte of brackets becomes many megabytes of small heap objects — multiplied by every request in flight.

open as a page

An operator edits a Go daemon's JSON config and reloads it, but the setting has no effect and nothing errors. How do you diagnose it?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Prove which bytes the process read, then re-decode those bytes with a json.Decoder using DisallowUnknownFields, which names the first key no field matched. If that passes, look for a duplicate key later in the file or a later layer overriding the value.

open as a page

In Go, a request struct decoded from JSON carries an empty TenantID three layers deep. How do you keep invalid decoded values out of the rest of the service?

level: seniorimportance: should knowfreq 44%

basics

~20 s

Convert at the edge. The handler decodes a wire struct, then builds a separate domain value through a constructor that returns an error, and only that value travels inward. Deeper layers then have no way to receive an unchecked request.

open as a page

Your platform caps every request body at 1 MB and a team's legitimate 5 MB payload now returns 413. How do you decide?

level: principalimportance: should knowfreq 28%

basics

~20 s

Do not raise the global default. Price the request — cap times decoded expansion times requests in flight — then grant a per-route exception with its own cap and concurrency bound, or change the payload shape so the large case never travels in a request body.

open as a page

How do you decide whether a shared Go config loader rejects unknown JSON keys across a fleet?

level: principalimportance: should knowfreq 28%

basics

~20 s

Decide per stage, not per fleet. Reject unknown keys where failing is cheap and early — CI validation and process startup — and report rather than reject on hot reload of a serving process, with the unknown keys always named in logs.

open as a page

Why does io.LimitReader in front of a JSON decoder turn an over-sized request into a parse error?

level: middleimportance: nice to knowfreq 38%

basics

~20 s

io.LimitReader reports io.EOF once its byte budget is spent, which looks exactly like the stream ending. The decoder sees a value cut off mid-way and reports truncated input, so the handler cannot tell an over-sized body from malformed JSON.

open as a page

How does encoding/json match a JSON key like "MAXCONNS" to a struct field named MaxConns?

level: middleimportance: nice to knowfreq 32%

basics

~10 s

encoding/json prefers an exact match on the field's name, and falls back to a match that ignores letter case. So MAXCONNS, maxconns and MaxCONNS all decode into MaxConns, while max_conns matches nothing.

open as a page

How would you write a Go fuzz target for a function that decodes and validates a request?

level: seniorimportance: nice to knowfreq 27%

basics

~20 s

Add a FuzzXxx function taking *testing.F, seed it with f.Add on real payloads, and inside f.Fuzz assert properties: parsing never panics, and a nil error always yields a value satisfying the invariants callers rely on.

open as a page