Skip to content

reverseproxy: prevent body close on dial-error retries#7547

Merged
francislavoie merged 1 commit intocaddyserver:masterfrom
tpaulus:tpaulus/7546-read-on-closed-body
Mar 4, 2026
Merged

reverseproxy: prevent body close on dial-error retries#7547
francislavoie merged 1 commit intocaddyserver:masterfrom
tpaulus:tpaulus/7546-read-on-closed-body

Conversation

@tpaulus
Copy link
Contributor

@tpaulus tpaulus commented Mar 4, 2026

Fixes #7546

cloneRequest shallow-copies Body, so clonedReq.Body and r.Body share the same io.ReadCloser. When Go's transport hits a dial error it calls req.Body.Close() before reading any bytes, which kills the original body for all subsequent retry attempts ("http: invalid Read on closed Body" → 502).

Wrap the body in io.NopCloser when retries are configured so the transport's Close is a no-op. The real body is closed by the HTTP server when the handler returns. For already-buffered bodies (via request_buffers) the buffer is extracted into the same NopCloser path so both cases share a single code block.

Assistance Disclosure

AI debugged the issue. I wrote the code, and AI generated the tests & commit message.

)

cloneRequest shallow-copies Body, so clonedReq.Body and r.Body share
the same io.ReadCloser. When Go's transport hits a dial error it calls
req.Body.Close() before reading any bytes, which kills the original
body for all subsequent retry attempts ("http: invalid Read on closed
Body" → 502).

Wrap the body in io.NopCloser when retries are configured so the
transport's Close is a no-op. The real body is closed by the HTTP
server when the handler returns. For already-buffered bodies (via
request_buffers) the buffer is extracted into the same NopCloser path
so both cases share a single code block.

Unlike full eager buffering this adds zero memory or latency overhead —
streaming is preserved and only the Close call is intercepted.
@francislavoie francislavoie added the bug 🐞 Something isn't working label Mar 4, 2026
@francislavoie francislavoie added this to the v2.11.2 milestone Mar 4, 2026
Copy link
Member

@francislavoie francislavoie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that makes a lot of sense. Didn't realize dials would close the body, that's surprising, but to be expected I suppose.

Thanks!

@tpaulus
Copy link
Contributor Author

tpaulus commented Mar 4, 2026

Props go to @jafowler for interrogating the AI long enough to identify and repro the issue.

@francislavoie francislavoie merged commit a5e7c6e into caddyserver:master Mar 4, 2026
27 checks passed
@github-actions github-actions bot mentioned this pull request Mar 6, 2026
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug 🐞 Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

reverse_proxy LoadBalancing Dial Timeout results in "http: invalid Read on closed Body"

2 participants