From f1ffa65db0f5cabff416564b08dc3fa1beff0700 Mon Sep 17 00:00:00 2001 From: wenchy Date: Thu, 5 Feb 2026 16:23:21 +0800 Subject: [PATCH] fix: Timeout option sets the HTTP request context timeout by [context.WithTimeout] --- client.go | 16 ++++++++-------- options.go | 7 +++---- request.go | 5 +++-- request_test.go | 41 +++++++++++++++++++++++++++++------------ 4 files changed, 43 insertions(+), 26 deletions(-) diff --git a/client.go b/client.go index 831274e..e36046b 100644 --- a/client.go +++ b/client.go @@ -51,7 +51,13 @@ func NewClient(setters ...ClientOption) *Client { // request is the common func to send an HTTP request. func (c *Client) request(method, url string, opts *Options, body []byte) (*Response, error) { - r, err := newRequest(method, url, opts, body) + ctx := opts.ctx + if opts.Timeout > 0 { // ctx with timeout if specified + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, opts.Timeout) + defer cancel() + } + r, err := newRequest(ctx, method, url, opts, body) if err != nil { return nil, err } @@ -62,12 +68,6 @@ func (c *Client) request(method, url string, opts *Options, body []byte) (*Respo } *r.opts.DumpRequestOut = string(reqDump) } - ctx := opts.ctx - if opts.Timeout > 0 { - var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(ctx, opts.Timeout) - defer cancel() - } var interceptors []InterceptorFunc if r.opts.Interceptor != nil { interceptors = append(interceptors, r.opts.Interceptor) @@ -84,7 +84,7 @@ func (c *Client) request(method, url string, opts *Options, body []byte) (*Respo // do sends an HTTP request and returns an HTTP response, following policy // (such as redirects, cookies, auth) as configured on the client. -func (c *Client) do(ctx context.Context, r *Request) (*Response, error) { +func (c *Client) do(_ context.Context, r *Request) (*Response, error) { // If the returned error is nil, the Response will contain // a non-nil Body which the user is expected to close. resp, err := c.client.Do(r.Request) diff --git a/options.go b/options.go index 6bee0b5..28bb70a 100644 --- a/options.go +++ b/options.go @@ -331,8 +331,7 @@ func BasicAuth(username, password string) Option { } } -// Timeout creates a new context with specified timeout for -// the current request. +// Timeout sets the HTTP request context timeout by [context.WithTimeout]. func Timeout(timeout time.Duration) Option { return func(opts *Options) { opts.Timeout = timeout @@ -343,8 +342,8 @@ func Timeout(timeout time.Duration) Option { // input param (req or resp) if not nil. // // Refer: -// - https://pkg.go.dev/net/http/httputil#DumpRequestOut -// - https://pkg.go.dev/net/http/httputil#DumpResponse +// - https://pkg.go.dev/net/http/httputil#DumpRequestOut +// - https://pkg.go.dev/net/http/httputil#DumpResponse func Dump(req, resp *string) Option { return func(opts *Options) { opts.DumpRequestOut = req diff --git a/request.go b/request.go index 6ed6820..94d2c58 100644 --- a/request.go +++ b/request.go @@ -5,6 +5,7 @@ package requests import ( "bytes" + "context" "encoding/json" "fmt" "io" @@ -32,8 +33,8 @@ func (r *Request) Text() string { } // newRequest creates a new HTTP request. -func newRequest(method, url string, opts *Options, body []byte) (*Request, error) { - r, err := http.NewRequestWithContext(opts.ctx, method, url, opts.Body) +func newRequest(ctx context.Context, method, url string, opts *Options, body []byte) (*Request, error) { + r, err := http.NewRequestWithContext(ctx, method, url, opts.Body) if err != nil { return nil, err } diff --git a/request_test.go b/request_test.go index 5d2d26b..6841b68 100644 --- a/request_test.go +++ b/request_test.go @@ -126,14 +126,10 @@ func TestGetWithContext(t *testing.T) { time.Sleep(100 * time.Millisecond) w.WriteHeader(http.StatusOK) })) - defer testServer.Close() - ctx10ms, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond) - defer cancel() - ctx200ms, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) - defer cancel() type args struct { - url string - options []Option + url string + ctxTimeout time.Duration + options []Option } tests := []struct { name string @@ -143,19 +139,37 @@ func TestGetWithContext(t *testing.T) { { name: "with context 10ms", args: args{ - url: testServer.URL, + url: testServer.URL, + ctxTimeout: 10 * time.Millisecond, + }, + wantErr: true, + }, + { + name: "with context 200ms", + args: args{ + url: testServer.URL, + ctxTimeout: 200 * time.Millisecond, + }, + wantErr: false, + }, + { + name: "with context 200ms and WithTimeout 10ms", + args: args{ + url: testServer.URL, + ctxTimeout: 200 * time.Millisecond, options: []Option{ - Context(ctx10ms), + Timeout(10 * time.Millisecond), }, }, wantErr: true, }, { - name: "with context 200ms", + name: "with context 200ms and WithTimeout 150ms", args: args{ - url: testServer.URL, + url: testServer.URL, + ctxTimeout: 200 * time.Millisecond, options: []Option{ - Context(ctx200ms), + Timeout(150 * time.Millisecond), }, }, wantErr: false, @@ -163,6 +177,9 @@ func TestGetWithContext(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), tt.args.ctxTimeout) + defer cancel() + tt.args.options = append(tt.args.options, Context(ctx)) got, err := Get(tt.args.url, tt.args.options...) if (err != nil) != tt.wantErr { t.Errorf("Get() error = %v, wantErr %v", err, tt.wantErr)