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

58
retry/retry_after_test.go Normal file
View File

@@ -0,0 +1,58 @@
package retry
import (
"net/http"
"testing"
"time"
)
func TestParseRetryAfter(t *testing.T) {
t.Run("seconds format", func(t *testing.T) {
resp := &http.Response{
Header: http.Header{"Retry-After": []string{"120"}},
}
d, ok := ParseRetryAfter(resp)
if !ok {
t.Fatal("expected ok=true")
}
if d != 120*time.Second {
t.Fatalf("expected 120s, got %v", d)
}
})
t.Run("empty header", func(t *testing.T) {
resp := &http.Response{
Header: make(http.Header),
}
d, ok := ParseRetryAfter(resp)
if ok {
t.Fatal("expected ok=false for empty header")
}
if d != 0 {
t.Fatalf("expected 0, got %v", d)
}
})
t.Run("nil response", func(t *testing.T) {
d, ok := ParseRetryAfter(nil)
if ok {
t.Fatal("expected ok=false for nil response")
}
if d != 0 {
t.Fatalf("expected 0, got %v", d)
}
})
t.Run("negative value", func(t *testing.T) {
resp := &http.Response{
Header: http.Header{"Retry-After": []string{"-5"}},
}
d, ok := ParseRetryAfter(resp)
if ok {
t.Fatal("expected ok=false for negative value")
}
if d != 0 {
t.Fatalf("expected 0, got %v", d)
}
})
}