@@ -2,11 +2,13 @@ package client
22
33import (
44 "bytes"
5+ "context"
56 "crypto/hmac"
67 "crypto/sha1" // nolint:gosec
78 "encoding/hex"
89 "encoding/json"
910 "fmt"
11+ "io"
1012 "io/ioutil"
1113 "net/http"
1214 "net/http/httputil"
@@ -27,6 +29,17 @@ const (
2729 SharedSecret = "complement"
2830)
2931
32+ type CtxKey string
33+
34+ const (
35+ CtxKeyWithRetryUntil CtxKey = "complement_retry_until" // contains *retryUntilParams
36+ )
37+
38+ type retryUntilParams struct {
39+ timeout time.Duration
40+ untilFn func (* http.Response ) bool
41+ }
42+
3043// RequestOpt is a functional option which will modify an outgoing HTTP request.
3144// See functions starting with `With...` in this package for more info.
3245type RequestOpt func (req * http.Request )
@@ -441,6 +454,16 @@ func WithQueries(q url.Values) RequestOpt {
441454 }
442455}
443456
457+ // WithRetryUntil will retry the request until the provided function returns true. Times out after
458+ // `timeout`, which will then fail the test.
459+ func WithRetryUntil (timeout time.Duration , untilFn func (res * http.Response ) bool ) RequestOpt {
460+ return func (req * http.Request ) {
461+ until := req .Context ().Value (CtxKeyWithRetryUntil ).(* retryUntilParams )
462+ until .timeout = timeout
463+ until .untilFn = untilFn
464+ }
465+ }
466+
444467// MustDoFunc is the same as DoFunc but fails the test if the returned HTTP response code is not 2xx.
445468func (c * CSAPI ) MustDoFunc (t * testing.T , method string , paths []string , opts ... RequestOpt ) * http.Response {
446469 t .Helper ()
@@ -479,6 +502,9 @@ func (c *CSAPI) DoFunc(t *testing.T, method string, paths []string, opts ...Requ
479502 if c .AccessToken != "" {
480503 req .Header .Set ("Authorization" , "Bearer " + c .AccessToken )
481504 }
505+ retryUntil := & retryUntilParams {}
506+ ctx := context .WithValue (req .Context (), CtxKeyWithRetryUntil , retryUntil )
507+ req = req .WithContext (ctx )
482508
483509 // set functional options
484510 for _ , o := range opts {
@@ -502,21 +528,48 @@ func (c *CSAPI) DoFunc(t *testing.T, method string, paths []string, opts ...Requ
502528 t .Logf ("Request body: <binary:%s>" , contentType )
503529 }
504530 }
505- // Perform the HTTP request
506- res , err := c .Client .Do (req )
507- if err != nil {
508- t .Fatalf ("CSAPI.DoFunc response returned error: %s" , err )
509- }
510- // debug log the response
511- if c .Debug && res != nil {
512- var dump []byte
513- dump , err = httputil .DumpResponse (res , true )
531+ now := time .Now ()
532+ for {
533+ // Perform the HTTP request
534+ res , err := c .Client .Do (req )
514535 if err != nil {
515- t .Fatalf ("CSAPI.DoFunc failed to dump response body: %s" , err )
536+ t .Fatalf ("CSAPI.DoFunc response returned error: %s" , err )
537+ }
538+ // debug log the response
539+ if c .Debug && res != nil {
540+ var dump []byte
541+ dump , err = httputil .DumpResponse (res , true )
542+ if err != nil {
543+ t .Fatalf ("CSAPI.DoFunc failed to dump response body: %s" , err )
544+ }
545+ t .Logf ("%s" , string (dump ))
546+ }
547+ if retryUntil == nil || retryUntil .timeout == 0 {
548+ return res // don't retry
549+ }
550+
551+ // check the condition, make a copy of the response body first in case the check consumes it
552+ var resBody []byte
553+ if res .Body != nil {
554+ resBody , err = ioutil .ReadAll (res .Body )
555+ if err != nil {
556+ t .Fatalf ("CSAPI.DoFunc failed to read response body for RetryUntil check: %s" , err )
557+ }
558+ res .Body = io .NopCloser (bytes .NewBuffer (resBody ))
516559 }
517- t .Logf ("%s" , string (dump ))
560+ if retryUntil .untilFn (res ) {
561+ // remake the response and return
562+ res .Body = io .NopCloser (bytes .NewBuffer (resBody ))
563+ return res
564+ }
565+ // condition not satisfied, do we timeout yet?
566+ if time .Since (now ) > retryUntil .timeout {
567+ t .Fatalf ("CSAPI.DoFunc RetryUntil: %v %v timed out after %v" , method , req .URL , retryUntil .timeout )
568+ }
569+ t .Logf ("CSAPI.DoFunc RetryUntil: %v %v response condition not yet met, retrying" , method , req .URL )
570+ // small sleep to avoid tight-looping
571+ time .Sleep (100 * time .Millisecond )
518572 }
519- return res
520573}
521574
522575// NewLoggedClient returns an http.Client which logs requests/responses
0 commit comments