Fix retry body replay and jitter panic; drive delays via internal/clock
Gate the retry decision on body rewindability: an idempotent request whose body cannot be replayed (no GetBody) is now returned as-is instead of looping with an empty body or surfacing a stale, already-drained response. Guard ExponentialBackoff against rand.Int64N panicking when delay/2 rounds to zero. Use internal/clock for inter-attempt delays so retry timing is consistent with the rest of the codebase and testable without real sleeps.
This commit is contained in:
@@ -37,18 +37,16 @@ func Transport(opts ...Option) middleware.Middleware {
|
||||
var exhausted bool
|
||||
|
||||
for attempt := range cfg.maxAttempts {
|
||||
// For retries (attempt > 0), restore the request body.
|
||||
if attempt > 0 {
|
||||
if req.GetBody != nil {
|
||||
body, bodyErr := req.GetBody()
|
||||
if bodyErr != nil {
|
||||
return resp, bodyErr
|
||||
}
|
||||
req.Body = body
|
||||
} else if req.Body != nil {
|
||||
// Body was consumed and cannot be re-created.
|
||||
return resp, err
|
||||
// For retries (attempt > 0) the body was consumed by the
|
||||
// previous attempt; restore it via GetBody. The rewindability
|
||||
// check below guarantees GetBody is set whenever we loop with a
|
||||
// non-nil body, so this branch is always safe.
|
||||
if attempt > 0 && req.GetBody != nil {
|
||||
body, bodyErr := req.GetBody()
|
||||
if bodyErr != nil {
|
||||
return nil, bodyErr
|
||||
}
|
||||
req.Body = body
|
||||
}
|
||||
|
||||
resp, err = next.RoundTrip(req)
|
||||
@@ -64,6 +62,13 @@ func Transport(opts ...Option) middleware.Middleware {
|
||||
break
|
||||
}
|
||||
|
||||
// If the body cannot be rewound, a retry would replay with an
|
||||
// empty body. Return the current result as-is instead of
|
||||
// draining it and looping with a corrupted request.
|
||||
if req.Body != nil && req.GetBody == nil {
|
||||
break
|
||||
}
|
||||
|
||||
// Compute delay: use backoff or policy delay, whichever is larger.
|
||||
delay := cfg.backoff.Delay(attempt)
|
||||
if policyDelay > delay {
|
||||
@@ -84,12 +89,12 @@ func Transport(opts ...Option) middleware.Middleware {
|
||||
}
|
||||
|
||||
// Wait for the delay or context cancellation.
|
||||
timer := time.NewTimer(delay)
|
||||
timer := cfg.clk.NewTimer(delay)
|
||||
select {
|
||||
case <-req.Context().Done():
|
||||
timer.Stop()
|
||||
return nil, req.Context().Err()
|
||||
case <-timer.C:
|
||||
case <-timer.C():
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,9 +123,9 @@ func (defaultPolicy) ShouldRetry(_ int, req *http.Request, resp *http.Response,
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusTooManyRequests, // 429
|
||||
http.StatusBadGateway, // 502
|
||||
http.StatusBadGateway, // 502
|
||||
http.StatusServiceUnavailable, // 503
|
||||
http.StatusGatewayTimeout: // 504
|
||||
http.StatusGatewayTimeout: // 504
|
||||
return true, 0
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user