From 67a9e0657e60df8c78510065e8977d86ee17d01c Mon Sep 17 00:00:00 2001 From: Petr <26906365+chebyrash@users.noreply.github.com> Date: Mon, 24 Nov 2025 23:03:18 +0400 Subject: [PATCH] reverseproxy: Fix retries for requests with bodies (#7360) * capture the buffered body once, then reset clonedReq.Body before each retry * no copy * keep receiver name * set the buf to nil after extraction and only return it to pool if not nil --------- Co-authored-by: WeidiDeng --- .../caddyhttp/reverseproxy/reverseproxy.go | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/modules/caddyhttp/reverseproxy/reverseproxy.go b/modules/caddyhttp/reverseproxy/reverseproxy.go index d207e240e..13bbee422 100644 --- a/modules/caddyhttp/reverseproxy/reverseproxy.go +++ b/modules/caddyhttp/reverseproxy/reverseproxy.go @@ -437,6 +437,20 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht reqHost := clonedReq.Host reqHeader := clonedReq.Header + // If the cloned request body was fully buffered, keep a reference to its + // buffer so we can reuse it across retries and return it to the pool + // once we’re done. + var bufferedReqBody *bytes.Buffer + if reqBodyBuf, ok := clonedReq.Body.(bodyReadCloser); ok && reqBodyBuf.body == nil && reqBodyBuf.buf != nil { + bufferedReqBody = reqBodyBuf.buf + reqBodyBuf.buf = nil + + defer func() { + bufferedReqBody.Reset() + bufPool.Put(bufferedReqBody) + }() + } + start := time.Now() defer func() { // total proxying duration, including time spent on LB and retries @@ -455,8 +469,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht // and reusable, so if a backend partially or fully reads the body but then // produces an error, the request can be repeated to the next backend with // the full body (retries should only happen for idempotent requests) (see #6259) - if reqBodyBuf, ok := r.Body.(bodyReadCloser); ok && reqBodyBuf.body == nil { - r.Body = io.NopCloser(bytes.NewReader(reqBodyBuf.buf.Bytes())) + if bufferedReqBody != nil { + clonedReq.Body = io.NopCloser(bytes.NewReader(bufferedReqBody.Bytes())) } var done bool @@ -1538,7 +1552,12 @@ type bodyReadCloser struct { } func (brc bodyReadCloser) Close() error { - bufPool.Put(brc.buf) + // Inside this package this will be set to nil for fully-buffered + // requests due to the possibility of retrial. + if brc.buf != nil { + bufPool.Put(brc.buf) + } + // For fully-buffered bodies, body is nil, so Close is a no-op. if brc.body != nil { return brc.body.Close() }