Skip to content

Commit 48a1a59

Browse files
authored
feat: use context timeout instead of http client timeout option #545 (#922)
- add Request.SetTimeout method
1 parent 8b24a96 commit 48a1a59

File tree

6 files changed

+104
-27
lines changed

6 files changed

+104
-27
lines changed

client.go

+17-4
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ type Client struct {
183183
disableWarn bool
184184
allowMethodGetPayload bool
185185
allowMethodDeletePayload bool
186+
timeout time.Duration
186187
retryCount int
187188
retryWaitTime time.Duration
188189
retryMaxWaitTime time.Duration
@@ -612,6 +613,7 @@ func (c *Client) R() *Request {
612613
Cookies: make([]*http.Cookie, 0),
613614
PathParams: make(map[string]string),
614615
RawPathParams: make(map[string]string),
616+
Timeout: c.timeout,
615617
Debug: c.debug,
616618
IsTrace: c.isTrace,
617619
AuthScheme: c.authScheme,
@@ -1122,13 +1124,24 @@ func (c *Client) SetContentLength(l bool) *Client {
11221124
return c
11231125
}
11241126

1125-
// SetTimeout method sets the timeout for a request raised by the client.
1127+
// Timeout method returns the timeout duration value from the client
1128+
func (c *Client) Timeout() time.Duration {
1129+
c.lock.RLock()
1130+
defer c.lock.RUnlock()
1131+
return c.timeout
1132+
}
1133+
1134+
// SetTimeout method is used to set a timeout for a request raised by the client.
1135+
//
1136+
// client.SetTimeout(1 * time.Minute)
1137+
//
1138+
// It can be overridden at the request level. See [Request.SetTimeout]
11261139
//
1127-
// client.SetTimeout(time.Duration(1 * time.Minute))
1140+
// NOTE: Resty uses [context.WithTimeout] on the request, it does not use [http.Client.Timeout]
11281141
func (c *Client) SetTimeout(timeout time.Duration) *Client {
11291142
c.lock.Lock()
11301143
defer c.lock.Unlock()
1131-
c.httpClient.Timeout = timeout
1144+
c.timeout = timeout
11321145
return c
11331146
}
11341147

@@ -2077,7 +2090,7 @@ func (c *Client) execute(req *Request) (*Response, error) {
20772090
requestDebugLogger(c, req)
20782091

20792092
req.Time = time.Now()
2080-
resp, err := c.Client().Do(req.RawRequest)
2093+
resp, err := c.Client().Do(req.withTimeout())
20812094

20822095
response := &Response{Request: req, RawResponse: resp}
20832096
response.setReceivedAt()

client_test.go

+2-3
Original file line numberDiff line numberDiff line change
@@ -135,10 +135,9 @@ func TestClientTimeout(t *testing.T) {
135135
ts := createGetServer(t)
136136
defer ts.Close()
137137

138-
c := dcnl().SetTimeout(time.Millisecond * 200)
138+
c := dcnl().SetTimeout(200 * time.Millisecond)
139139
_, err := c.R().Get(ts.URL + "/set-timeout-test")
140-
141-
assertEqual(t, true, strings.Contains(err.Error(), "Client.Timeout"))
140+
assertEqual(t, true, errors.Is(err, context.DeadlineExceeded))
142141
}
143142

144143
func TestClientTimeoutWithinThreshold(t *testing.T) {

context_test.go

+1-3
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
"context"
1111
"errors"
1212
"net/http"
13-
"strings"
1413
"sync/atomic"
1514
"testing"
1615
"time"
@@ -212,8 +211,7 @@ func TestClientRetryWithSetContext(t *testing.T) {
212211

213212
assertNotNil(t, ts)
214213
assertNotNil(t, err)
215-
assertEqual(t, true, (strings.HasPrefix(err.Error(), "Get "+ts.URL+"/") ||
216-
strings.HasPrefix(err.Error(), "Get \""+ts.URL+"/\"")))
214+
assertEqual(t, true, errors.Is(err, context.DeadlineExceeded))
217215
}
218216

219217
func TestRequestContext(t *testing.T) {

request.go

+35-2
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ type Request struct {
6161
AllowMethodGetPayload bool
6262
AllowMethodDeletePayload bool
6363
IsDone bool
64+
Timeout time.Duration
6465
RetryCount int
6566
RetryWaitTime time.Duration
6667
RetryMaxWaitTime time.Duration
@@ -84,6 +85,7 @@ type Request struct {
8485
isSaveResponse bool
8586
jsonEscapeHTML bool
8687
ctx context.Context
88+
ctxCancelFunc context.CancelFunc
8789
values map[string]any
8890
client *Client
8991
bodyBuf *bytes.Buffer
@@ -894,6 +896,18 @@ func (r *Request) SetCookies(rs []*http.Cookie) *Request {
894896
return r
895897
}
896898

899+
// SetTimeout method is used to set a timeout for the current request
900+
//
901+
// client.R().SetTimeout(1 * time.Minute)
902+
//
903+
// It overrides the timeout set at the client instance level, See [Client.SetTimeout]
904+
//
905+
// NOTE: Resty uses [context.WithTimeout] on the request, it does not use [http.Client.Timeout]
906+
func (r *Request) SetTimeout(timeout time.Duration) *Request {
907+
r.Timeout = timeout
908+
return r
909+
}
910+
897911
// SetLogger method sets given writer for logging Resty request and response details.
898912
// By default, requests and responses inherit their logger from the client.
899913
//
@@ -1273,8 +1287,14 @@ func (r *Request) Execute(method, url string) (res *Response, err error) {
12731287
break
12741288
}
12751289
if r.Context().Err() != nil {
1276-
err = wrapErrors(r.Context().Err(), err)
1277-
break
1290+
if r.ctxCancelFunc != nil {
1291+
r.ctxCancelFunc()
1292+
r.ctxCancelFunc = nil
1293+
}
1294+
if !errors.Is(err, context.DeadlineExceeded) {
1295+
err = wrapErrors(r.Context().Err(), err)
1296+
break
1297+
}
12781298
}
12791299
}
12801300

@@ -1425,6 +1445,7 @@ func (r *Request) Clone(ctx context.Context) *Request {
14251445
rr.initTraceIfEnabled()
14261446
r.values = make(map[string]any)
14271447
r.multipartErrChan = nil
1448+
r.ctxCancelFunc = nil
14281449

14291450
// copy bodyBuf
14301451
if r.bodyBuf != nil {
@@ -1634,6 +1655,18 @@ func (r *Request) isIdempotent() bool {
16341655
return found || r.AllowNonIdempotentRetry
16351656
}
16361657

1658+
func (r *Request) withTimeout() *http.Request {
1659+
if _, found := r.Context().Deadline(); found {
1660+
return r.RawRequest
1661+
}
1662+
if r.Timeout > 0 {
1663+
ctx, ctxCancelFunc := context.WithTimeout(r.Context(), r.Timeout)
1664+
r.ctxCancelFunc = ctxCancelFunc
1665+
return r.RawRequest.WithContext(ctx)
1666+
}
1667+
return r.RawRequest
1668+
}
1669+
16371670
func jsonIndent(v []byte) []byte {
16381671
buf := acquireBuffer()
16391672
defer releaseBuffer(buf)

request_test.go

+41
Original file line numberDiff line numberDiff line change
@@ -2110,6 +2110,47 @@ func TestRequestNoRetryOnNonIdempotentMethod(t *testing.T) {
21102110
assertEqual(t, 500, resp.StatusCode())
21112111
}
21122112

2113+
func TestRequestContextTimeout(t *testing.T) {
2114+
ts := createGetServer(t)
2115+
defer ts.Close()
2116+
2117+
t.Run("use client set timeout", func(t *testing.T) {
2118+
c := dcnl().SetTimeout(200 * time.Millisecond)
2119+
assertEqual(t, true, c.Timeout() > 0)
2120+
2121+
req := c.R()
2122+
assertEqual(t, true, req.Timeout > 0)
2123+
2124+
_, err := req.Get(ts.URL + "/set-timeout-test")
2125+
2126+
assertEqual(t, true, errors.Is(err, context.DeadlineExceeded))
2127+
})
2128+
2129+
t.Run("use request set timeout", func(t *testing.T) {
2130+
c := dcnl()
2131+
assertEqual(t, true, c.Timeout() == 0)
2132+
2133+
_, err := c.R().
2134+
SetTimeout(200 * time.Millisecond).
2135+
Get(ts.URL + "/set-timeout-test")
2136+
2137+
assertEqual(t, true, errors.Is(err, context.DeadlineExceeded))
2138+
})
2139+
2140+
t.Run("use external context for timeout", func(t *testing.T) {
2141+
ctx, ctxCancelFunc := context.WithTimeout(context.Background(), 200*time.Millisecond)
2142+
defer ctxCancelFunc()
2143+
2144+
c := dcnl()
2145+
_, err := c.R().
2146+
SetContext(ctx).
2147+
Get(ts.URL + "/set-timeout-test")
2148+
2149+
assertEqual(t, true, errors.Is(err, context.DeadlineExceeded))
2150+
})
2151+
2152+
}
2153+
21132154
func TestRequestPanicContext(t *testing.T) {
21142155
defer func() {
21152156
if r := recover(); r == nil {

retry_test.go

+8-15
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,12 @@ func TestConditionalGetRequestLevel(t *testing.T) {
8585
logResponse(t, resp)
8686
}
8787

88-
func TestClientRetryGet(t *testing.T) {
88+
func TestClientRetryGetWithTimeout(t *testing.T) {
8989
ts := createGetServer(t)
9090
defer ts.Close()
9191

9292
c := dcnl().
93-
SetTimeout(time.Millisecond * 50).
93+
SetTimeout(50 * time.Millisecond).
9494
SetRetryCount(3)
9595

9696
resp, err := c.R().Get(ts.URL + "/set-retrycount-test")
@@ -99,9 +99,7 @@ func TestClientRetryGet(t *testing.T) {
9999
assertEqual(t, 0, resp.StatusCode())
100100
assertEqual(t, 0, len(resp.Cookies()))
101101
assertEqual(t, 0, len(resp.Header()))
102-
103-
assertEqual(t, true, strings.HasPrefix(err.Error(), "Get "+ts.URL+"/set-retrycount-test") ||
104-
strings.HasPrefix(err.Error(), "Get \""+ts.URL+"/set-retrycount-test\""))
102+
assertEqual(t, true, errors.Is(err, context.DeadlineExceeded))
105103
}
106104

107105
func TestClientRetryWithMinAndMaxWaitTime(t *testing.T) {
@@ -561,7 +559,7 @@ func TestClientRetryCountWithTimeout(t *testing.T) {
561559
attempt := 0
562560

563561
c := dcnl().
564-
SetTimeout(time.Millisecond * 50).
562+
SetTimeout(50 * time.Millisecond).
565563
SetRetryCount(1).
566564
AddRetryCondition(
567565
func(r *Response, _ error) bool {
@@ -576,12 +574,8 @@ func TestClientRetryCountWithTimeout(t *testing.T) {
576574
assertEqual(t, 0, resp.StatusCode())
577575
assertEqual(t, 0, len(resp.Cookies()))
578576
assertEqual(t, 0, len(resp.Header()))
579-
580-
// 2 attempts were made
581577
assertEqual(t, 2, resp.Request.Attempt)
582-
583-
assertEqual(t, true, strings.HasPrefix(err.Error(), "Get "+ts.URL+"/set-retrycount-test") ||
584-
strings.HasPrefix(err.Error(), "Get \""+ts.URL+"/set-retrycount-test\""))
578+
assertEqual(t, true, errors.Is(err, context.DeadlineExceeded))
585579
}
586580

587581
func TestClientRetryTooManyRequestsAndRecover(t *testing.T) {
@@ -596,6 +590,7 @@ func TestClientRetryTooManyRequestsAndRecover(t *testing.T) {
596590
SetHeader(hdrContentTypeKey, "application/json; charset=utf-8").
597591
SetJSONEscapeHTML(false).
598592
SetResult(AuthSuccess{}).
593+
SetTimeout(10 * time.Millisecond).
599594
Get(ts.URL + "/set-retry-error-recover")
600595

601596
assertError(t, err)
@@ -608,7 +603,7 @@ func TestClientRetryTooManyRequestsAndRecover(t *testing.T) {
608603
assertNil(t, resp.Error())
609604
}
610605

611-
func TestClientRetryHook(t *testing.T) {
606+
func TestClientRetryHookWithTimeout(t *testing.T) {
612607
ts := createGetServer(t)
613608
defer ts.Close()
614609

@@ -641,9 +636,7 @@ func TestClientRetryHook(t *testing.T) {
641636

642637
assertEqual(t, retryCount+1, resp.Request.Attempt)
643638
assertEqual(t, 3, hookCalledCount)
644-
645-
assertEqual(t, true, strings.HasPrefix(err.Error(), "Get "+ts.URL+"/set-retrycount-test") ||
646-
strings.HasPrefix(err.Error(), "Get \""+ts.URL+"/set-retrycount-test\""))
639+
assertEqual(t, true, errors.Is(err, context.DeadlineExceeded))
647640
}
648641

649642
var errSeekFailure = fmt.Errorf("failing seek test")

0 commit comments

Comments
 (0)