Skip to content

Commit e4aaa04

Browse files
authored
Allow retries in http client (#15)
1 parent 7ccc8e9 commit e4aaa04

File tree

2 files changed

+142
-79
lines changed

2 files changed

+142
-79
lines changed

client.go

Lines changed: 105 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"sort"
1515
"strings"
1616
"sync"
17+
"time"
1718

1819
"github.com/swaggest/assertjson"
1920
"github.com/swaggest/assertjson/json5"
@@ -50,13 +51,42 @@ type Client struct {
5051
// reqConcurrency is a number of simultaneous requests to send.
5152
reqConcurrency int
5253

54+
retryBackOff RetryBackOff
5355
followRedirects bool
5456

5557
otherRespBody []byte
5658
otherResp *http.Response
5759
otherRespExpected bool
5860
}
5961

62+
// RetryBackOff defines retry strategy.
63+
//
64+
// This interface matches github.com/cenkalti/retryBackOff/v4.BackOff.
65+
type RetryBackOff interface {
66+
// NextBackOff returns the duration to wait before retrying the operation,
67+
// or -1 to indicate that no more retries should be made.
68+
//
69+
// Example usage:
70+
//
71+
// duration := retryBackOff.NextBackOff();
72+
// if (duration == retryBackOff.Stop) {
73+
// // Do not retry operation.
74+
// } else {
75+
// // Sleep for duration and retry operation.
76+
// }
77+
//
78+
NextBackOff() time.Duration
79+
}
80+
81+
// RetryBackOffFunc implements RetryBackOff with a function.
82+
type RetryBackOffFunc func() time.Duration
83+
84+
// NextBackOff returns the duration to wait before retrying the operation,
85+
// or -1 to indicate that no more retries should be made.
86+
func (r RetryBackOffFunc) NextBackOff() time.Duration {
87+
return r()
88+
}
89+
6090
var (
6191
errEmptyBody = errors.New("received empty body")
6292
errResponseCardinality = errors.New("response status cardinality too high")
@@ -111,6 +141,7 @@ func (c *Client) Reset() *Client {
111141

112142
c.reqConcurrency = 0
113143
c.followRedirects = false
144+
c.retryBackOff = nil
114145

115146
c.otherResp = nil
116147
c.otherRespBody = nil
@@ -151,6 +182,13 @@ func (c *Client) FollowRedirects() *Client {
151182
return c
152183
}
153184

185+
// AllowRetries allows sending multiple requests until first response assertion passes.
186+
func (c *Client) AllowRetries(b RetryBackOff) *Client {
187+
c.retryBackOff = b
188+
189+
return c
190+
}
191+
154192
// WithContext adds context to request.
155193
func (c *Client) WithContext(ctx context.Context) *Client {
156194
c.ctx = ctx
@@ -286,6 +324,36 @@ func (c *Client) do() (err error) {
286324
return c.checkResponses(statusCodeCount, bodies, resps)
287325
}
288326

327+
func (c *Client) expectResp(check func() error) (err error) {
328+
if c.resp != nil {
329+
return check()
330+
}
331+
332+
if c.retryBackOff != nil {
333+
for {
334+
if err = c.do(); err == nil {
335+
if err = check(); err == nil {
336+
return nil
337+
}
338+
}
339+
340+
dur := c.retryBackOff.NextBackOff()
341+
342+
if dur == -1 {
343+
return err
344+
}
345+
346+
time.Sleep(dur)
347+
}
348+
}
349+
350+
if err := c.do(); err != nil {
351+
return err
352+
}
353+
354+
return check()
355+
}
356+
289357
// CheckResponses checks if responses qualify idempotence criteria.
290358
//
291359
// Operation is considered idempotent in one of two cases:
@@ -453,26 +521,16 @@ func (c *Client) doOnce() (*http.Response, error) {
453521

454522
// ExpectResponseStatus sets expected response status code.
455523
func (c *Client) ExpectResponseStatus(statusCode int) error {
456-
if c.resp == nil {
457-
err := c.do()
458-
if err != nil {
459-
return err
460-
}
461-
}
462-
463-
return c.assertResponseCode(statusCode, c.resp)
524+
return c.expectResp(func() error {
525+
return c.assertResponseCode(statusCode, c.resp)
526+
})
464527
}
465528

466529
// ExpectResponseHeader asserts expected response header value.
467530
func (c *Client) ExpectResponseHeader(key, value string) error {
468-
if c.resp == nil {
469-
err := c.do()
470-
if err != nil {
471-
return err
472-
}
473-
}
474-
475-
return c.assertResponseHeader(key, value, c.resp)
531+
return c.expectResp(func() error {
532+
return c.assertResponseHeader(key, value, c.resp)
533+
})
476534
}
477535

478536
// CheckUnexpectedOtherResponses fails if other responses were present, but not expected with
@@ -492,17 +550,13 @@ func (c *Client) CheckUnexpectedOtherResponses() error {
492550
//
493551
// Does not affect single (non-concurrent) calls.
494552
func (c *Client) ExpectNoOtherResponses() error {
495-
if c.resp == nil {
496-
if err := c.do(); err != nil {
497-
return err
553+
return c.expectResp(func() error {
554+
if c.otherResp != nil {
555+
return c.assertResponseCode(c.resp.StatusCode, c.otherResp)
498556
}
499-
}
500-
501-
if c.otherResp != nil {
502-
return c.assertResponseCode(c.resp.StatusCode, c.otherResp)
503-
}
504557

505-
return nil
558+
return nil
559+
})
506560
}
507561

508562
// ExpectOtherResponsesStatus sets expectation for response status to be received one or more times during concurrent
@@ -513,35 +567,27 @@ func (c *Client) ExpectNoOtherResponses() error {
513567
func (c *Client) ExpectOtherResponsesStatus(statusCode int) error {
514568
c.otherRespExpected = true
515569

516-
if c.resp == nil {
517-
if err := c.do(); err != nil {
518-
return err
570+
return c.expectResp(func() error {
571+
if c.otherResp == nil {
572+
return errNoOtherResponses
519573
}
520-
}
521-
522-
if c.otherResp == nil {
523-
return errNoOtherResponses
524-
}
525574

526-
return c.assertResponseCode(statusCode, c.otherResp)
575+
return c.assertResponseCode(statusCode, c.otherResp)
576+
})
527577
}
528578

529579
// ExpectOtherResponsesHeader sets expectation for response header value to be received one or more times during
530580
// concurrent calling.
531581
func (c *Client) ExpectOtherResponsesHeader(key, value string) error {
532582
c.otherRespExpected = true
533583

534-
if c.resp == nil {
535-
if err := c.do(); err != nil {
536-
return err
584+
return c.expectResp(func() error {
585+
if c.otherResp == nil {
586+
return errNoOtherResponses
537587
}
538-
}
539-
540-
if c.otherResp == nil {
541-
return errNoOtherResponses
542-
}
543588

544-
return c.assertResponseHeader(key, value, c.otherResp)
589+
return c.assertResponseHeader(key, value, c.otherResp)
590+
})
545591
}
546592

547593
func (c *Client) assertResponseCode(statusCode int, resp *http.Response) error {
@@ -571,14 +617,9 @@ func (c *Client) assertResponseHeader(key, value string, resp *http.Response) er
571617
//
572618
// In concurrent mode such response must be met only once or for all calls.
573619
func (c *Client) ExpectResponseBodyCallback(cb func(received []byte) error) error {
574-
if c.resp == nil {
575-
err := c.do()
576-
if err != nil {
577-
return err
578-
}
579-
}
580-
581-
return c.checkBody(nil, c.respBody, cb)
620+
return c.expectResp(func() error {
621+
return c.checkBody(nil, c.respBody, cb)
622+
})
582623
}
583624

584625
// ExpectOtherResponsesBodyCallback sets expectation for response body to be received one or more times during concurrent
@@ -589,32 +630,22 @@ func (c *Client) ExpectResponseBodyCallback(cb func(received []byte) error) erro
589630
func (c *Client) ExpectOtherResponsesBodyCallback(cb func(received []byte) error) error {
590631
c.otherRespExpected = true
591632

592-
if c.resp == nil {
593-
err := c.do()
594-
if err != nil {
595-
return err
633+
return c.expectResp(func() error {
634+
if c.otherResp == nil {
635+
return errNoOtherResponses
596636
}
597-
}
598-
599-
if c.otherResp == nil {
600-
return errNoOtherResponses
601-
}
602637

603-
return c.checkBody(nil, c.otherRespBody, cb)
638+
return c.checkBody(nil, c.otherRespBody, cb)
639+
})
604640
}
605641

606642
// ExpectResponseBody sets expectation for response body to be received.
607643
//
608644
// In concurrent mode such response must be met only once or for all calls.
609645
func (c *Client) ExpectResponseBody(body []byte) error {
610-
if c.resp == nil {
611-
err := c.do()
612-
if err != nil {
613-
return err
614-
}
615-
}
616-
617-
return c.checkBody(body, c.respBody, nil)
646+
return c.expectResp(func() error {
647+
return c.checkBody(body, c.respBody, nil)
648+
})
618649
}
619650

620651
// ExpectOtherResponsesBody sets expectation for response body to be received one or more times during concurrent
@@ -625,18 +656,13 @@ func (c *Client) ExpectResponseBody(body []byte) error {
625656
func (c *Client) ExpectOtherResponsesBody(body []byte) error {
626657
c.otherRespExpected = true
627658

628-
if c.resp == nil {
629-
err := c.do()
630-
if err != nil {
631-
return err
659+
return c.expectResp(func() error {
660+
if c.otherResp == nil {
661+
return errNoOtherResponses
632662
}
633-
}
634-
635-
if c.otherResp == nil {
636-
return errNoOtherResponses
637-
}
638663

639-
return c.checkBody(body, c.otherRespBody, nil)
664+
return c.checkBody(body, c.otherRespBody, nil)
665+
})
640666
}
641667

642668
func (c *Client) checkBody(expected, received []byte, cb func(received []byte) error) (err error) {

client_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,3 +251,40 @@ func TestClient_Fork(t *testing.T) {
251251
case <-done:
252252
}
253253
}
254+
255+
func TestClient_AllowRetries(t *testing.T) {
256+
tries := 0
257+
258+
srv := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
259+
tries++
260+
261+
if tries == 5 {
262+
writer.WriteHeader(http.StatusOK)
263+
264+
return
265+
}
266+
267+
writer.WriteHeader(http.StatusInternalServerError)
268+
}))
269+
defer srv.Close()
270+
271+
c := httpmock.NewClient(srv.URL)
272+
273+
c.WithMethod(http.MethodGet)
274+
c.WithURI("/")
275+
276+
retriesLeft := 10
277+
278+
c.AllowRetries(httpmock.RetryBackOffFunc(func() time.Duration {
279+
retriesLeft--
280+
281+
if retriesLeft <= 0 {
282+
return -1
283+
}
284+
285+
return time.Millisecond
286+
}))
287+
288+
assert.NoError(t, c.ExpectResponseStatus(http.StatusOK))
289+
assert.Equal(t, 5, tries)
290+
}

0 commit comments

Comments
 (0)