What is http.Request.GetBody, and which body types make net/http fill it in?
answer
- one field answers can this be sent again
- it returns a reader, not the bytes
- the constructor only knows three types
- buffer, reader, string reader
- nil means the request is single-use
basics
~20 sGetBody is a field on http.Request holding a func that returns a fresh copy of the request body. http.NewRequest and NewRequestWithContext populate it only for *bytes.Buffer, *bytes.Reader and *strings.Reader; any other reader leaves it nil.
solid answer
~40 s`Request.GetBody` has type `func() (io.ReadCloser, error)` and exists to answer one question: can this request be sent again? Calling it returns a brand-new reader over the same payload. `http.NewRequest` and `http.NewRequestWithContext` set it automatically when the body you passed is a `*bytes.Buffer`, `*bytes.Reader` or `*strings.Reader`, because for those three the package knows the full contents and the length, so it also sets `ContentLength`. Pass any other `io.Reader` — an `*os.File`, a pipe, a custom wrapper — and `GetBody` stays nil, `ContentLength` stays 0 meaning unknown, and the request is not replayable. A retry loop should treat `GetBody != nil` as its licence to attempt again, and if you build a request by hand you set both `GetBody` and `ContentLength` yourself.
code
go · 12 linespayload := []byte(`{"event":"invoice.paid"}`)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(payload))
if err != nil {
return err
}
// req.ContentLength is set, and req.GetBody returns a fresh reader over payload.
fresh, err := req.GetBody()
if err != nil {
return err
}
defer fresh.Close()go deeper
Know that GetBody exists and that its job is to hand back a fresh reader over the same payload, so a request can be written to the wire more than once.
Be able to name the three body types the constructors recognise, explain why ContentLength is set only for those, and write the closure by hand for a body built another way.
Use GetBody as the explicit guard in your own retry code: a nil GetBody means this request must not be attempted twice, and the client should surface that rather than send a partial payload.
Decide what your shared client requires of callers — whether it accepts an arbitrary io.Reader and quietly loses replayability, or demands bytes it can buffer and bounds the memory that implies.
## What the field is for ```go type Request struct { Body io.ReadCloser GetBody func() (io.ReadCloser, error) // ... } ``` `Body` is the one-shot stream that gets written to the connection. `GetBody` is the recipe for making another one. Every call to it must return a reader positioned at the start of the same payload, independent of any reader handed out before. It is the single place in `net/http` where "this request can be sent more than once" is expressed as data rather than as a comment. ## When the constructor fills it in `http.NewRequest(method, url string, body io.Reader)` and its context-carrying twin `http.NewRequestWithContext(ctx, method, url, body)` inspect the dynamic type of `body`. For exactly three standard-library types — `*bytes.Buffer`, `*bytes.Reader` and `*strings.Reader` — they can ask the value for its remaining length, so they set `ContentLength` and install a `GetBody` closure that reconstructs a reader over the same underlying bytes. As a small extra: when such a body is empty, the constructor sets `Body` to `http.NoBody` rather than a live reader. For anything else the constructor cannot know the length without consuming the stream, and it will not do that behind your back. So `ContentLength` is left at 0 — which, for a client request with a non-nil body, means *unknown* and results in chunked transfer encoding — and `GetBody` is left nil. This is why the three types matter in practice: the difference between `bytes.NewReader(payload)` and `io.NopCloser(bytes.NewReader(payload))` is the difference between a replayable request and an unreplayable one, even though both wrap the same bytes. ## Who calls it besides your code Two parts of `net/http` use `GetBody` on your behalf. The `Client` uses it when it has to reissue a request, and the `Transport` uses it when a request fails on a connection it had picked out of its idle set — a connection the server may have closed while it sat idle. That transport-level retry is deliberately conservative: it only happens when the request is replayable (`Body` nil, or `GetBody` non-nil) *and* the method is one of GET, HEAD, OPTIONS or TRACE, or the request carries an idempotency header. It is not a general retry facility, and you should not count on it as one — it exists to paper over stale pooled connections, not over a failing server. ## Setting it yourself When you construct the body some other way, wire both fields explicitly: ```go req.ContentLength = int64(len(payload)) req.GetBody = func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(payload)), nil } req.Body, _ = req.GetBody() ``` Note that `Body` itself is just the first product of `GetBody`. Writing it this way makes it structurally impossible for the two to disagree, which is the failure mode when someone sets `GetBody` but leaves `Body` pointing at a different reader. The closure must be safe to call repeatedly and must not depend on state the previous attempt mutated. Capturing an immutable `[]byte` is the easy correct case. Capturing an `*os.File` and calling `Seek(0, io.SeekStart)` inside the closure works too, but only while the file is still open and unmodified — which is exactly the kind of assumption that breaks when a retry happens minutes later. ## Using it in a retry loop A delivery worker that wants to attempt the same POST several times can either rebuild the whole request or clone it and refresh the body: ```go attempt := req.Clone(ctx) if req.GetBody != nil { body, err := req.GetBody() if err != nil { return err } attempt.Body = body } ``` `Request.Clone` gives you a deep-enough copy that the Transport's mutations on one attempt do not leak into the next, and the `GetBody != nil` check is the honest guard: if the caller handed you an unbuffered stream, you cannot retry, and the right behaviour is to say so rather than to send a truncated payload. ## The rule to remember `Body` is the bytes for this attempt. `GetBody` is the promise that there can be another attempt. If it is nil, the request is single-use, and no amount of retry logic above it can change that.
- What does ContentLength being 0 with a non-nil body actually mean for a client request?It means unknown, not empty. net/http then sends the request with chunked transfer encoding and reads the body until EOF. If you genuinely have zero bytes to send, use http.NoBody or a nil body so the request is written with an explicit zero length instead.
- Does a non-nil GetBody mean net/http will retry the request for you?No. GetBody is a precondition, not a policy. The Transport reissues a request only when it failed on a connection taken from its idle set, and only for GET, HEAD, OPTIONS and TRACE or a request carrying an idempotency header. Everything else is your own retry code's job.
- Is a GetBody closure that seeks an *os.File back to the start acceptable?It compiles and it works in the happy case, but it ties replayability to a file handle that must still be open and unchanged when the retry runs, possibly seconds later. If the payload is small, capture a []byte instead; if it is large, accept that the request is not replayable and design the delivery around re-reading from durable storage.
saying these in an interview costs you the question
- Believes GetBody is populated for any io.Reader you pass
- Sets GetBody but leaves Body pointing at a different reader
- Reads ContentLength 0 with a body as meaning an empty payload
- Assumes a non-nil GetBody makes net/http retry POSTs automatically
- Writes a GetBody closure that returns the same already-used reader