@@ -4,50 +4,108 @@ import (
44 "bytes"
55 "context"
66 "encoding/json"
7+ "errors"
78 "fmt"
89 "io"
910 "net/http"
1011 "net/url"
12+ "strconv"
13+ "time"
14+
15+ "github.com/avast/retry-go/v4"
16+ )
17+
18+ const (
19+ defaultWindowLimit = 400
20+ defaultWindowDuration = 1 * time .Minute
21+
22+ headerRateLimitRemaining = "X-Rate-Limit-Remaining"
1123)
1224
1325type HttpClient struct {
14- client * http.Client
15- baseUrl * url.URL
26+ client * http.Client
27+ baseUrl * url.URL
28+ rateLimiter RateLimiter
29+ retryEnabled bool
30+ retryMaxDelay time.Duration
31+ retryDelay time.Duration
32+ retryMaxAttempts uint
33+ logger Log
1634}
1735
18- func NewHttpClient (client * http.Client , baseUrl string ) (* HttpClient , error ) {
36+ func NewHttpClient (client * http.Client , baseUrl string , logger Log ) (* HttpClient , error ) {
1937 parsed , err := url .Parse (baseUrl )
2038 if err != nil {
2139 return nil , err
2240 }
23- return & HttpClient {client : client , baseUrl : parsed }, nil
41+
42+ return & HttpClient {
43+ client : client ,
44+ baseUrl : parsed ,
45+ rateLimiter : newFixedWindowCountRateLimiter (defaultWindowLimit , defaultWindowDuration ),
46+ retryEnabled : true ,
47+ retryMaxAttempts : 10 ,
48+ retryDelay : 1 * time .Second ,
49+ retryMaxDelay : defaultWindowDuration ,
50+ logger : logger ,
51+ }, nil
2452}
2553
2654func (c * HttpClient ) Get (ctx context.Context , name , path string , responseBody interface {}) error {
27- return c .connection (ctx , http .MethodGet , name , path , nil , nil , responseBody )
55+ return c .connectionWithRetries (ctx , http .MethodGet , name , path , nil , nil , responseBody )
2856}
2957
3058func (c * HttpClient ) GetWithQuery (ctx context.Context , name , path string , query url.Values , responseBody interface {}) error {
31- return c .connection (ctx , http .MethodGet , name , path , query , nil , responseBody )
59+ return c .connectionWithRetries (ctx , http .MethodGet , name , path , query , nil , responseBody )
3260}
3361
3462func (c * HttpClient ) Put (ctx context.Context , name , path string , requestBody interface {}, responseBody interface {}) error {
35- return c .connection (ctx , http .MethodPut , name , path , nil , requestBody , responseBody )
63+ return c .connectionWithRetries (ctx , http .MethodPut , name , path , nil , requestBody , responseBody )
3664}
3765
3866func (c * HttpClient ) Post (ctx context.Context , name , path string , requestBody interface {}, responseBody interface {}) error {
39- return c .connection (ctx , http .MethodPost , name , path , nil , requestBody , responseBody )
67+ return c .connectionWithRetries (ctx , http .MethodPost , name , path , nil , requestBody , responseBody )
4068}
4169
4270func (c * HttpClient ) Delete (ctx context.Context , name , path string , responseBody interface {}) error {
43- return c .connection (ctx , http .MethodDelete , name , path , nil , nil , responseBody )
71+ return c .connectionWithRetries (ctx , http .MethodDelete , name , path , nil , nil , responseBody )
4472}
4573
4674func (c * HttpClient ) DeleteWithQuery (ctx context.Context , name , path string , requestBody interface {}, responseBody interface {}) error {
47- return c .connection (ctx , http .MethodDelete , name , path , nil , requestBody , responseBody )
75+ return c .connectionWithRetries (ctx , http .MethodDelete , name , path , nil , requestBody , responseBody )
76+ }
77+
78+ func (c * HttpClient ) connectionWithRetries (ctx context.Context , method , name , path string , query url.Values , requestBody interface {}, responseBody interface {}) error {
79+ return retry .Do (func () error {
80+ return c .connection (ctx , method , name , path , query , requestBody , responseBody )
81+ },
82+ retry .Attempts (c .retryMaxAttempts ),
83+ retry .Delay (c .retryDelay ),
84+ retry .MaxDelay (c .retryMaxDelay ),
85+ retry .RetryIf (func (err error ) bool {
86+ if ! c .retryEnabled {
87+ return false
88+ }
89+ var target * HTTPError
90+ if errors .As (err , & target ) && target .StatusCode == http .StatusTooManyRequests {
91+ c .logger .Println (fmt .Sprintf ("status code 429 received, request will be retried" ))
92+ return true
93+ }
94+ return false
95+ }),
96+ retry .LastErrorOnly (true ),
97+ retry .Context (ctx ),
98+ )
4899}
49100
50101func (c * HttpClient ) connection (ctx context.Context , method , name , path string , query url.Values , requestBody interface {}, responseBody interface {}) error {
102+ if c .rateLimiter != nil {
103+ err := c .rateLimiter .Wait (ctx )
104+ if err != nil {
105+ return err
106+ }
107+ }
108+
51109 parsed := new (url.URL )
52110 * parsed = * c .baseUrl
53111
@@ -81,6 +139,16 @@ func (c *HttpClient) connection(ctx context.Context, method, name, path string,
81139 return fmt .Errorf ("failed to %s: %w" , name , err )
82140 }
83141
142+ remainingLimit := response .Header .Get (headerRateLimitRemaining )
143+ if remainingLimit != "" {
144+ if limit , err := strconv .Atoi (remainingLimit ); err == nil {
145+ err = c .rateLimiter .Update (limit )
146+ if err != nil {
147+ return err
148+ }
149+ }
150+ }
151+
84152 defer response .Body .Close ()
85153
86154 if response .StatusCode > 299 {
0 commit comments