Skip to content

Commit 826f040

Browse files
committed
fix(ethclient): impose upper limit for retry delays on HTTP Ethereum API
1 parent 778e9ed commit 826f040

1 file changed

Lines changed: 49 additions & 0 deletions

File tree

pkg/ethutil/client.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import (
88
"fmt"
99
"log/slog"
1010
"math"
11+
"net/http"
1112
"net/url"
13+
"strconv"
1214
"strings"
1315
"time"
1416

@@ -17,6 +19,34 @@ import (
1719
"github.com/hashicorp/go-retryablehttp"
1820
)
1921

22+
// parseRetryAfterHeader parses the Retry-After header and returns the
23+
// delay duration according to the spec: https://httpwg.org/specs/rfc7231.html#header.retry-after
24+
// The bool returned will be true if the header was successfully parsed.
25+
// Otherwise, the header was either not present, or was not parseable according to the spec.
26+
func parseRetryAfterHeader(headers []string) (time.Duration, bool) {
27+
if len(headers) == 0 || headers[0] == "" {
28+
return 0, false
29+
}
30+
header := headers[0]
31+
// 'Retry-After' is provided in seconds.
32+
if sleep, err := strconv.ParseInt(header, 10, 64); err == nil {
33+
if sleep < 0 { // a negative sleep doesn't make sense
34+
return 0, false
35+
}
36+
return time.Second * time.Duration(sleep), true
37+
}
38+
39+
// 'Retry-After' is provided as a date.
40+
retryTime, err := time.Parse(time.RFC1123, header)
41+
if err != nil {
42+
return 0, false
43+
}
44+
if duration := retryTime.Sub(time.Now()); duration > 0 {
45+
return duration, true
46+
}
47+
return 0, true // past date
48+
}
49+
2050
// RetryConfig holds configuration for the retryable HTTP client.
2151
type RetryConfig struct {
2252
MaxRetries uint64
@@ -39,6 +69,25 @@ func NewEthClient(
3969
rclient.RetryMax = int(min(retryConfig.MaxRetries, uint64(math.MaxInt)))
4070
rclient.RetryWaitMin = retryConfig.RetryMinWait
4171
rclient.RetryWaitMax = retryConfig.RetryMaxWait
72+
// Supply a custom Backoff that takes 'min(serverRetryAfter, RetryMaxWait)'.
73+
// The provider keeps its ability to ask for politeness; it loses the ability
74+
// to dictate the node's clock.
75+
rclient.Backoff = func(minDuration, maxDuration time.Duration, attemptNum int, resp *http.Response) time.Duration {
76+
if resp != nil {
77+
if resp.StatusCode == http.StatusTooManyRequests || resp.StatusCode == http.StatusServiceUnavailable {
78+
if sleep, ok := parseRetryAfterHeader(resp.Header["Retry-After"]); ok {
79+
return min(maxDuration, max(sleep, 0))
80+
}
81+
}
82+
}
83+
84+
mult := math.Pow(2, float64(attemptNum)) * float64(minDuration)
85+
sleep := time.Duration(mult)
86+
if float64(sleep) != mult || sleep > maxDuration {
87+
sleep = maxDuration
88+
}
89+
return sleep
90+
}
4291

4392
opts := []rpc.ClientOption{
4493
rpc.WithHTTPClient(rclient.StandardClient()),

0 commit comments

Comments
 (0)