An HTTP request can appear to have survived a redirect or retry while concealing the question that matters operationally: did net/http send a newly recreated body, or did the next attempt inherit an exhausted reader?
That distinction is narrow, observable, and worth testing directly. Go's http.Request.GetBody is an optional function that returns a new copy of the request body for client-side replay. The official Request documentation explicitly ties it to cases where a body must be read more than once, while Client documents that HTTP 307 and 308 redirects preserve the method and body only when GetBody is available. NewRequest/NewRequestWithContext populate GetBody automatically for several standard reader types.
The harder case is Transport retry. The current Transport documentation says a network-error retry is conditional: the connection must already have been used successfully, the request must be considered replayable/idempotent, and a request with a body needs GetBody available. A successful final 200 therefore does not prove the body lifecycle by itself.
This playbook builds a disposable loopback fixture that records what the server actually receives at the first and second boundaries. It separates documented contract, predicted outcome, captured observation, and release interpretation. No production system, customer data, secret, remote dependency, or fabricated transcript is required.
Define the acceptance contract, baseline, and ownership boundary
The acceptance question is not “did client.Do return nil?” It is not even “did the second request succeed?” The acceptance question is:
When net/http crosses a boundary that requires re-sending a request body, does the next attempt receive a fresh reader whose bytes equal the original payload?
For a redirect case, the boundary is the transition from the request that receives HTTP 307 or 308 to the redirected request. For a Transport retry, the boundary is the transition from a failed network attempt on a previously usable connection to the internally retried attempt.
The official API gives us a useful state model. Request.GetBody is optional and returns a new copy of Body; it is intended for client requests when the body may need to be read more than once. Its existence does not replace Body: the documentation says Body still has to be set. io.ReadCloser, meanwhile, is only the composition of Reader and Closer; that interface alone says nothing about rewinding, cloning, or replaying byte state.
That gives the test five evidence fields:
Evidence field | What it proves |
Runtime and OS | Which implementation was actually exercised |
req.GetBody != nil | Whether a replay factory is present |
First server-observed body | What was consumed on the original attempt |
Second server-observed body | Whether replay crossed the boundary intact |
Final client result | What the caller saw after redirect/retry processing |
A release candidate is ACCEPT only when the expected replay boundary is crossed and the server independently records the complete original body after that boundary. A green status without that second observation is insufficient.
A REPAIR decision means the failure is understood and local: for example, the request was constructed around a one-shot reader, GetBody was nil, and the code can be changed to create replayable bodies correctly.
A HOLD decision applies when the observed state cannot be mapped to the documented lifecycle. Typical reasons include an unpinned runtime, an uncontrolled proxy, an unexpected HTTP/2 path, or evidence that does not establish which request attempt produced which bytes.
A RECONCILE decision applies when an earlier attempt may already have produced application-visible effects before retry. Body correctness and effect duplication are separate problems. A byte-for-byte replay can be technically correct while the operation still requires idempotency or downstream reconciliation.
Pin versions and fixture ownership
Capture the environment before interpreting behavior:
go version
go env GOOS GOARCH
uname -srmoThe preparation capture used for this article was:
go version go1.23.2 linux/amd64
Linux 6.18.57 x86_64 GNU/LinuxThat is execution evidence for this fixture, not a claim that Go 1.23.2 is current. The pkg.go.dev pages consulted during research displayed the standard-library documentation under Go 1.27.3 on November 6, 2026. This version split matters: implementation observations belong to the pinned fixture runtime; API claims belong to the cited documentation. Any release acceptance run should therefore repeat the fixture under the exact runtime being approved.
Refonte Learning's HTTP Cache Revalidation: Validate Conditional Requests tests representation freshness, validator use, and cache isolation rather than Go request-body reconstruction. CloudFront Cache Isolation: Audit Personalized Responses studies forwarding, cache keys, and user-specific response isolation rather than Request.GetBody.
Build the disposable fixture and record the documented behavior
The fixture needs two kinds of local server.
The first can use httptest.Server. Its job is to make a 307 redirect observable: /start reads the original body, responds with 307 Temporary Redirect, and points the client to /sink. /sink reads the body again. The server ledger therefore distinguishes “the client eventually returned success” from “the same payload arrived after redirection.”
The second server must exercise Transport retry rather than application-level retry. That requires more control. A raw TCP listener accepts one connection, successfully serves a warm-up request so the connection becomes previously used, then reads a POST and closes the socket without returning a response. The client sees a response-read failure on a reused connection. A second accepted connection records whether Transport retries and, if it does, whether the new attempt contains the original payload.
That shape maps directly to the current Transport contract: retries after network error are constrained by prior connection use and request replayability; a request with a body requires GetBody for replay. The Transport source's body-rewind path closes the previously used body when appropriate, calls GetBody, and installs the returned reader in a copied request.
The redirect behavior is separately documented. HTTP 301, 302, and 303 generally transition to GET/HEAD semantics with no original body, while 307 and 308 preserve method and body provided GetBody is defined. The client.go implementation also shows the negative branch: when a 307/308 needs to resend a non-empty body but the initiating request has no GetBody, redirect following is disabled rather than pretending that replay is possible.
NewRequest has special handling for bytes.Buffer, bytes.Reader, and *strings.Reader: it snapshots enough state to create a new reader when GetBody is called. This is why the positive control should use a standard supported body type rather than hand-installing GetBody; it validates the constructor behavior engineers commonly rely on.
A compact fixture looks like this:
package main
import (
"bufio"
"fmt"
"io"
"log"
"net"
"net/http"
"net/http/httptest"
"strings"
"sync"
"time"
)
type oneShot struct {
r strings.Reader
}
func (o oneShot) Read(p []byte) (int, error) {
return o.r.Read(p)
}
func (o oneShot) Close() error {
return nil
}
func redirectCases() {
var mu sync.Mutex
var seen []string
srv := httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r http.Request) {
b, := io.ReadAll(r.Body)
mu.Lock()
seen = append(seen, r.URL.Path+":"+string(b))
mu.Unlock()
if r.URL.Path == "/start" {
w.Header().Set("Location", "/sink")
w.WriteHeader(http.StatusTemporaryRedirect)
return
}
w.WriteHeader(http.StatusNoContent)
},
))
defer srv.Close()
payload := "alpha=1"
req, := http.NewRequest(
http.MethodPost,
srv.URL+"/start",
strings.NewReader(payload),
)
resp, err := srv.Client().Do(req)
if err != nil {
log.Fatal(err)
}
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
mu.Lock()
fmt.Printf(
"D1 status=%d getbody=%t seen=%q\n",
resp.StatusCode,
req.GetBody != nil,
seen,
)
seen = nil
mu.Unlock()
req2, _ := http.NewRequest(
http.MethodPost,
srv.URL+"/start",
&oneShot{r: strings.NewReader(payload)},
)
req2.ContentLength = int64(len(payload))
resp2, err := srv.Client().Do(req2)
if err != nil {
log.Fatal(err)
}
io.Copy(io.Discard, resp2.Body)
resp2.Body.Close()
mu.Lock()
fmt.Printf(
"D2 status=%d getbody=%t seen=%q\n",
resp2.StatusCode,
req2.GetBody != nil,
seen,
)
mu.Unlock()
}
func readReq(br bufio.Reader) (http.Request, []byte, error) {
req, err := http.ReadRequest(br)
if err != nil {
return nil, nil, err
}
b, err := io.ReadAll(req.Body)
req.Body.Close()
return req, b, err
}The remaining retry server is intentionally HTTP/1.1. That is not an assertion that HTTP/2 has identical failure boundaries; it is a control decision that makes connection reuse and socket closure directly observable.
State the false-positive hypothesis
The false-positive hypothesis is simple:
A final successful response can be mistaken for proof that the body was replayed correctly.
For the redirect case, imagine inspecting only resp.StatusCode == 204. That tells us /sink eventually answered, but without server evidence it does not tell us what bytes /sink received.
For Transport retry, a final 200 OK is even more misleading. A retry may have occurred, or the first attempt may have succeeded; the operator cannot distinguish the two from the final status alone. Even if retry definitely occurred, a custom retry layer outside net/http might reuse an exhausted reader incorrectly while still reaching an endpoint that accepts an empty body.
The negative control must therefore remove only the replay capability while keeping the payload and request shape otherwise comparable. The oneShot wrapper above is useful because it is an io.ReadCloser-compatible reader but is not one of the standard body types for which NewRequest installs GetBody. The predicted state is:
Body non-nil
ContentLength > 0
GetBody == nilThat negative state is not “bad Go” in isolation. Some requests never need replay. It becomes unacceptable only when the surrounding contract requires redirect or retry semantics that depend on recreating the body.
Reproduce the misleading success signal and run the negative controls
Complete the raw retry server with one warm-up request, a forced connection failure, and a second accept:
func retryCase(replayable bool) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
log.Fatal(err)
}
defer ln.Close()
addr := "http://" + ln.Addr().String()
ledger := make(chan string, 8)
go func() {
c1, err := ln.Accept()
if err != nil {
return
}
br := bufio.NewReader(c1)
r1, b1, err := readReq(br)
if err != nil {
c1.Close()
return
}
ledger <- fmt.Sprintf(
"warmup %s %q", r1.URL.Path, string(b1),
)
fmt.Fprint(c1,
"HTTP/1.1 204 No Content\r\n"+
"Content-Length: 0\r\n"+
"Connection: keep-alive\r\n\r\n")
r2, b2, err := readReq(br)
if err != nil {
c1.Close()
return
}
ledger <- fmt.Sprintf(
"attempt1 %s %q", r2.URL.Path, string(b2),
)
// Deliberately send no response.
c1.Close()
c2, err := ln.Accept()
if err != nil {
return
}
br2 := bufio.NewReader(c2)
r3, b3, err := readReq(br2)
if err == nil {
ledger <- fmt.Sprintf(
"attempt2 %s %q",
r3.URL.Path,
string(b3),
)
fmt.Fprint(c2,
"HTTP/1.1 200 OK\r\n"+
"Content-Length: 2\r\n"+
"Connection: close\r\n\r\nOK")
}
c2.Close()
}()
tr := &http.Transport{}
client := &http.Client{
Transport: tr,
Timeout: 2 time.Second,
}
defer tr.CloseIdleConnections()
warm, err := client.Get(addr + "/warm")
if err != nil {
log.Fatal(err)
}
io.Copy(io.Discard, warm.Body)
warm.Body.Close()
payload := "bravo=2"
var req http.Request
if replayable {
req, = http.NewRequest(
http.MethodPost,
addr+"/retry",
strings.NewReader(payload),
)
} else {
req, = http.NewRequest(
http.MethodPost,
addr+"/retry",
&oneShot{r: strings.NewReader(payload)},
)
req.ContentLength = int64(len(payload))
}
req.Header.Set("Idempotency-Key", "fixture-1")
resp, err := client.Do(req)
if err != nil {
fmt.Printf(
"R replayable=%t getbody=%t err=%q\n",
replayable,
req.GetBody != nil,
err.Error(),
)
} else {
b, _ := io.ReadAll(resp.Body)
resp.Body.Close()
fmt.Printf(
"R replayable=%t getbody=%t status=%d body=%q\n",
replayable,
req.GetBody != nil,
resp.StatusCode,
string(b),
)
}
}The Idempotency-Key is not decorative. Current Transport documentation states that HTTP methods GET, HEAD, OPTIONS, and TRACE are treated as idempotent, and requests carrying Idempotency-Key or X-Idempotency-Key are also treated as replayable for this decision. A POST body with GetBody but no replayability signal should not be generalized into “Transport retries arbitrary POSTs.”
This is an important distinction between redirect and retry. A 307/308 asks the client to repeat the request semantics at another URI; Client's redirect path checks whether the body can be recreated. Transport network retry has an additional replayability policy because the original attempt may have reached the server before the connection failed. The same GetBody mechanism participates in both paths, but the eligibility rules are not identical.
The captured preparation transcript on Go 1.23.2 was:
D1 status=204 getbody=true seen=["/start:alpha=1" "/sink:alpha=1"]
D2 status=307 getbody=false seen=["/start:alpha=1"]
R replayable=true getbody=true status=200 body="OK"
warmup /warm ""
attempt1 /retry "bravo=2"
attempt2 /retry "bravo=2"
R replayable=false getbody=false err="Post \"http://127.0.0.1:45537/retry\": EOF"
warmup /warm ""
attempt1 /retry "bravo=2"That is the boundary evidence.
D1 did not merely end with 204. /start observed alpha=1, and /sink independently observed alpha=1 after the 307. The request also had GetBody != nil.
D2 preserved the original 307 instead of following it. The server saw the payload only at /start; there was no /sink observation. That matches the documented rule that a 307/308 requiring a non-empty body is not automatically followed when GetBody is unavailable.
The retry positive control is stronger than the caller's final 200. The raw server records the payload once on the first connection, deliberately closes that connection before a response, then records the same payload on a second connection. That demonstrates body reconstruction across an actual Transport retry boundary in the pinned runtime.
The retry negative control is equally important. It records the payload on attempt one, receives no second request, and the caller gets EOF. The difference is not endpoint behavior or payload length. It is replay eligibility, including the absence of GetBody.
For comparison, Refonte Learning's When asyncio.gather Raises, Which Tasks Are Still Running? uses a similar evidence discipline (aggregate outcome is separated from worker/effect state), but its subject is Python task lifetime, not HTTP request-body reconstruction. The overlap is methodological, not experimental.
Compare the competing timelines
The positive retry timeline is:
connection 1 established
|
warm-up request succeeds
|
connection 1 becomes reusable
|
POST attempt 1 sends "bravo=2"
|
server reads body, closes without response
|
Transport classifies request as replayable
|
Transport needs a new body
|
GetBody() returns fresh reader
|
connection 2 established
|
POST attempt 2 sends "bravo=2"
|
server returns 200The negative timeline is:
connection 1 established
|
warm-up request succeeds
|
POST attempt 1 sends "bravo=2"
|
server closes without response
|
Transport evaluates retry
|
body cannot be rewound: GetBody == nil
|
no attempt 2
|
caller receives EOFThose timelines explain why a single application log such as request succeeded after retry is weak evidence. It collapses eligibility, body recreation, second-attempt transmission, server receipt, and response into one statement.
Measure identity, lifecycle, and completed effects independently
A subtle mistake is to ask whether “the same request” was replayed. That phrase can refer to several identities that should not be conflated.
The logical operation may be the same. The *http.Request value exposed to application code may originate from the same initial variable. The retry implementation may copy request metadata internally. The body reader, however, must not merely retain logical identity; it must provide readable byte state from the beginning.
The source's retry helper is explicit about that lifecycle. When rewinding is required, the Transport closes the previous body if appropriate, requires GetBody, calls it, copies the request value, and replaces Body with the newly created reader. This is why pointer identity is not the right evidence target. The body lifecycle is.
An additional negative control makes the exhausted-reader failure visible outside the automatic Transport retry path:
func exhaustedManualReuse() {
payload := "charlie=3"
var mu sync.Mutex
var seen []string
srv := httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
b, := io.ReadAll(r.Body)
mu.Lock()
seen = append(seen, string(b))
mu.Unlock()
w.WriteHeader(http.StatusOK)
},
))
defer srv.Close()
req, := http.NewRequest(
http.MethodPost,
srv.URL,
&oneShot{r: strings.NewReader(payload)},
)
req.ContentLength = int64(len(payload))
client := srv.Client()
resp1, err1 := client.Do(req)
if resp1 != nil {
resp1.Body.Close()
}
resp2, err2 := client.Do(req)
if resp2 != nil {
resp2.Body.Close()
}
fmt.Printf(
"N1 firstErr=%v secondErr=%v seen=%q\n",
err1,
err2,
seen,
)
}Captured output:
N1 firstErr=<nil>
secondErr=Post "http://127.0.0.1:39277":
net/http: HTTP/1.x transport connection broken:
http: ContentLength=9 with Body length 0
seen=["charlie=3"]That result is particularly useful during incident diagnosis because it demonstrates the exhausted-reader model directly. The first send consumes the reader. A second Do on the same request does not magically invoke GetBody merely because application code reuses the request. The Go source tests themselves warn that a Request cannot simply be reused across Client.Do calls without rewinding Body; GetBody is used for Transport failure/redirect handling, not as a general automatic reset for arbitrary repeated Do calls.
This distinction prevents a common false repair. Code like this is unsafe:
req, _ := http.NewRequest("POST", url, body)
for attempt := 0; attempt < 3; attempt++ {
resp, err := client.Do(req)
// retry same req
}Even when req.GetBody happens to be populated, an application-owned retry loop should not assume Client.Do rewinds the original request before every new call. If the application owns the retry boundary, it should also own explicit body reconstruction.
A better pattern is to keep immutable payload state and construct each attempt from it:
payload := []byte{"op":"update"})
for attempt := 0; attempt < maxAttempts; attempt++ {
req, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
target,
bytes.NewReader(payload),
)
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", key)
resp, err := client.Do(req)
// classify response/error before deciding another attempt
}Alternatively, if the request metadata must be preserved, explicitly call the original request's GetBody, clone/copy the request appropriately, and attach that new body. What matters is not style preference; it is that the code makes ownership of replay state unambiguous.
A successful replay also does not prove that retry is safe for the operation. The first server in the retry fixture reads the entire body before the socket closes. In a real service, the application behind that server might already have committed an effect. Transport cannot infer whether that effect happened merely from the missing response. That is precisely why retry eligibility and application-level idempotency remain separate concerns. The current Transport's use of idempotent methods or idempotency-key headers is a replay policy, not proof that a downstream implementation has reconciled duplicated effects.
Preserve independent observation
The server ledger should remain independent from client logs.
Do not implement the assertion solely as:
fmt.Println("retrying with fresh body")That records intention.
Prefer a ledger owned by the receiving side:
case_id
connection_id
path
method
body_length
body_digest
body_text_for_synthetic_fixture
arrival_orderFor production-like payload sizes, a digest is preferable to dumping the full body. For this disposable fixture, the body is synthetic and tiny, so recording the exact text is acceptable.
The same evidence rule appears in other lifecycle investigations. Refonte Learning's One Exception Can Stop a Java Periodic Task distinguishes task outcome from executor liveness rather than inferring one from the other. Here, the analogous split is client result versus receiver-observed request body.
Build the evidence ledger, repair the boundary, and recheck
A case-by-case ledger should be compact enough to review during release acceptance.
Case | Input construction | Expected boundary | Captured observation on Go 1.23.2 | Decision |
D1 | strings.NewReader, POST, 307 | Redirect follows with fresh body | /start:alpha=1, /sink:alpha=1, final 204 | ACCEPT |
D2 | custom one-shot reader, POST, 307 | Redirect cannot replay | /start:alpha=1, no /sink, final 307 | ACCEPT as negative control |
R1 | strings.NewReader, POST + Idempotency-Key | Transport retries after forced read failure | attempt 1 and attempt 2 both bravo=2, final 200 | ACCEPT |
R2 | one-shot reader, same retry setup | No replay because GetBody=nil | attempt 1 only, caller EOF | ACCEPT as negative control |
N1 | manually reuse same exhausted request | Second application call must not be mistaken for rewind | first body received; second call errors with Body length 0 | ACCEPT as negative control |
The strongest evidence is contrastive. R1 alone shows that one replayable request succeeded under the fixture. R2 shows that removing GetBody changes behavior at the expected boundary. N1 eliminates the theory that any *http.Request can simply be reused indefinitely because a previous send succeeded.
The repair is correspondingly narrow: make the body source recreatable at every boundary owned by your code, and preserve the documented replay conditions for boundaries owned by net/http.
For constructor-owned replay, prefer body values that NewRequest understands when practical:
req, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
target,
strings.NewReader(payload),
)or:
req, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
target,
bytes.NewReader(payloadBytes),
)The standard-library source shows automatic GetBody installation for bytes.Buffer, bytes.Reader, and *strings.Reader. Do not generalize that behavior to an arbitrary io.Reader.
For custom streaming bodies that genuinely must be replayable, provide an explicit factory whose semantics are clear:
payload := append([]byte(nil), original...)
req, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
target,
io.NopCloser(bytes.NewReader(payload)),
)
if err != nil {
return err
}
req.ContentLength = int64(len(payload))
req.GetBody = func() (io.ReadCloser, error) {
return io.NopCloser(bytes.NewReader(payload)), nil
}This trades memory for deterministic replay. That may be correct for bounded payloads and inappropriate for very large or streaming bodies. The acceptance contract should name that limit rather than silently buffering arbitrary traffic.
For file-backed payloads, a replay factory can reopen the file and seek/read from the known beginning, but file identity and immutability now become part of the lifecycle contract. A pathname that can be replaced between attempts is not equivalent to an immutable byte snapshot. The ledger should therefore record the source policy (snapshot, immutable file, generated body), not merely “GetBody present.”
Recheck after the recovery action
The repair is not complete when req.GetBody != nil becomes true.
Repeat the original negative case without weakening the fixture. The recovery test should satisfy all of these conditions:
same synthetic payload
same 307 or forced retry boundary
same server-side ledger
same response-failure injection
same timeout
only body reconstruction policy changedThen require second-boundary evidence.
For a repaired redirect case:
/start receives alpha=1
307 returned
/sink receives alpha=1
final status is downstream responseFor a repaired retry case:
attempt1 receives bravo=2
first connection closes before response
attempt2 receives bravo=2
final response arrivesIf the final response succeeds but the ledger lacks attempt two, classify the fixture as invalid or the injected fault as ineffective. Do not promote that run to acceptance.
If attempt two arrives with the wrong bytes, classify REPAIR even if the endpoint returns 200. That is exactly the type of false success this playbook is designed to expose.
If attempt one may have performed a state-changing operation before the injected connection failure, classify the HTTP replay mechanism independently from the application effect. GetBody can be ACCEPT while the end-to-end operation is RECONCILE.
Reconcile partial work and make the release-gate decision
HTTP replay is not rollback.
The raw retry fixture deliberately creates an ambiguous application moment: the server has read the complete request body before it closes the connection. From the client's perspective, there is no usable response. From the receiving side, the request definitely arrived.
That is the correct place to separate transport evidence from effect evidence.
Suppose an endpoint applies transfer=10, commits it, and then its connection fails before the response reaches the client. A replayable body lets Transport send those bytes again under eligible retry conditions. That demonstrates correct body reconstruction; it does not establish whether the second arrival is safe. An application-level idempotency mechanism must decide whether the second logical request is duplicate, continuation, or new work.
Therefore the evidence ledger should add effect fields for state-changing systems:
logical_operation_id
attempt_id
server_received
application_started
effect_committed
idempotency_record_found
response_emitted
client_response_seenThe local body fixture intentionally avoids real state changes, so it cannot prove application idempotency. That limit should remain explicit.
Refonte Learning's Python Subprocess Exit Is Not Pipe EOF makes the same category of operational distinction in a different domain: process completion and stream completion are separate lifecycle facts. Here, transport completion and application-effect completion are likewise different evidence boundaries.
A practical release matrix is:
Evidence state | Decision | Required action |
Replay-required case crosses boundary with identical body; controls behave as predicted | ACCEPT | Preserve fixture and runtime capture with release evidence |
Body cannot be recreated, but cause is isolated to request construction | REPAIR | Provide a correct body factory or reconstruct each application-owned attempt |
Runtime, protocol path, retry trigger, or attempt identity is ambiguous | HOLD | Improve observability before changing production behavior |
First attempt may have committed externally visible work before replay | RECONCILE | Check operation-level idempotency/effect state before treating retry as safe |
Decide accept, repair, hold or reconcile
Use ACCEPT only when all of the following are true for the runtime under approval:
go version and OS/architecture are captured.
The replay-triggering boundary is deterministic and independently observed.
The positive control has a recreatable body.
The negative control removes that ability and fails differently at the expected boundary.
The second receiver observation matches the original payload exactly.
Any effect-level ambiguity is outside the body test and separately accounted for.
Use REPAIR when evidence is already sufficient to identify a violated body lifecycle. Examples include a custom reader with nil GetBody, a retry loop that reuses the original consumed request, or a GetBody function that recreates different bytes.
Use HOLD when evidence cannot discriminate among competing explanations. A final EOF with no connection ledger, for example, does not establish that GetBody caused the failure. A final 200 with no attempt ledger does not establish that retry occurred.
Use RECONCILE when transport replay worked but the first attempt's external effect is uncertain. That state is neither a body-replay failure nor a clean acceptance of end-to-end semantics.
One additional caveat belongs in the release record: the current standard-library documentation may evolve after the runtime being validated. The Go 1.8 release notes show that Request.GetBody and 307/308 body-preserving redirect support were introduced together, and that NewRequest automatically populated GetBody for common body types at that time. Current source and docs should still be checked for the exact target release rather than assuming historical behavior has remained implementation-identical.
Document limits, operational ownership, and the Refonte Learning context
This fixture proves a deliberately narrow claim.
It can prove that, under one pinned Go runtime and controlled HTTP/1.1 loopback setup, a request body constructed from a supported replayable reader was recreated across a 307 redirect and across a specific eligible Transport retry, while corresponding one-shot-reader controls did not cross those boundaries.
It does not prove that every network failure triggers a retry. The Transport contract is conditional and conservative. It does not prove that every POST is retryable. It does not prove that HTTP/2 exposes the same socket-level failure choreography. It does not prove downstream side effects are idempotent. It does not establish that an arbitrary third-party RoundTripper follows the standard Transport's lifecycle. It does not prove that application-owned repeated calls to Do will rewind a request automatically.
It also should not be generalized from GetBody != nil alone. The factory can return an error, return different bytes, read mutable state, or reopen a changed resource. Correctness requires both availability and semantic fidelity.
Operational ownership should therefore be explicit:
Request-construction owner:
defines the canonical payload source
Retry-policy owner:
decides whether application-level retry is permitted
net/http boundary:
may perform documented redirect/Transport replay
Fixture owner:
keeps positive and negative controls runnable
Release owner:
records runtime, transcript, and decision
Effect owner:
reconciles any potentially duplicated operationThat separation keeps recovery practical. If R2 unexpectedly retries under a future runtime, do not “fix” the server first. Preserve the transcript, inspect the target release's current docs and source, and classify the discrepancy before changing application code. If R1 stops retrying, first determine whether retry eligibility, connection reuse, idempotency signaling, or body reconstruction changed.
The Refonte Learning Backend Development page currently describes a three-month program with a listed commitment of 10–12 hours per week and competencies including RESTful APIs, microservices, testing/debugging backend services, databases, authentication/authorization, and Docker/cloud deployment. The page specifically names Node.js, Express, MongoDB, and SQL in its curriculum; it does not provide evidence that this net/http/Go fixture is taught there. Engineers evaluating the program can therefore treat this playbook as an example of the evidence discipline useful in backend work, not as a claim about a Go-specific course module.
The final release artifact should remain small: the fixture source, environment capture, transcript, case ledger, and one decision line. For the captured Go 1.23.2 preparation run above, the body-replay mechanism itself is ACCEPT: positive redirect and Transport-retry cases crossed their intended boundaries with byte-identical bodies, and the one-shot controls failed at the corresponding replay boundaries. Any real state-changing service built on the same mechanism would still require its own independent RECONCILE assessment for effects that may have occurred before a retry.
