When must middleware use (*http.Request).Clone instead of WithContext?
answer
- one method copies more than the other
- shallow copy shares the maps
- mutating headers leaks to the caller
- the stream is still shared either way
basics
~20 sUse Clone whenever the middleware changes anything besides the context. WithContext returns a shallow copy sharing the Header map, URL and form values with the original, so a header written on the copy is visible through both; Clone deep-copies them.
solid answer
~40 s`WithContext` does `*r2 = *r` and swaps the context, so the returned request shares the caller's `Header` map, `*url.URL`, `Trailer` and form values. Attaching a context value is safe under that sharing; setting `r2.Header.Set("X-Tenant", id)` is not, because the outer middleware and anything else holding the original request now see the header too. `Clone(ctx)` exists for that case: it deep-copies `URL`, `Header`, `Trailer`, `TransferEncoding` and the form fields onto the new request. What it does not copy is the `Body` — both requests hold the same `io.ReadCloser`, so only one of them may read it. In practice: `WithContext` for attaching request-scoped values, `Clone` for a middleware that rewrites the request it forwards, and `Clone` for any request you derive to send elsewhere.
code
go · 8 linesfunc addTenantHeader(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// WithContext would share this header map with the caller's request.
r2 := r.Clone(r.Context())
r2.Header.Set("X-Tenant", "acme")
next.ServeHTTP(w, r2)
})
}go deeper
Know that there are two ways to copy a request and that only one of them gives you an independent header map; reach for the deep copy when you are changing headers.
Name what each method copies and what it shares, and explain why a header written on a shallow copy shows up in the request an outer frame still holds.
Bring the body caveat and the cost: the stream is shared by both copies, and cloning on a hot path that only attaches a value is wasted allocation you would catch in a benchmark.
Set the house rule for middleware that rewrites requests, so a mutation never leaks into a request another frame owns, and so derived outbound requests never share an inbound header map.
## Two copies, one difference Both methods have the same signature shape and both panic on a nil context: ```go func (r *Request) WithContext(ctx context.Context) *Request func (r *Request) Clone(ctx context.Context) *Request ``` `WithContext` allocates a new `Request` and copies the struct across field by field. That copies the *reference-shaped* fields as references: the `Header` is a `map[string][]string` and the copy holds the same map; `URL` is a pointer and the copy holds the same pointer; `Body` is an interface value and the copy holds the same reader; `Form` and `PostForm` are maps too. `Clone` starts the same way and then rebuilds the parts you would otherwise share: it copies the `*url.URL` into a new one, clones the `Header` and `Trailer` maps, copies the `TransferEncoding` slice, and clones `Form`, `PostForm` and the multipart form. What it deliberately does not do is duplicate the `Body` — a body is a stream, and there is no way to give two requests independent copies of one without buffering it. ## Why the sharing bites A middleware in a multi-tenant service resolves the tenant and wants downstream code — including an outbound call made by a handler — to see it as a header as well as a context value: ```go r2 := r.WithContext(ctx) r2.Header.Set("X-Tenant", id) // writes into the caller's map too next.ServeHTTP(w, r2) ``` The write lands in the one map both requests point at. That is invisible while the chain runs top to bottom, and it surfaces in the code that runs *after* `next.ServeHTTP` returns: an outer middleware that logs the request it was given now logs a header it never saw arrive, and a test asserting the inbound request was not modified fails in a way that looks impossible. The same class of surprise appears in the other direction — stripping an `Authorization` header from the copy strips it from the original. With `Clone`, the write is confined: ```go r2 := r.Clone(r.Context()) r2.Header.Set("X-Tenant", id) next.ServeHTTP(w, r2) ``` ## The body is still shared This is the detail that separates a candidate who has read the method from one who assumes "deep copy" means everything. After `Clone`, both requests reference the same body reader. Reading it through one drains it for the other; closing it through one closes it for both. If a middleware genuinely needs to read the body and let the handler read it too, the copy has to be explicit: read it into memory, then give each request a fresh reader over those bytes — and that is a decision with a memory cost, since a request body can be large. ## Choosing between them - Attaching a context value and nothing else — `WithContext`. It is one allocation and it is the idiomatic middleware move. - Rewriting headers, the URL path, or form values for the downstream handler — `Clone`, so the mutation cannot leak back into the request an outer frame still holds. - Building a request to send somewhere else out of an inbound one — `Clone`, and set the fields an outbound request needs; sharing the inbound header map with an outbound request is a mistake that is very hard to see in review. - Either way, pass a real context: both panic on nil, and `r.Context()` is the natural argument when you are not changing the context at all. ## The cost `Clone` is measurably more work than `WithContext`: two map clones, a URL copy, several slice copies. On a hot path that only attaches a value, using `Clone` "to be safe" allocates for nothing. The rule that survives review is behavioural rather than performance-driven: use the copy whose isolation matches what you are about to mutate, and say so in a comment when the choice is not obvious.
- Does Clone give the two requests independent bodies?No. `Body` is an `io.ReadCloser` and both requests hold the same one, so reading through either drains it for both. To let two consumers read it you must buffer the bytes yourself and hand each request a fresh reader over them, accepting the memory cost.
- If Clone is safer, why not use it everywhere?It does real work: cloning the header and trailer maps, copying the URL, and copying the form values. On a path that only attaches a context value that is pure allocation for no isolation you needed. Pick the copy whose isolation matches the mutation you are about to make.
- What do both methods do if you pass a nil context?Both panic. Neither will substitute a background context for you, so derive from `r.Context()` when you are not changing the context — `r.Clone(r.Context())` is the usual call when you only want the deep copy.
saying these in an interview costs you the question
- Thinks WithContext isolates the header map
- Believes Clone duplicates the request body
- Mutates headers on a WithContext copy and calls it safe
- Uses Clone on every request to be safe, without reason
- Assumes the two methods differ only in name