Files
picoclaw/pkg/utils/http_retry.go
T
2026-03-30 13:04:51 +08:00

88 lines
1.6 KiB
Go

package utils
import (
"context"
"fmt"
"net/http"
"strconv"
"time"
)
const maxRetries = 3
var retryDelayUnit = time.Second
func shouldRetry(statusCode int) bool {
return statusCode == http.StatusTooManyRequests ||
statusCode >= 500
}
func DoRequestWithRetry(client *http.Client, req *http.Request) (*http.Response, error) {
var resp *http.Response
var err error
for i := range maxRetries {
if i > 0 && resp != nil {
resp.Body.Close()
}
resp, err = client.Do(req)
if err == nil {
if resp.StatusCode == http.StatusOK {
break
}
if !shouldRetry(resp.StatusCode) {
break
}
}
if i < maxRetries-1 {
if err = sleepWithCtx(req.Context(), retryDelayForAttempt(resp, i)); err != nil {
if resp != nil {
resp.Body.Close()
}
return nil, fmt.Errorf("failed to sleep: %w", err)
}
}
}
return resp, err
}
func retryDelayForAttempt(resp *http.Response, attempt int) time.Duration {
fallback := retryDelayUnit * time.Duration(attempt+1)
if resp == nil || resp.StatusCode != http.StatusTooManyRequests {
return fallback
}
retryAfter := resp.Header.Get("Retry-After")
if retryAfter == "" {
return fallback
}
if seconds, err := strconv.Atoi(retryAfter); err == nil && seconds >= 0 {
return time.Duration(seconds) * time.Second
}
if when, err := http.ParseTime(retryAfter); err == nil {
delay := time.Until(when)
if delay < 0 {
return 0
}
return delay
}
return fallback
}
func sleepWithCtx(ctx context.Context, d time.Duration) error {
timer := time.NewTimer(d)
defer timer.Stop()
select {
case <-ctx.Done():
return ctx.Err()
case <-timer.C:
return nil
}
}