skip to content

What is http.Request.GetBody, and which body types make net/http fill it in?

level: middleimportance: should knowfreq 40%

answer

  1. one field answers can this be sent again
  2. it returns a reader, not the bytes
  3. the constructor only knows three types
  4. buffer, reader, string reader
  5. nil means the request is single-use

basics

~20 s

GetBody 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 lines
go
payload := []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

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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