Add retry transport with configurable backoff and Retry-After support

Implements retry middleware as a RoundTripper wrapper:
- Exponential and constant backoff strategies with jitter
- RFC 7231 Retry-After header parsing (seconds and HTTP-date)
- Default policy retries idempotent methods on 429/5xx and network errors
- Body restoration via GetBody, context cancellation, response body cleanup
This commit is contained in:
2026-03-20 14:21:53 +03:00
parent 6b1941fce7
commit 505c7b8c4f
7 changed files with 660 additions and 0 deletions

43
retry/retry_after.go Normal file
View File

@@ -0,0 +1,43 @@
package retry
import (
"net/http"
"strconv"
"time"
)
// ParseRetryAfter extracts the delay from a Retry-After header (RFC 7231).
// It supports both the delay-seconds format ("120") and the HTTP-date format
// ("Fri, 31 Dec 1999 23:59:59 GMT"). Returns the duration and true if the
// header was present and valid; otherwise returns 0 and false.
func ParseRetryAfter(resp *http.Response) (time.Duration, bool) {
if resp == nil {
return 0, false
}
val := resp.Header.Get("Retry-After")
if val == "" {
return 0, false
}
// Try delay-seconds first (most common).
if seconds, err := strconv.ParseInt(val, 10, 64); err == nil {
if seconds < 0 {
return 0, false
}
return time.Duration(seconds) * time.Second, true
}
// Try HTTP-date format (RFC 7231 section 7.1.1.1).
t, err := http.ParseTime(val)
if err != nil {
return 0, false
}
delay := time.Until(t)
if delay < 0 {
// The date is in the past; no need to wait.
return 0, true
}
return delay, true
}