44 "bytes"
55 "context"
66 "encoding/json"
7+ "errors"
78 "fmt"
89 "io"
910 "net/http"
@@ -24,6 +25,8 @@ type Client struct {
2425 Sleep func (context.Context , time.Duration ) error
2526 Now func () time.Time
2627 ThrottleBelow int
28+ MaxAttempts int
29+ RetryBackoff time.Duration
2730}
2831
2932type ConversationListItem struct {
@@ -178,48 +181,93 @@ func (c Client) doJSON(ctx context.Context, method, path string, query url.Value
178181 if len (query ) > 0 {
179182 u .RawQuery = query .Encode ()
180183 }
181- var body io. Reader
184+ var bodyData [] byte
182185 if requestBody != nil {
183186 data , err := json .Marshal (requestBody )
184187 if err != nil {
185188 return err
186189 }
187- body = bytes . NewReader ( data )
190+ bodyData = data
188191 }
189- req , err := http .NewRequestWithContext (ctx , method , u .String (), body )
190- if err != nil {
191- return err
192- }
193- req .Header .Set ("Accept" , "application/json" )
194- req .Header .Set ("Content-Type" , "application/json" )
195192 version := c .Version
196193 if version == "" {
197194 version = DefaultAPIVersion
198195 }
199- req .Header .Set ("Intercom-Version" , version )
200- if c .Token != "" {
201- req .Header .Set ("Authorization" , "Bearer " + c .Token )
202- }
203196 client := c .HTTPClient
204197 if client == nil {
205198 client = http .DefaultClient
206199 }
207- resp , err := client . Do ( req )
208- if err != nil {
209- return err
200+ attempts := c . MaxAttempts
201+ if attempts <= 0 {
202+ attempts = 1
210203 }
211- defer resp .Body .Close ()
212- if resp .StatusCode == http .StatusTooManyRequests {
213- return RateLimitError {StatusCode : resp .StatusCode , RetryAfter : retryAfter (resp .Header , c .now ())}
204+ for attempt := 1 ; attempt <= attempts ; attempt ++ {
205+ var body io.Reader
206+ if bodyData != nil {
207+ body = bytes .NewReader (bodyData )
208+ }
209+ req , err := http .NewRequestWithContext (ctx , method , u .String (), body )
210+ if err != nil {
211+ return err
212+ }
213+ req .Header .Set ("Accept" , "application/json" )
214+ req .Header .Set ("Content-Type" , "application/json" )
215+ req .Header .Set ("Intercom-Version" , version )
216+ if c .Token != "" {
217+ req .Header .Set ("Authorization" , "Bearer " + c .Token )
218+ }
219+ resp , err := client .Do (req )
220+ if err != nil {
221+ if attempt < attempts && shouldRetryError (err ) {
222+ if sleepErr := c .sleep (ctx , retryDelay (c .RetryBackoff , attempt )); sleepErr != nil {
223+ return sleepErr
224+ }
225+ continue
226+ }
227+ return err
228+ }
229+ if resp .StatusCode == http .StatusTooManyRequests {
230+ delay := retryAfter (resp .Header , c .now ())
231+ resp .Body .Close ()
232+ return RateLimitError {StatusCode : resp .StatusCode , RetryAfter : delay }
233+ }
234+ if resp .StatusCode >= 500 && resp .StatusCode <= 599 && attempt < attempts {
235+ io .Copy (io .Discard , io .LimitReader (resp .Body , 4096 ))
236+ resp .Body .Close ()
237+ if sleepErr := c .sleep (ctx , retryDelay (c .RetryBackoff , attempt )); sleepErr != nil {
238+ return sleepErr
239+ }
240+ continue
241+ }
242+ if resp .StatusCode < 200 || resp .StatusCode >= 300 {
243+ body , _ := io .ReadAll (io .LimitReader (resp .Body , 4096 ))
244+ resp .Body .Close ()
245+ return HTTPStatusError {Method : method , Path : path , StatusCode : resp .StatusCode , Body : strings .TrimSpace (string (body ))}
246+ }
247+ if err := c .maybeThrottle (ctx , resp .Header ); err != nil {
248+ resp .Body .Close ()
249+ return err
250+ }
251+ err = json .NewDecoder (resp .Body ).Decode (responseBody )
252+ resp .Body .Close ()
253+ return err
214254 }
215- if resp .StatusCode < 200 || resp .StatusCode >= 300 {
216- body , _ := io .ReadAll (io .LimitReader (resp .Body , 4096 ))
217- return HTTPStatusError {Method : method , Path : path , StatusCode : resp .StatusCode , Body : strings .TrimSpace (string (body ))}
255+ return nil
256+ }
257+
258+ func shouldRetryError (err error ) bool {
259+ var netErr interface { Timeout () bool }
260+ return errors .As (err , & netErr ) && netErr .Timeout ()
261+ }
262+
263+ func retryDelay (base time.Duration , attempt int ) time.Duration {
264+ if base <= 0 {
265+ base = time .Second
218266 }
219- if err := c . maybeThrottle ( ctx , resp . Header ); err != nil {
220- return err
267+ if attempt <= 1 {
268+ return base
221269 }
222- return json . NewDecoder ( resp . Body ). Decode ( responseBody )
270+ return time . Duration ( attempt ) * base
223271}
224272
225273func decodeEntitiesPage (raw json.RawMessage , keys ... string ) ([]Entity , string , error ) {
@@ -357,6 +405,23 @@ func (c Client) maybeThrottle(ctx context.Context, header http.Header) error {
357405 return c .Sleep (ctx , delay )
358406}
359407
408+ func (c Client ) sleep (ctx context.Context , delay time.Duration ) error {
409+ if delay <= 0 {
410+ return nil
411+ }
412+ if c .Sleep != nil {
413+ return c .Sleep (ctx , delay )
414+ }
415+ timer := time .NewTimer (delay )
416+ defer timer .Stop ()
417+ select {
418+ case <- ctx .Done ():
419+ return ctx .Err ()
420+ case <- timer .C :
421+ return nil
422+ }
423+ }
424+
360425func (c Client ) now () time.Time {
361426 if c .Now != nil {
362427 return c .Now ()
0 commit comments