Skip to content

Commit 3f81ed6

Browse files
authored
feat(enhancement)!: remove HTTP status code check and add new exported retry condition for status codes (#1130)
#1080
1 parent 12468ab commit 3f81ed6

5 files changed

Lines changed: 86 additions & 23 deletions

File tree

client.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1363,7 +1363,9 @@ func (c *Client) IsRetryDefaultConditions() bool {
13631363
}
13641364

13651365
// SetRetryDefaultConditions method is used to enable/disable the Resty's default
1366-
// retry conditions
1366+
// retry conditions that checks transport, headers and URL errors.
1367+
//
1368+
// By default it is enabled.
13671369
//
13681370
// It can be overridden at request level, see [Request.SetRetryDefaultConditions]
13691371
func (c *Client) SetRetryDefaultConditions(b bool) *Client {

request.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1171,7 +1171,9 @@ func (r *Request) SetRetryDelayStrategy(rs RetryDelayStrategyFunc) *Request {
11711171
}
11721172

11731173
// SetRetryDefaultConditions method is used to enable/disable the Resty's default
1174-
// retry conditions on request level
1174+
// retry conditions on request level, that checks transport, headers and URL errors.
1175+
//
1176+
// By default it is enabled.
11751177
//
11761178
// It overrides value set at the client instance level, see [Client.SetRetryDefaultConditions]
11771179
func (r *Request) SetRetryDefaultConditions(b bool) *Request {
@@ -1509,7 +1511,7 @@ func (r *Request) Execute(method, url string) (res *Response, err error) {
15091511

15101512
// apply default retry conditions
15111513
if r.IsRetryDefaultConditions {
1512-
needsRetry = applyRetryDefaultConditions(res, err)
1514+
needsRetry = isDoNotRetryError(err)
15131515
}
15141516

15151517
// apply user-defined retry conditions if default one

retry.go

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,52 @@ var (
4848
regexErrInvalidHeader = regexp.MustCompile("invalid header")
4949
)
5050

51-
func applyRetryDefaultConditions(res *Response, err error) bool {
51+
// RetryConditionStatusTooManyRequests is a RetryConditionFunc that returns true
52+
// if the response status code is 429 Too Many Requests.
53+
//
54+
// - The 429 status code indicates that the user has sent too many requests in a given amount
55+
// of time ("rate limiting").
56+
// - Retrying after receiving a 429 status code can be effective, especially if the server includes
57+
// a Retry-After header indicating when to retry.
58+
func RetryConditionStatusTooManyRequests(res *Response, _ error) bool {
59+
if res == nil {
60+
return false
61+
}
62+
return res.StatusCode() == http.StatusTooManyRequests
63+
}
64+
65+
// RetryConditionStatus5XX is a RetryConditionFunc that returns true if the response status code is 500 or above,
66+
// excluding Status 501 - Not Implemented.
67+
//
68+
// - 5XX status codes are generally considered temporary server errors that may be resolved on retry
69+
// - The rationale for excluding 501 Not Implemented is that it indicates the server does not support the
70+
// functionality required to fulfill the request,
71+
func RetryConditionStatus5XX(res *Response, _ error) bool {
72+
if res == nil {
73+
return false
74+
}
75+
return res.StatusCode() >= 500 && res.StatusCode() != http.StatusNotImplemented
76+
}
77+
78+
// RetryConditionStatusZero is a RetryConditionFunc that returns true if the response status code is 0.
79+
//
80+
// - A status code of 0 typically indicates that no response was received from the server, which can occur
81+
// due to network errors, timeouts, or other issues that prevent the request from being completed.
82+
// - Retrying when a status code of 0 is encountered can be effective, as it may allow the request to succeed
83+
// on subsequent attempts if the underlying issue is transient.
84+
func RetryConditionStatusZero(res *Response, _ error) bool {
85+
if res == nil {
86+
return false
87+
}
88+
return res.StatusCode() == 0
89+
}
90+
91+
// isDoNotRetryError checks whether the given request error should trigger a retry.
92+
//
93+
// It returns true only for retryable URL/network errors and false for errors that
94+
// should not be retried, such as TLS certificate errors, invalid scheme, invalid
95+
// headers, and too many redirects.
96+
func isDoNotRetryError(err error) bool {
5297
// no retry on TLS error
5398
if _, ok := err.(*tls.CertificateVerificationError); ok {
5499
return false
@@ -68,20 +113,6 @@ func applyRetryDefaultConditions(res *Response, err error) bool {
68113
return u.Temporary() // possible retry if it's true
69114
}
70115

71-
if res == nil {
72-
return false
73-
}
74-
75-
// certain HTTP status codes are temporary so that we can retry
76-
// - 429 Too Many Requests
77-
// - 500 or above (it's better to ignore 501 Not Implemented)
78-
// - 0 No status code received
79-
if res.StatusCode() == http.StatusTooManyRequests ||
80-
(res.StatusCode() >= 500 && res.StatusCode() != http.StatusNotImplemented) ||
81-
res.StatusCode() == 0 {
82-
return true
83-
}
84-
85116
return false
86117
}
87118

retry_test.go

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,7 @@ func TestClientRetryTooManyRequestsAndRecover(t *testing.T) {
573573
SetJSONEscapeHTML(false).
574574
SetResult(AuthSuccess{}).
575575
SetTimeout(10 * time.Millisecond).
576+
AddRetryConditions(RetryConditionStatusTooManyRequests).
576577
Get(ts.URL + "/set-retry-error-recover")
577578

578579
assertError(t, err)
@@ -649,7 +650,8 @@ func TestResetMultipartReaderSeekStartError(t *testing.T) {
649650

650651
c := dcnl().
651652
SetRetryCount(2).
652-
SetTimeout(200 * time.Millisecond)
653+
SetTimeout(200 * time.Millisecond).
654+
AddRetryConditions(RetryConditionStatus5XX)
653655

654656
resp, err := c.R().
655657
SetFileReader("name", "filename", testSeeker).
@@ -756,7 +758,8 @@ func TestRequestRetryTooManyRequestsHeaderRetryAfter(t *testing.T) {
756758
ts := createGetServer(t)
757759
defer ts.Close()
758760

759-
c := dcnl()
761+
c := dcnl().
762+
AddRetryConditions(RetryConditionStatusTooManyRequests)
760763

761764
resp, err := c.R().
762765
SetRetryCount(2).
@@ -823,7 +826,7 @@ func TestRetryDefaultConditions(t *testing.T) {
823826
})
824827

825828
t.Run("nil values", func(t *testing.T) {
826-
result := applyRetryDefaultConditions(nil, nil)
829+
result := isDoNotRetryError(nil)
827830
assertFalse(t, result)
828831
})
829832
}
@@ -1105,9 +1108,20 @@ func TestRetryCoverage(t *testing.T) {
11051108
assertEqual(t, 2*time.Second, dur2)
11061109
})
11071110

1111+
t.Run("retry condition nil response", func(t *testing.T) {
1112+
result := RetryConditionStatusTooManyRequests(nil, nil)
1113+
assertFalse(t, result, "expected no retry for nil response")
1114+
1115+
result = RetryConditionStatus5XX(nil, nil)
1116+
assertFalse(t, result, "expected no retry for nil response")
1117+
1118+
result = RetryConditionStatusZero(nil, nil)
1119+
assertFalse(t, result, "expected no retry for nil response")
1120+
})
1121+
11081122
t.Run("mock tls cert error", func(t *testing.T) {
11091123
certError := tls.CertificateVerificationError{}
1110-
result1 := applyRetryDefaultConditions(nil, &certError)
1124+
result1 := isDoNotRetryError(&certError)
11111125
assertFalse(t, result1, "expected no retry for tls.CertificateVerificationError")
11121126
})
11131127
}

sse.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ type (
7676
retryCount int
7777
retryWaitTime time.Duration
7878
retryMaxWaitTime time.Duration
79+
retryConditions []RetryConditionFunc
7980
serverSentRetry time.Duration
8081
maxBufSize int
8182
onOpen SSEOpenFunc
@@ -117,13 +118,19 @@ func NewSSESource() *SSESource {
117118
retryCount: 3,
118119
retryWaitTime: defaultWaitTime,
119120
retryMaxWaitTime: defaultMaxWaitTime,
121+
retryConditions: make([]RetryConditionFunc, 0),
120122
maxBufSize: defaultSseMaxBufSize,
121123
onEvent: make(map[string]*callback),
122124
httpClient: &http.Client{
123125
Jar: createCookieJar(),
124126
Transport: createTransport(nil, nil),
125127
},
126128
}
129+
sse.retryConditions = append(sse.retryConditions,
130+
RetryConditionStatusZero,
131+
RetryConditionStatusTooManyRequests,
132+
RetryConditionStatus5XX,
133+
)
127134
return sse
128135
}
129136

@@ -603,7 +610,14 @@ func (sse *SSESource) connect() (*http.Response, error) {
603610
}
604611

605612
rRes := wrapResponse(resp, req)
606-
needsRetry := applyRetryDefaultConditions(rRes, doErr)
613+
needsRetry := isDoNotRetryError(doErr)
614+
if !needsRetry && resp != nil {
615+
for _, retryCondition := range sse.retryConditions {
616+
if needsRetry = retryCondition(rRes, doErr); needsRetry {
617+
break
618+
}
619+
}
620+
}
607621

608622
// retry not required stop here
609623
if !needsRetry {

0 commit comments

Comments
 (0)