fix: Buffer request body for retry overrides, close intermediate response bodies#629
fix: Buffer request body for retry overrides, close intermediate response bodies#629danskmt wants to merge 15 commits into
Conversation
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
04c64a0 to
9c4597b
Compare
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
| realCloser io.Closer | ||
| } | ||
|
|
||
| func (b *noCloseSeekBody) Read(p []byte) (int, error) { return b.ReadSeeker.Read(p) } |
There was a problem hiding this comment.
This can probably be removed, but please verify.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
PR Reviewer Guide 🔍
|
| return io.NopCloser(bytes.NewBuffer(bodyBytes)), nil | ||
| } | ||
| firstBody, firstBodyErr := newGetBody() | ||
| return firstBody, newGetBody, nil, firstBodyErr |
There was a problem hiding this comment.
can you maybe return struct, error instead of all those firstBody, newGetBody, error, error
There was a problem hiding this comment.
I'm thinking of considering this type of cleanup for followup task
There was a problem hiding this comment.
sounds good to me


Description
Fixes CLI-1591 and addresses several connection/resource management gaps in the retry middleware.
1. Always buffer request body for retry overrides
POST request bodies were not preserved across retry attempts when a 429 triggered the per-status-code retry override (3 attempts despite
maxAttempts=1).Root cause: Body buffering was gated on
maxAttempts > 1, but the per-status-code override ingetMaxRetryAttemptsis only evaluated after the first response — by which point the body was already consumed and never buffered.Fix:
ensureGetBodyExistsnow always prepares a replayable body using three strategies in order of preference:GetBodyalready set (e.g.http.NewRequestwith*bytes.Reader) → use as-is, no copy.io.ReadSeeker(e.g.*os.File) → wrap to suppress Close, rewind withSeek(0).io.ReadAllinto memory buffer.2. Close intermediate response bodies between retries
The middleware returned
(response, retryErr)on retriable status codes, butbackoff.Retrydiscarded intermediate responses without closing their bodies — leaking one TCP connection per retry. This matches the drain+close behavior in stdlib'sClient.do.drainAndCloseis called eagerly when the retry decision is made (non-permanent errors). On permanent 503 wheregetErrorListreplacesresponse.Bodywith a buffer copy, the orphaned original transport body is also closed.3. Cleanup: remove dead code
Removed redundant
noCloseSeekBody.Readmethod — the embeddedio.ReadSeekeralready promotesRead.Ticket: CLI-1591
Test coverage
TestRetryMiddleware_429_POST_BodyPreservedAcrossRetriesTestRetryMiddleware_GetBodyPreset_SkipsBufferingTestRetryMiddleware_GetBodyNil_BuffersBodyTestRetryMiddleware_SeekableBody_NoBufferTestRetryMiddleware_IntermediateResponseBodyClosedTestRetryMiddleware_MultipleIntermediateResponseBodiesClosedTestRetryMiddleware_ContextCancellation_BodyClosedTestRetryMiddleware_503Permanent_OriginalBodyClosedChecklist
make test)make generate)make lint)go get github.com/snyk/go-application-framework@YOUR_LATEST_GAF_COMMITin thecliv2directory.go mod tidyin thecliv2directory.go.modandgo.sumchanges.PR in CLI: snyk/cli#6912