From a4bb30bdc84b5e839b7e4206c134ab4b3c586025 Mon Sep 17 00:00:00 2001 From: Kybxd <627940450@qq.com> Date: Fri, 18 Jul 2025 11:26:44 +0800 Subject: [PATCH 1/9] feat: allow creating custom clients --- auth.go | 16 +++ client.go | 61 ++++++-- default.go | 33 +++++ env.go | 84 ----------- interceptor.go | 38 +++++ internal/auth/auth.go | 16 --- method.go | 47 ++++++- options.go | 48 +------ .../redirector/redirect.go => redirector.go | 2 +- request.go | 128 +++-------------- request_test.go | 131 +----------------- 11 files changed, 203 insertions(+), 401 deletions(-) create mode 100644 auth.go create mode 100644 default.go delete mode 100644 env.go create mode 100644 interceptor.go delete mode 100644 internal/auth/auth.go rename internal/auth/redirector/redirect.go => redirector.go (95%) diff --git a/auth.go b/auth.go new file mode 100644 index 0000000..61c476d --- /dev/null +++ b/auth.go @@ -0,0 +1,16 @@ +package requests + +type AuthType int + +const ( + NoAuth AuthType = iota + AuthTypeBasic + AuthTypeProxy // TODO + AuthTypeDigest // TODO +) + +type AuthInfo struct { + Type AuthType + Username string + Password string +} diff --git a/client.go b/client.go index 5c967ba..173345b 100644 --- a/client.go +++ b/client.go @@ -4,23 +4,57 @@ import ( "context" "net/http" "net/http/httputil" + "time" ) -// Do is called by Interceptor to complete HTTP requests. -type Do func(ctx context.Context, r *Request) (*Response, error) - -// InterceptorFunc provides a hook to intercept the execution of an HTTP request -// invocation. When an interceptor(s) is set, requests delegates all HTTP -// client invocations to the interceptor, and it is the responsibility of the -// interceptor to call do to complete the processing of the HTTP request. -type InterceptorFunc func(ctx context.Context, r *Request, do Do) (*Response, error) +// ClientOption is the functional option type. +type ClientOption func(*Client) type Client struct { *http.Client + interceptor InterceptorFunc +} + +func NewClient(options ...ClientOption) *Client { + client := newDefaultClient() + for _, setter := range options { + setter(client) + } + return client +} + +func WithTimeout(timeout time.Duration) ClientOption { + return func(c *Client) { + c.Timeout = timeout + } +} + +func WithTransport(transport http.RoundTripper) ClientOption { + return func(c *Client) { + c.Transport = transport + } } -// Do sends the HTTP request and returns after response is received. -func (c *Client) Do(ctx context.Context, r *Request) (*Response, error) { +func WithInterceptor(interceptors ...InterceptorFunc) ClientOption { + return func(c *Client) { + c.interceptor = ChainInterceptors(interceptors...) + } +} + +// 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(method, url string, opts *Options, body []byte) (*Response, error) { + r, err := newRequest(method, url, opts, body) + if err != nil { + return nil, err + } + var ctx context.Context + if opts.ctx != nil { + ctx = opts.ctx // use ctx from options if set + r.WithContext(opts.ctx) + } else { + ctx = context.Background() + } if r.opts.DumpRequestOut != nil { reqDump, err := httputil.DumpRequestOut(r.Request, true) if err != nil { @@ -28,15 +62,12 @@ func (c *Client) Do(ctx context.Context, r *Request) (*Response, error) { } *r.opts.DumpRequestOut = string(reqDump) } - if ctx != nil { - r = r.WithContext(ctx) - } var interceptors []InterceptorFunc if r.opts.Interceptor != nil { interceptors = append(interceptors, r.opts.Interceptor) } - if env.interceptor != nil { - interceptors = append(interceptors, env.interceptor) + if c.interceptor != nil { + interceptors = append(interceptors, c.interceptor) } interceptor := ChainInterceptors(interceptors...) if interceptor != nil { diff --git a/default.go b/default.go new file mode 100644 index 0000000..15261fe --- /dev/null +++ b/default.go @@ -0,0 +1,33 @@ +package requests + +import ( + "net/http" + "sync" +) + +var ( + once sync.Once + defaultClient *Client +) + +func newDefaultClient() *Client { + return &Client{ + Client: &http.Client{ + CheckRedirect: RedirectPolicyFunc, + }, + } +} + +func GetDefaultClient() *Client { + once.Do(func() { + defaultClient = newDefaultClient() + }) + return defaultClient +} + +func InitDefaultClient(options ...ClientOption) { + client := GetDefaultClient() + for _, setter := range options { + setter(client) + } +} diff --git a/env.go b/env.go deleted file mode 100644 index 875e637..0000000 --- a/env.go +++ /dev/null @@ -1,84 +0,0 @@ -package requests - -import ( - "context" - "net/http" - "time" -) - -type environment struct { - timeout time.Duration - // transport establishes network connections as needed and - // caches them for reuse by subsequent calls. It uses HTTP proxies as - // directed by the environment variables HTTP_PROXY, HTTPS_PROXY and - // NO_PROXY (or the lowercase versions thereof). - transport *http.Transport - // hostRoundTrippers specify the host-specific RoundTripper to use for the - // request. If not found, http.DefaultTransport is used. - hostRoundTrippers map[string]http.RoundTripper - // interceptor intercepts each HTTP request. - interceptor InterceptorFunc -} - -var env environment - -func init() { - env.timeout = 60 * time.Second // default timeout - transport, ok := http.DefaultTransport.(*http.Transport) - if !ok { - panic("Ooh! http.DefaultTransport's underlying is not *http.Transport. Maybe golang team has changed it.") - } - env.transport = transport -} - -// SetEnvTimeout sets the default timeout for each HTTP request at -// the environment level. -func SetEnvTimeout(timeout time.Duration) { - env.timeout = timeout -} - -// WithInterceptor specifies the interceptor for each HTTP request. -func WithInterceptor(interceptors ...InterceptorFunc) { - // Prepend env.interceptor to the chaining interceptors if it exists, since - // env.interceptor will be executed before any other chained interceptors. - if env.interceptor != nil { - interceptors = append([]InterceptorFunc{env.interceptor}, interceptors...) - } - env.interceptor = ChainInterceptors(interceptors...) -} - -// ChainInterceptors chains multiple interceptors into one. -func ChainInterceptors(interceptors ...InterceptorFunc) InterceptorFunc { - switch len(interceptors) { - case 0: - return nil - case 1: - return interceptors[0] - default: - return func(ctx context.Context, r *Request, do Do) (*Response, error) { - return interceptors[0](ctx, r, getChainDo(interceptors, 0, do)) - } - } -} - -// getChainDo generates the chained do recursively. -func getChainDo(interceptors []InterceptorFunc, curr int, finalDo Do) Do { - if curr == len(interceptors)-1 { - return finalDo - } - return func(ctx context.Context, r *Request) (*Response, error) { - return interceptors[curr+1](ctx, r, getChainDo(interceptors, curr+1, finalDo)) - } -} - -// SetHostTransport sets the host-specific RoundTripper to use for the request. -// -// # Example -// -// SetHostTransport(map[string]http.RoundTripper{ -// "example1.com": http.DefaultTransport, -// "example2.com": MyCustomTransport, -// }) -func SetHostTransport(rts map[string]http.RoundTripper) { - env.hostRoundTrippers = rts -} diff --git a/interceptor.go b/interceptor.go new file mode 100644 index 0000000..7558623 --- /dev/null +++ b/interceptor.go @@ -0,0 +1,38 @@ +package requests + +import ( + "context" +) + +// Do is called by Interceptor to complete HTTP requests. +type Do func(ctx context.Context, r *Request) (*Response, error) + +// InterceptorFunc provides a hook to intercept the execution of an HTTP request +// invocation. When an interceptor(s) is set, requests delegates all HTTP +// client invocations to the interceptor, and it is the responsibility of the +// interceptor to call do to complete the processing of the HTTP request. +type InterceptorFunc func(ctx context.Context, r *Request, do Do) (*Response, error) + +// ChainInterceptors chains multiple interceptors into one. +func ChainInterceptors(interceptors ...InterceptorFunc) InterceptorFunc { + switch len(interceptors) { + case 0: + return nil + case 1: + return interceptors[0] + default: + return func(ctx context.Context, r *Request, do Do) (*Response, error) { + return interceptors[0](ctx, r, getChainDo(interceptors, 0, do)) + } + } +} + +// getChainDo generates the chained do recursively. +func getChainDo(interceptors []InterceptorFunc, curr int, finalDo Do) Do { + if curr == len(interceptors)-1 { + return finalDo + } + return func(ctx context.Context, r *Request) (*Response, error) { + return interceptors[curr+1](ctx, r, getChainDo(interceptors, curr+1, finalDo)) + } +} diff --git a/internal/auth/auth.go b/internal/auth/auth.go deleted file mode 100644 index a42dd5d..0000000 --- a/internal/auth/auth.go +++ /dev/null @@ -1,16 +0,0 @@ -package auth - -type Type int - -const ( - NoAuth Type = iota - BasicAuth - ProxyAuth // TODO - DigestAuth // TODO -) - -type AuthInfo struct { - Type Type - Username string - Password string -} diff --git a/method.go b/method.go index 6076f21..6f87d95 100644 --- a/method.go +++ b/method.go @@ -2,17 +2,54 @@ package requests import "net/http" +// Get sends an HTTP request with GET method. +// +// On error, any Response can be ignored. A non-nil Response with a +// non-nil error only occurs when Response.StatusCode() is not 2xx. +func (c *Client) Get(url string, options ...Option) (*Response, error) { + return c.callMethod(http.MethodGet, url, options...) +} + +// Post sends an HTTP POST request. +func (c *Client) Post(url string, options ...Option) (*Response, error) { + return c.callMethod(http.MethodPost, url, options...) +} + +// Put sends an HTTP request with PUT method. +// +// On error, any Response can be ignored. A non-nil Response with a +// non-nil error only occurs when Response.StatusCode() is not 2xx. +func (c *Client) Put(url string, options ...Option) (*Response, error) { + return c.callMethod(http.MethodPut, url, options...) +} + +// Patch sends an HTTP request with PATCH method. +// +// On error, any Response can be ignored. A non-nil Response with a +// non-nil error only occurs when Response.StatusCode() is not 2xx. +func (c *Client) Patch(url string, options ...Option) (*Response, error) { + return c.callMethod(http.MethodPatch, url, options...) +} + +// Delete sends an HTTP request with DELETE method. +// +// On error, any Response can be ignored. A non-nil Response with a +// non-nil error only occurs when Response.StatusCode() is not 2xx. +func (c *Client) Delete(url string, options ...Option) (*Response, error) { + return c.callMethod(http.MethodDelete, url, options...) +} + // Get sends an HTTP request with GET method. // // On error, any Response can be ignored. A non-nil Response with a // non-nil error only occurs when Response.StatusCode() is not 2xx. func Get(url string, options ...Option) (*Response, error) { - return callMethod(http.MethodGet, url, options...) + return GetDefaultClient().Get(url, options...) } // Post sends an HTTP POST request. func Post(url string, options ...Option) (*Response, error) { - return callMethod(http.MethodPost, url, options...) + return GetDefaultClient().Post(url, options...) } // Put sends an HTTP request with PUT method. @@ -20,7 +57,7 @@ func Post(url string, options ...Option) (*Response, error) { // On error, any Response can be ignored. A non-nil Response with a // non-nil error only occurs when Response.StatusCode() is not 2xx. func Put(url string, options ...Option) (*Response, error) { - return callMethod(http.MethodPut, url, options...) + return GetDefaultClient().Put(url, options...) } // Patch sends an HTTP request with PATCH method. @@ -28,7 +65,7 @@ func Put(url string, options ...Option) (*Response, error) { // On error, any Response can be ignored. A non-nil Response with a // non-nil error only occurs when Response.StatusCode() is not 2xx. func Patch(url string, options ...Option) (*Response, error) { - return callMethod(http.MethodPatch, url, options...) + return GetDefaultClient().Patch(url, options...) } // Delete sends an HTTP request with DELETE method. @@ -36,5 +73,5 @@ func Patch(url string, options ...Option) (*Response, error) { // On error, any Response can be ignored. A non-nil Response with a // non-nil error only occurs when Response.StatusCode() is not 2xx. func Delete(url string, options ...Option) (*Response, error) { - return callMethod(http.MethodDelete, url, options...) + return GetDefaultClient().Delete(url, options...) } diff --git a/options.go b/options.go index 870c432..49b850e 100644 --- a/options.go +++ b/options.go @@ -7,9 +7,6 @@ import ( "net/http" "net/url" "os" - "time" - - "github.com/Wenchy/requests/internal/auth" ) // Options defines all optional parameters for HTTP request. @@ -33,19 +30,13 @@ type Options struct { ToJSON any // auth - AuthInfo *auth.AuthInfo - // request timeout - Timeout time.Duration - - DisableKeepAlives bool + AuthInfo *AuthInfo // dump DumpRequestOut *string DumpResponse *string // interceptor Interceptor InterceptorFunc - // round tripper - RoundTripper http.RoundTripper } // Option is the functional option type. @@ -56,7 +47,6 @@ func newDefaultOptions() *Options { return &Options{ Headers: http.Header{}, bodyType: bodyTypeDefault, - Timeout: env.timeout, } } @@ -326,37 +316,14 @@ func ToJSON(v any) Option { // BasicAuth is the option to implement HTTP Basic Auth. func BasicAuth(username, password string) Option { return func(opts *Options) { - opts.AuthInfo = &auth.AuthInfo{ - Type: auth.BasicAuth, + opts.AuthInfo = &AuthInfo{ + Type: AuthTypeBasic, Username: username, Password: password, } } } -// Timeout specifies a time limit for requests made by this -// Client. The timeout includes connection time, any -// redirects, and reading the response body. The timer remains -// running after Get, Head, Post, or Do return and will -// interrupt reading of the Response.Body. -// -// A Timeout of zero means no timeout. Default is 60s. -func Timeout(timeout time.Duration) Option { - return func(opts *Options) { - opts.Timeout = timeout - } -} - -// DisableKeepAlives, if true, disables HTTP keep-alives and will -// only use the connection to the server for a single HTTP request. -// -// This is unrelated to the similarly named TCP keep-alives. -func DisableKeepAlives() Option { - return func(opts *Options) { - opts.DisableKeepAlives = true - } -} - // Dump dumps outgoing client request and response to the corresponding // input param (req or resp) if not nil. // @@ -377,12 +344,3 @@ func Interceptor(interceptor InterceptorFunc) Option { opts.Interceptor = interceptor } } - -// Transport specifies a custom RoundTripper for current request only. -// -// NOTE: If specified, then option DisableKeepAlives() will not work. -func Transport(rt http.RoundTripper) Option { - return func(opts *Options) { - opts.RoundTripper = rt - } -} diff --git a/internal/auth/redirector/redirect.go b/redirector.go similarity index 95% rename from internal/auth/redirector/redirect.go rename to redirector.go index 9bdb22e..972cda3 100644 --- a/internal/auth/redirector/redirect.go +++ b/redirector.go @@ -1,4 +1,4 @@ -package redirector +package requests import ( "errors" diff --git a/request.go b/request.go index be35142..d5a02d3 100644 --- a/request.go +++ b/request.go @@ -11,9 +11,6 @@ import ( "io" "mime/multipart" "net/http" - - "github.com/Wenchy/requests/internal/auth" - "github.com/Wenchy/requests/internal/auth/redirector" ) // Request is a wrapper of http.Request. @@ -25,9 +22,8 @@ type Request struct { // WithContext returns a shallow copy of r.Request with its context changed to ctx. // The provided ctx must be non-nil. -func (r *Request) WithContext(ctx context.Context) *Request { +func (r *Request) WithContext(ctx context.Context) { r.Request = r.Request.WithContext(ctx) - return r } // Bytes returns the HTTP request body as []byte. @@ -65,93 +61,15 @@ func newRequest(method, url string, opts *Options, body []byte) (*Request, error // auth if opts.AuthInfo != nil { // TODO(wenchy): some other auth types - if opts.AuthInfo.Type == auth.BasicAuth { + if opts.AuthInfo.Type == AuthTypeBasic { r.SetBasicAuth(opts.AuthInfo.Username, opts.AuthInfo.Password) } } return &Request{Request: r, opts: opts, body: body}, nil } -// do sends an HTTP request and returns an HTTP response, following policy -// (such as redirects, cookies, auth) as configured on the client. -func do(method, url string, opts *Options, body []byte) (*Response, error) { - req, err := newRequest(method, url, opts, body) - if err != nil { - return nil, err - } - - // NOTE: Keep-Alive & Connection Pooling - // - // 1. Keep-Alive - // - // The net/http Transport documentation uses the term to refer to - // persistent connections. A keep-alive or persistent connection - // is a connection that can be used for more than one HTTP - // transaction. - // - // The Transport.IdleConnTimeout field specifies how long the - // transport keeps an unused connection in the pool before closing - // the connection. - // - // The net Dialer documentation uses the keep-alive term to refer - // the TCP feature for probing the health of a connection. - // Dialer.KeepAlive field specifies how frequently TCP keep-alive - // probes are sent to the peer. - // - // 2. Connection Pooling - // - // Connections are added to the pool in the function - // Transport.tryPutIdleConn. The connection is not pooled if - // Transport.DisableKeepAlives is true or Transport.MaxIdleConnsPerHost - // is less than zero. - // - // Setting either value disables pooling. The transport adds the - // "Connection: close" request header when DisableKeepAlives is true. - // This may or may not be desirable depending on what you are testing. - // - // 3. References: - // - // - https://stackoverflow.com/questions/57683132/turning-off-connection-pool-for-go-http-client - // - https://stackoverflow.com/questions/59656164/what-is-the-difference-between-net-dialerkeepalive-and-http-transportidletimeo - var roundTripper http.RoundTripper - if opts.RoundTripper != nil { - roundTripper = opts.RoundTripper - } else { - if rt := env.hostRoundTrippers[req.Host]; rt != nil { - // Use the host-specific RoundTripper if set. - roundTripper = rt - } else if opts.DisableKeepAlives { - // If option DisableKeepAlives set as true, then clone a new transport - // just for this one-off HTTP request. - transport := env.transport.Clone() - transport.DisableKeepAlives = true - roundTripper = transport - } else { - // If option DisableKeepAlives not set as true, then use the default - // transport. - roundTripper = env.transport - } - } - client := &Client{ - Client: &http.Client{ - CheckRedirect: redirector.RedirectPolicyFunc, - Timeout: opts.Timeout, - Transport: roundTripper, - }, - } - var ctx context.Context - if opts.ctx != nil { - ctx = opts.ctx // use ctx from options if set - } else { - newCtx, cancel := context.WithTimeout(context.Background(), opts.Timeout) - defer cancel() - ctx = newCtx - } - return client.Do(ctx, req) -} - // request sends an HTTP request. -func request(method, url string, opts *Options) (*Response, error) { +func request(c *Client, method, url string, opts *Options) (*Response, error) { // NOTE: get the body size from io.Reader. It is costy for large body. body := bytes.NewBuffer(nil) if opts.Body != nil { @@ -161,12 +79,12 @@ func request(method, url string, opts *Options) (*Response, error) { } } opts.Body = body - return do(method, url, opts, body.Bytes()) + return c.Do(method, url, opts, body.Bytes()) } // requestData sends an HTTP request to the specified URL, with raw string // as the request body. -func requestData(method, url string, opts *Options) (*Response, error) { +func requestData(c *Client, method, url string, opts *Options) (*Response, error) { body := bytes.NewBuffer(nil) if opts.Data != nil { d := fmt.Sprintf("%v", opts.Data) @@ -178,12 +96,12 @@ func requestData(method, url string, opts *Options) (*Response, error) { // TODO: judge content type // opts.Headers["Content-Type"] = "application/x-www-form-urlencoded" opts.Body = body - return do(method, url, opts, body.Bytes()) + return c.Do(method, url, opts, body.Bytes()) } // requestForm sends an HTTP request to the specified URL, with form's keys and // values URL-encoded as the request body. -func requestForm(method, url string, opts *Options) (*Response, error) { +func requestForm(c *Client, method, url string, opts *Options) (*Response, error) { body := bytes.NewBuffer(nil) if opts.Form != nil { d := opts.Form.Encode() @@ -194,11 +112,11 @@ func requestForm(method, url string, opts *Options) (*Response, error) { } opts.Headers.Set("Content-Type", "application/x-www-form-urlencoded") opts.Body = body - return do(method, url, opts, body.Bytes()) + return c.Do(method, url, opts, body.Bytes()) } // requestJSON sends an HTTP request, and encode request body as json. -func requestJSON(method, url string, opts *Options) (*Response, error) { +func requestJSON(c *Client, method, url string, opts *Options) (*Response, error) { body := bytes.NewBuffer(nil) if opts.JSON != nil { d, err := json.Marshal(opts.JSON) @@ -212,11 +130,11 @@ func requestJSON(method, url string, opts *Options) (*Response, error) { } opts.Headers.Set("Content-Type", "application/json") opts.Body = body - return do(method, url, opts, body.Bytes()) + return c.Do(method, url, opts, body.Bytes()) } // requestFiles sends an uploading request for multiple multipart-encoded files. -func requestFiles(method, url string, opts *Options) (*Response, error) { +func requestFiles(c *Client, method, url string, opts *Options) (*Response, error) { body := bytes.NewBuffer(nil) bodyWriter := multipart.NewWriter(body) if opts.Files != nil { @@ -236,7 +154,7 @@ func requestFiles(method, url string, opts *Options) (*Response, error) { } opts.Headers.Set("Content-Type", bodyWriter.FormDataContentType()) opts.Body = body - return do(method, url, opts, body.Bytes()) + return c.Do(method, url, opts, body.Bytes()) } type bodyType int @@ -249,21 +167,17 @@ const ( bodyTypeFiles ) -type dispatcher func(method, url string, opts *Options) (*Response, error) - -var dispatchers map[bodyType]dispatcher +type dispatcher func(c *Client, method, url string, opts *Options) (*Response, error) -func init() { - dispatchers = map[bodyType]dispatcher{ - bodyTypeDefault: request, - bodyTypeData: requestData, - bodyTypeForm: requestForm, - bodyTypeJSON: requestJSON, - bodyTypeFiles: requestFiles, - } +var dispatchers map[bodyType]dispatcher = map[bodyType]dispatcher{ + bodyTypeDefault: request, + bodyTypeData: requestData, + bodyTypeForm: requestForm, + bodyTypeJSON: requestJSON, + bodyTypeFiles: requestFiles, } -func callMethod(method, url string, options ...Option) (*Response, error) { +func (c *Client) callMethod(method, url string, options ...Option) (*Response, error) { opts := parseOptions(options...) - return dispatchers[opts.bodyType](method, url, opts) + return dispatchers[opts.bodyType](c, method, url, opts) } diff --git a/request_test.go b/request_test.go index 9b11149..124121d 100644 --- a/request_test.go +++ b/request_test.go @@ -22,7 +22,7 @@ import ( ) func init() { - WithInterceptor(logInterceptor, metricInterceptor, traceInterceptor) + InitDefaultClient(WithInterceptor(logInterceptor, metricInterceptor, traceInterceptor)) } func logInterceptor(ctx context.Context, r *Request, do Do) (*Response, error) { @@ -86,26 +86,6 @@ func TestGet(t *testing.T) { }, wantErr: false, }, - { - name: "http server not available", - args: args{ - url: "https://127.0.0.1:4004", - options: []Option{ - Timeout(120 * time.Second), - }, - }, - wantErr: true, - }, - { - name: "disable keep alive", - args: args{ - url: testServer.URL, - options: []Option{ - DisableKeepAlives(), - }, - }, - wantErr: false, - }, { name: "manipulate URLs and query parameters", args: args{ @@ -446,7 +426,6 @@ func TestPostJSON(t *testing.T) { type args struct { url string options []Option - timeout time.Duration } tests := []struct { name string @@ -468,16 +447,12 @@ func TestPostJSON(t *testing.T) { ToText(&textResp), Dump(&reqDump, &respDump), }, - timeout: 5 * time.Second, }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if tt.args.timeout != 0 { - SetEnvTimeout(tt.args.timeout) - } got, err := Post(tt.args.url, tt.args.options...) if (err != nil) != tt.wantErr { t.Errorf("Get() error = %v, wantErr %v", err, tt.wantErr) @@ -575,7 +550,7 @@ func TestPostFiles(t *testing.T) { "file1": fh1, "file2": fh2, }), - Timeout(120 * time.Second), + // Timeout(120 * time.Second), }, }, wantErr: false, @@ -589,7 +564,7 @@ func TestPostFiles(t *testing.T) { "file1": fh1, "file2": fh2, }), - Timeout(120 * time.Second), + // Timeout(120 * time.Second), }, }, wantErr: true, @@ -640,7 +615,6 @@ func TestPatch(t *testing.T) { "status": 0, "message": "hello http patch", }), - Timeout(120 * time.Second), }, }, wantErr: false, @@ -654,7 +628,6 @@ func TestPatch(t *testing.T) { "status": 0, "message": "hello http patch", }), - Timeout(120 * time.Second), }, }, wantErr: true, @@ -768,7 +741,6 @@ func TestInterceptors(t *testing.T) { "file1": fh1, "file2": fh2, }), - Timeout(120 * time.Second), }, }, wantErr: false, @@ -791,100 +763,3 @@ func TestInterceptors(t *testing.T) { }) } } - -type CustomTransport struct { - *http.Transport -} - -func (ct CustomTransport) RoundTrip(req *http.Request) (*http.Response, error) { - req.Header.Set("X-Transport", "CustomTransport") - return ct.Transport.RoundTrip(req) -} - -func TestSetHostTransport(t *testing.T) { - testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - body := r.Header.Get("X-Transport") - n, err := w.Write([]byte(body)) - assert.NoError(t, err) - assert.Equal(t, n, len(body)) - })) - defer testServer.Close() - testServer1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - })) - defer testServer1.Close() - testServer2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - body := r.Header.Get("X-Transport") + "(testServer2)" - n, err := w.Write([]byte(body)) - assert.NoError(t, err) - assert.Equal(t, n, len(body)) - })) - defer testServer.Close() - // Parse the URL string - serverURL, err := url.Parse(testServer.URL) - assert.NoError(t, err) - - defaultTransport, ok := http.DefaultTransport.(*http.Transport) - assert.Equal(t, true, ok) - - trans := defaultTransport.Clone() - trans.DisableKeepAlives = true - trans.MaxIdleConns = 1 - trans.IdleConnTimeout = 10 * time.Second - customTransport := CustomTransport{ - Transport: trans, - } - - SetHostTransport(map[string]http.RoundTripper{ - serverURL.Host: customTransport, - }) - type args struct { - url string - options []Option - } - tests := []struct { - name string - args args - wantErr bool - wantBody string - }{ - { - name: "hit host transport at env level", - args: args{ - url: testServer.URL, - }, - wantBody: "CustomTransport", - }, - { - name: "miss host transport at env level", - args: args{ - url: testServer1.URL, - }, - wantBody: "", - }, - { - name: "host transport for current request", - args: args{ - url: testServer2.URL, - options: []Option{ - Transport(customTransport), - }, - }, - wantBody: "CustomTransport(testServer2)", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := Get(tt.args.url, tt.args.options...) - if (err != nil) != tt.wantErr { - t.Errorf("Get() error = %v, wantErr %v", err, tt.wantErr) - return - } - if err == nil { - assert.Equal(t, tt.wantBody, string(got.Bytes())) - } - }) - } -} From a2fd835e7ec31ebfed3b3ae77210c7e4525c644c Mon Sep 17 00:00:00 2001 From: Kybxd <627940450@qq.com> Date: Mon, 21 Jul 2025 14:04:12 +0800 Subject: [PATCH 2/9] feat: use context.Background() as default ctx in Options --- client.go | 11 ++--------- options.go | 1 + request.go | 2 +- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/client.go b/client.go index 173345b..8cafaa0 100644 --- a/client.go +++ b/client.go @@ -48,13 +48,6 @@ func (c *Client) Do(method, url string, opts *Options, body []byte) (*Response, if err != nil { return nil, err } - var ctx context.Context - if opts.ctx != nil { - ctx = opts.ctx // use ctx from options if set - r.WithContext(opts.ctx) - } else { - ctx = context.Background() - } if r.opts.DumpRequestOut != nil { reqDump, err := httputil.DumpRequestOut(r.Request, true) if err != nil { @@ -71,9 +64,9 @@ func (c *Client) Do(method, url string, opts *Options, body []byte) (*Response, } interceptor := ChainInterceptors(interceptors...) if interceptor != nil { - return interceptor(ctx, r, c.do) + return interceptor(opts.ctx, r, c.do) } - return c.do(ctx, r) + return c.do(opts.ctx, r) } func (c *Client) do(ctx context.Context, r *Request) (*Response, error) { diff --git a/options.go b/options.go index 49b850e..9e87532 100644 --- a/options.go +++ b/options.go @@ -45,6 +45,7 @@ type Option func(*Options) // newDefaultOptions creates a new default HTTP options. func newDefaultOptions() *Options { return &Options{ + ctx: context.Background(), Headers: http.Header{}, bodyType: bodyTypeDefault, } diff --git a/request.go b/request.go index d5a02d3..88243ea 100644 --- a/request.go +++ b/request.go @@ -38,7 +38,7 @@ 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.NewRequest(method, url, opts.Body) + r, err := http.NewRequestWithContext(opts.ctx, method, url, opts.Body) if err != nil { return nil, err } From 9997ac56b6f9f9f98e1da38328ac5198added382 Mon Sep 17 00:00:00 2001 From: Kybxd <627940450@qq.com> Date: Mon, 21 Jul 2025 14:34:57 +0800 Subject: [PATCH 3/9] feat: add test cases --- client_test.go | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 client_test.go diff --git a/client_test.go b/client_test.go new file mode 100644 index 0000000..2b4637c --- /dev/null +++ b/client_test.go @@ -0,0 +1,79 @@ +package requests + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + "time" +) + +func TestClientOption(t *testing.T) { + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Logf("query strings: %v", r.URL.Query()) + t.Logf("headers: %v", r.Header) + time.Sleep(100 * time.Millisecond) + w.WriteHeader(http.StatusOK) + })) + defer testServer.Close() + type args struct { + url string + options []ClientOption + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "with timeout", + args: args{ + url: testServer.URL, + options: []ClientOption{ + WithTimeout(time.Millisecond), + }, + }, + wantErr: true, + }, + { + name: "with transport", + args: args{ + url: testServer.URL, + options: []ClientOption{ + WithTransport(func() http.RoundTripper { + transport := http.DefaultTransport.(*http.Transport).Clone() + transport.DisableKeepAlives = true + return transport + }()), + }, + }, + wantErr: false, + }, + { + name: "with interceptor", + args: args{ + url: testServer.URL, + options: []ClientOption{ + WithInterceptor(func(ctx context.Context, r *Request, do Do) (*Response, error) { + t.Logf("method: %s", r.Method) + return do(ctx, r) + }), + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cli := NewClient(tt.args.options...) + got, err := cli.Get(tt.args.url) + if (err != nil) != tt.wantErr { + t.Errorf("Get() error = %v, wantErr %v", err, tt.wantErr) + return + } + if err == nil { + t.Logf("response body: %+v\n", got.Text()) + } + }) + } +} From 47b23b0061b7d9d1f1d44dfe2ed0198840b453df Mon Sep 17 00:00:00 2001 From: Kybxd <627940450@qq.com> Date: Mon, 21 Jul 2025 14:36:39 +0800 Subject: [PATCH 4/9] feat: add test cases --- request_test.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/request_test.go b/request_test.go index 124121d..fb64c02 100644 --- a/request_test.go +++ b/request_test.go @@ -113,13 +113,13 @@ func TestGet(t *testing.T) { func TestGetWithContext(t *testing.T) { testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - time.Sleep(2 * time.Second) + time.Sleep(100 * time.Millisecond) w.WriteHeader(http.StatusOK) })) defer testServer.Close() - ctx1s, cancel := context.WithTimeout(context.Background(), time.Second) + ctx10ms, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond) defer cancel() - ctx5s, cancel := context.WithTimeout(context.Background(), 5*time.Second) + ctx200ms, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) defer cancel() type args struct { url string @@ -131,21 +131,21 @@ func TestGetWithContext(t *testing.T) { wantErr bool }{ { - name: "with context 1s", + name: "with context 10ms", args: args{ url: testServer.URL, options: []Option{ - Context(ctx1s), + Context(ctx10ms), }, }, wantErr: true, }, { - name: "with context 3s", + name: "with context 200ms", args: args{ url: testServer.URL, options: []Option{ - Context(ctx5s), + Context(ctx200ms), }, }, wantErr: false, From 23ae2478dae329ff8c486d33993a368828005784 Mon Sep 17 00:00:00 2001 From: Kybxd <627940450@qq.com> Date: Tue, 22 Jul 2025 21:31:23 +0800 Subject: [PATCH 5/9] fix: cr --- auth.go | 16 ------------ client.go | 10 ++++++-- default.go | 4 ++- internal/auth/auth.go | 16 ++++++++++++ .../auth/redirector/redirector.go | 2 +- options.go | 25 ++++++++++++++++--- request.go | 4 ++- request_test.go | 17 +++++++++++-- 8 files changed, 68 insertions(+), 26 deletions(-) delete mode 100644 auth.go create mode 100644 internal/auth/auth.go rename redirector.go => internal/auth/redirector/redirector.go (95%) diff --git a/auth.go b/auth.go deleted file mode 100644 index 61c476d..0000000 --- a/auth.go +++ /dev/null @@ -1,16 +0,0 @@ -package requests - -type AuthType int - -const ( - NoAuth AuthType = iota - AuthTypeBasic - AuthTypeProxy // TODO - AuthTypeDigest // TODO -) - -type AuthInfo struct { - Type AuthType - Username string - Password string -} diff --git a/client.go b/client.go index 8cafaa0..1891db9 100644 --- a/client.go +++ b/client.go @@ -55,6 +55,12 @@ func (c *Client) Do(method, url string, opts *Options, body []byte) (*Response, } *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) @@ -64,9 +70,9 @@ func (c *Client) Do(method, url string, opts *Options, body []byte) (*Response, } interceptor := ChainInterceptors(interceptors...) if interceptor != nil { - return interceptor(opts.ctx, r, c.do) + return interceptor(ctx, r, c.do) } - return c.do(opts.ctx, r) + return c.do(ctx, r) } func (c *Client) do(ctx context.Context, r *Request) (*Response, error) { diff --git a/default.go b/default.go index 15261fe..39373a5 100644 --- a/default.go +++ b/default.go @@ -3,6 +3,8 @@ package requests import ( "net/http" "sync" + + "github.com/Wenchy/requests/internal/auth/redirector" ) var ( @@ -13,7 +15,7 @@ var ( func newDefaultClient() *Client { return &Client{ Client: &http.Client{ - CheckRedirect: RedirectPolicyFunc, + CheckRedirect: redirector.RedirectPolicyFunc, }, } } diff --git a/internal/auth/auth.go b/internal/auth/auth.go new file mode 100644 index 0000000..a42dd5d --- /dev/null +++ b/internal/auth/auth.go @@ -0,0 +1,16 @@ +package auth + +type Type int + +const ( + NoAuth Type = iota + BasicAuth + ProxyAuth // TODO + DigestAuth // TODO +) + +type AuthInfo struct { + Type Type + Username string + Password string +} diff --git a/redirector.go b/internal/auth/redirector/redirector.go similarity index 95% rename from redirector.go rename to internal/auth/redirector/redirector.go index 972cda3..9bdb22e 100644 --- a/redirector.go +++ b/internal/auth/redirector/redirector.go @@ -1,4 +1,4 @@ -package requests +package redirector import ( "errors" diff --git a/options.go b/options.go index 9e87532..c68f988 100644 --- a/options.go +++ b/options.go @@ -7,6 +7,9 @@ import ( "net/http" "net/url" "os" + "time" + + "github.com/Wenchy/requests/internal/auth" ) // Options defines all optional parameters for HTTP request. @@ -30,7 +33,10 @@ type Options struct { ToJSON any // auth - AuthInfo *AuthInfo + AuthInfo *auth.AuthInfo + // request timeout + Timeout time.Duration + // dump DumpRequestOut *string DumpResponse *string @@ -317,14 +323,27 @@ func ToJSON(v any) Option { // BasicAuth is the option to implement HTTP Basic Auth. func BasicAuth(username, password string) Option { return func(opts *Options) { - opts.AuthInfo = &AuthInfo{ - Type: AuthTypeBasic, + opts.AuthInfo = &auth.AuthInfo{ + Type: auth.BasicAuth, Username: username, Password: password, } } } +// Timeout specifies a time limit for requests made by this +// Client. The timeout includes connection time, any +// redirects, and reading the response body. The timer remains +// running after Get, Head, Post, or Do return and will +// interrupt reading of the Response.Body. +// +// A Timeout of zero means no timeout. Default is 60s. +func Timeout(timeout time.Duration) Option { + return func(opts *Options) { + opts.Timeout = timeout + } +} + // Dump dumps outgoing client request and response to the corresponding // input param (req or resp) if not nil. // diff --git a/request.go b/request.go index 88243ea..e50797c 100644 --- a/request.go +++ b/request.go @@ -11,6 +11,8 @@ import ( "io" "mime/multipart" "net/http" + + "github.com/Wenchy/requests/internal/auth" ) // Request is a wrapper of http.Request. @@ -61,7 +63,7 @@ func newRequest(method, url string, opts *Options, body []byte) (*Request, error // auth if opts.AuthInfo != nil { // TODO(wenchy): some other auth types - if opts.AuthInfo.Type == AuthTypeBasic { + if opts.AuthInfo.Type == auth.BasicAuth { r.SetBasicAuth(opts.AuthInfo.Username, opts.AuthInfo.Password) } } diff --git a/request_test.go b/request_test.go index fb64c02..c86cb9e 100644 --- a/request_test.go +++ b/request_test.go @@ -86,6 +86,16 @@ func TestGet(t *testing.T) { }, wantErr: false, }, + { + name: "http server not available", + args: args{ + url: "https://127.0.0.1:4004", + options: []Option{ + Timeout(120 * time.Second), + }, + }, + wantErr: true, + }, { name: "manipulate URLs and query parameters", args: args{ @@ -550,7 +560,7 @@ func TestPostFiles(t *testing.T) { "file1": fh1, "file2": fh2, }), - // Timeout(120 * time.Second), + Timeout(120 * time.Second), }, }, wantErr: false, @@ -564,7 +574,7 @@ func TestPostFiles(t *testing.T) { "file1": fh1, "file2": fh2, }), - // Timeout(120 * time.Second), + Timeout(120 * time.Second), }, }, wantErr: true, @@ -615,6 +625,7 @@ func TestPatch(t *testing.T) { "status": 0, "message": "hello http patch", }), + Timeout(120 * time.Second), }, }, wantErr: false, @@ -628,6 +639,7 @@ func TestPatch(t *testing.T) { "status": 0, "message": "hello http patch", }), + Timeout(120 * time.Second), }, }, wantErr: true, @@ -741,6 +753,7 @@ func TestInterceptors(t *testing.T) { "file1": fh1, "file2": fh2, }), + Timeout(120 * time.Second), }, }, wantErr: false, From 0a8c7aa03effdc750c4f270e6a548083d8046b81 Mon Sep 17 00:00:00 2001 From: Kybxd <627940450@qq.com> Date: Tue, 22 Jul 2025 21:31:54 +0800 Subject: [PATCH 6/9] fix: file rename --- internal/auth/redirector/{redirector.go => redirect.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename internal/auth/redirector/{redirector.go => redirect.go} (100%) diff --git a/internal/auth/redirector/redirector.go b/internal/auth/redirector/redirect.go similarity index 100% rename from internal/auth/redirector/redirector.go rename to internal/auth/redirector/redirect.go From cc3bdd46259784dea781142954372192c987a050 Mon Sep 17 00:00:00 2001 From: Kybxd <627940450@qq.com> Date: Tue, 22 Jul 2025 21:33:19 +0800 Subject: [PATCH 7/9] fix: cr --- client.go | 8 ++++---- default.go | 2 +- request.go | 7 ------- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/client.go b/client.go index 1891db9..e912f2b 100644 --- a/client.go +++ b/client.go @@ -11,7 +11,7 @@ import ( type ClientOption func(*Client) type Client struct { - *http.Client + client *http.Client interceptor InterceptorFunc } @@ -25,13 +25,13 @@ func NewClient(options ...ClientOption) *Client { func WithTimeout(timeout time.Duration) ClientOption { return func(c *Client) { - c.Timeout = timeout + c.client.Timeout = timeout } } func WithTransport(transport http.RoundTripper) ClientOption { return func(c *Client) { - c.Transport = transport + c.client.Transport = transport } } @@ -78,7 +78,7 @@ func (c *Client) Do(method, url string, opts *Options, body []byte) (*Response, func (c *Client) do(ctx 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) + resp, err := c.client.Do(r.Request) if err != nil { return nil, err } diff --git a/default.go b/default.go index 39373a5..45368e0 100644 --- a/default.go +++ b/default.go @@ -14,7 +14,7 @@ var ( func newDefaultClient() *Client { return &Client{ - Client: &http.Client{ + client: &http.Client{ CheckRedirect: redirector.RedirectPolicyFunc, }, } diff --git a/request.go b/request.go index e50797c..6e10818 100644 --- a/request.go +++ b/request.go @@ -5,7 +5,6 @@ package requests import ( "bytes" - "context" "encoding/json" "fmt" "io" @@ -22,12 +21,6 @@ type Request struct { body []byte // auto filled from Request.Body } -// WithContext returns a shallow copy of r.Request with its context changed to ctx. -// The provided ctx must be non-nil. -func (r *Request) WithContext(ctx context.Context) { - r.Request = r.Request.WithContext(ctx) -} - // Bytes returns the HTTP request body as []byte. func (r *Request) Bytes() []byte { return r.body From cbc86e115a74a3d65493b535d677ac1f915b701b Mon Sep 17 00:00:00 2001 From: Kybxd <627940450@qq.com> Date: Wed, 23 Jul 2025 10:35:04 +0800 Subject: [PATCH 8/9] feat: add notes --- client.go | 12 ++++++++++-- options.go | 9 ++------- request_test.go | 2 +- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/client.go b/client.go index e912f2b..6d827b2 100644 --- a/client.go +++ b/client.go @@ -10,11 +10,13 @@ import ( // ClientOption is the functional option type. type ClientOption func(*Client) +// Client is an HTTP client which wraps around [http.Client] for elegant APIs and easy use. type Client struct { client *http.Client interceptor InterceptorFunc } +// NewClient creates a new client to serve HTTP requests. func NewClient(options ...ClientOption) *Client { client := newDefaultClient() for _, setter := range options { @@ -23,21 +25,27 @@ func NewClient(options ...ClientOption) *Client { return client } +// WithTimeout specifies a time limit for requests made by this client. +// +// A Timeout of zero means no timeout. Default is zero. func WithTimeout(timeout time.Duration) ClientOption { return func(c *Client) { c.client.Timeout = timeout } } +// WithTransport specifies a round ripper for requests made by this client. func WithTransport(transport http.RoundTripper) ClientOption { return func(c *Client) { c.client.Transport = transport } } -func WithInterceptor(interceptors ...InterceptorFunc) ClientOption { +// WithInterceptor specifies an interceptor for requests made by this client. +// Use `ChainInterceptors` to chain multiple interceptors into one. +func WithInterceptor(interceptor InterceptorFunc) ClientOption { return func(c *Client) { - c.interceptor = ChainInterceptors(interceptors...) + c.interceptor = interceptor } } diff --git a/options.go b/options.go index c68f988..6bee0b5 100644 --- a/options.go +++ b/options.go @@ -331,13 +331,8 @@ func BasicAuth(username, password string) Option { } } -// Timeout specifies a time limit for requests made by this -// Client. The timeout includes connection time, any -// redirects, and reading the response body. The timer remains -// running after Get, Head, Post, or Do return and will -// interrupt reading of the Response.Body. -// -// A Timeout of zero means no timeout. Default is 60s. +// Timeout creates a new context with specified timeout for +// the current request. func Timeout(timeout time.Duration) Option { return func(opts *Options) { opts.Timeout = timeout diff --git a/request_test.go b/request_test.go index c86cb9e..5d2d26b 100644 --- a/request_test.go +++ b/request_test.go @@ -22,7 +22,7 @@ import ( ) func init() { - InitDefaultClient(WithInterceptor(logInterceptor, metricInterceptor, traceInterceptor)) + InitDefaultClient(WithInterceptor(ChainInterceptors(logInterceptor, metricInterceptor, traceInterceptor))) } func logInterceptor(ctx context.Context, r *Request, do Do) (*Response, error) { From a708a3e69c5d0a0ed035a215461dcf4095c4b6b9 Mon Sep 17 00:00:00 2001 From: wenchy Date: Wed, 23 Jul 2025 11:35:19 +0800 Subject: [PATCH 9/9] refactor: clean code --- client.go | 87 ++++++++++++++++++++++++++++++++++++++++-------------- default.go | 11 ++++--- method.go | 49 ++++-------------------------- request.go | 15 ++++------ 4 files changed, 82 insertions(+), 80 deletions(-) diff --git a/client.go b/client.go index 6d827b2..831274e 100644 --- a/client.go +++ b/client.go @@ -10,22 +10,7 @@ import ( // ClientOption is the functional option type. type ClientOption func(*Client) -// Client is an HTTP client which wraps around [http.Client] for elegant APIs and easy use. -type Client struct { - client *http.Client - interceptor InterceptorFunc -} - -// NewClient creates a new client to serve HTTP requests. -func NewClient(options ...ClientOption) *Client { - client := newDefaultClient() - for _, setter := range options { - setter(client) - } - return client -} - -// WithTimeout specifies a time limit for requests made by this client. +// WithTimeout specifies a time limit for each client request. // // A Timeout of zero means no timeout. Default is zero. func WithTimeout(timeout time.Duration) ClientOption { @@ -34,24 +19,38 @@ func WithTimeout(timeout time.Duration) ClientOption { } } -// WithTransport specifies a round ripper for requests made by this client. +// WithTransport specifies a transport for client. func WithTransport(transport http.RoundTripper) ClientOption { return func(c *Client) { c.client.Transport = transport } } -// WithInterceptor specifies an interceptor for requests made by this client. -// Use `ChainInterceptors` to chain multiple interceptors into one. +// WithInterceptor specifies an interceptor for client. +// You can use [ChainInterceptors] to chain multiple interceptors into one. func WithInterceptor(interceptor InterceptorFunc) ClientOption { return func(c *Client) { c.interceptor = interceptor } } -// 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(method, url string, opts *Options, body []byte) (*Response, error) { +// Client is an HTTP client which wraps around [http.Client] for elegant APIs and easy use. +type Client struct { + client *http.Client + interceptor InterceptorFunc +} + +// NewClient creates a new client to serve HTTP requests. +func NewClient(setters ...ClientOption) *Client { + client := newDefaultClient() + for _, setter := range setters { + setter(client) + } + return 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) if err != nil { return nil, err @@ -83,6 +82,8 @@ func (c *Client) Do(method, url string, opts *Options, body []byte) (*Response, return c.do(ctx, r) } +// 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) { // If the returned error is nil, the Response will contain // a non-nil Body which the user is expected to close. @@ -100,3 +101,45 @@ func (c *Client) do(ctx context.Context, r *Request) (*Response, error) { return newResponse(resp, r.opts) } + +// Get sends an HTTP request with GET method. +// +// On error, any Response can be ignored. A non-nil Response with a +// non-nil error only occurs when Response.StatusCode() is not 2xx. +func (c *Client) Get(url string, options ...Option) (*Response, error) { + return c.callMethod(http.MethodGet, url, options...) +} + +// Post sends an HTTP POST request. +func (c *Client) Post(url string, options ...Option) (*Response, error) { + return c.callMethod(http.MethodPost, url, options...) +} + +// Put sends an HTTP request with PUT method. +// +// On error, any Response can be ignored. A non-nil Response with a +// non-nil error only occurs when Response.StatusCode() is not 2xx. +func (c *Client) Put(url string, options ...Option) (*Response, error) { + return c.callMethod(http.MethodPut, url, options...) +} + +// Patch sends an HTTP request with PATCH method. +// +// On error, any Response can be ignored. A non-nil Response with a +// non-nil error only occurs when Response.StatusCode() is not 2xx. +func (c *Client) Patch(url string, options ...Option) (*Response, error) { + return c.callMethod(http.MethodPatch, url, options...) +} + +// Delete sends an HTTP request with DELETE method. +// +// On error, any Response can be ignored. A non-nil Response with a +// non-nil error only occurs when Response.StatusCode() is not 2xx. +func (c *Client) Delete(url string, options ...Option) (*Response, error) { + return c.callMethod(http.MethodDelete, url, options...) +} + +func (c *Client) callMethod(method, url string, options ...Option) (*Response, error) { + opts := parseOptions(options...) + return dispatchers[opts.bodyType](c, method, url, opts) +} diff --git a/default.go b/default.go index 45368e0..9902a93 100644 --- a/default.go +++ b/default.go @@ -3,6 +3,7 @@ package requests import ( "net/http" "sync" + "time" "github.com/Wenchy/requests/internal/auth/redirector" ) @@ -16,20 +17,22 @@ func newDefaultClient() *Client { return &Client{ client: &http.Client{ CheckRedirect: redirector.RedirectPolicyFunc, + Timeout: 10 * time.Second, }, } } -func GetDefaultClient() *Client { +func getDefaultClient() *Client { once.Do(func() { defaultClient = newDefaultClient() }) return defaultClient } -func InitDefaultClient(options ...ClientOption) { - client := GetDefaultClient() - for _, setter := range options { +// InitDefaultClient initializes the default client with given options. +func InitDefaultClient(setters ...ClientOption) { + client := getDefaultClient() + for _, setter := range setters { setter(client) } } diff --git a/method.go b/method.go index 6f87d95..8e6cc48 100644 --- a/method.go +++ b/method.go @@ -1,55 +1,16 @@ package requests -import "net/http" - -// Get sends an HTTP request with GET method. -// -// On error, any Response can be ignored. A non-nil Response with a -// non-nil error only occurs when Response.StatusCode() is not 2xx. -func (c *Client) Get(url string, options ...Option) (*Response, error) { - return c.callMethod(http.MethodGet, url, options...) -} - -// Post sends an HTTP POST request. -func (c *Client) Post(url string, options ...Option) (*Response, error) { - return c.callMethod(http.MethodPost, url, options...) -} - -// Put sends an HTTP request with PUT method. -// -// On error, any Response can be ignored. A non-nil Response with a -// non-nil error only occurs when Response.StatusCode() is not 2xx. -func (c *Client) Put(url string, options ...Option) (*Response, error) { - return c.callMethod(http.MethodPut, url, options...) -} - -// Patch sends an HTTP request with PATCH method. -// -// On error, any Response can be ignored. A non-nil Response with a -// non-nil error only occurs when Response.StatusCode() is not 2xx. -func (c *Client) Patch(url string, options ...Option) (*Response, error) { - return c.callMethod(http.MethodPatch, url, options...) -} - -// Delete sends an HTTP request with DELETE method. -// -// On error, any Response can be ignored. A non-nil Response with a -// non-nil error only occurs when Response.StatusCode() is not 2xx. -func (c *Client) Delete(url string, options ...Option) (*Response, error) { - return c.callMethod(http.MethodDelete, url, options...) -} - // Get sends an HTTP request with GET method. // // On error, any Response can be ignored. A non-nil Response with a // non-nil error only occurs when Response.StatusCode() is not 2xx. func Get(url string, options ...Option) (*Response, error) { - return GetDefaultClient().Get(url, options...) + return getDefaultClient().Get(url, options...) } // Post sends an HTTP POST request. func Post(url string, options ...Option) (*Response, error) { - return GetDefaultClient().Post(url, options...) + return getDefaultClient().Post(url, options...) } // Put sends an HTTP request with PUT method. @@ -57,7 +18,7 @@ func Post(url string, options ...Option) (*Response, error) { // On error, any Response can be ignored. A non-nil Response with a // non-nil error only occurs when Response.StatusCode() is not 2xx. func Put(url string, options ...Option) (*Response, error) { - return GetDefaultClient().Put(url, options...) + return getDefaultClient().Put(url, options...) } // Patch sends an HTTP request with PATCH method. @@ -65,7 +26,7 @@ func Put(url string, options ...Option) (*Response, error) { // On error, any Response can be ignored. A non-nil Response with a // non-nil error only occurs when Response.StatusCode() is not 2xx. func Patch(url string, options ...Option) (*Response, error) { - return GetDefaultClient().Patch(url, options...) + return getDefaultClient().Patch(url, options...) } // Delete sends an HTTP request with DELETE method. @@ -73,5 +34,5 @@ func Patch(url string, options ...Option) (*Response, error) { // On error, any Response can be ignored. A non-nil Response with a // non-nil error only occurs when Response.StatusCode() is not 2xx. func Delete(url string, options ...Option) (*Response, error) { - return GetDefaultClient().Delete(url, options...) + return getDefaultClient().Delete(url, options...) } diff --git a/request.go b/request.go index 6e10818..6ed6820 100644 --- a/request.go +++ b/request.go @@ -74,7 +74,7 @@ func request(c *Client, method, url string, opts *Options) (*Response, error) { } } opts.Body = body - return c.Do(method, url, opts, body.Bytes()) + return c.request(method, url, opts, body.Bytes()) } // requestData sends an HTTP request to the specified URL, with raw string @@ -91,7 +91,7 @@ func requestData(c *Client, method, url string, opts *Options) (*Response, error // TODO: judge content type // opts.Headers["Content-Type"] = "application/x-www-form-urlencoded" opts.Body = body - return c.Do(method, url, opts, body.Bytes()) + return c.request(method, url, opts, body.Bytes()) } // requestForm sends an HTTP request to the specified URL, with form's keys and @@ -107,7 +107,7 @@ func requestForm(c *Client, method, url string, opts *Options) (*Response, error } opts.Headers.Set("Content-Type", "application/x-www-form-urlencoded") opts.Body = body - return c.Do(method, url, opts, body.Bytes()) + return c.request(method, url, opts, body.Bytes()) } // requestJSON sends an HTTP request, and encode request body as json. @@ -125,7 +125,7 @@ func requestJSON(c *Client, method, url string, opts *Options) (*Response, error } opts.Headers.Set("Content-Type", "application/json") opts.Body = body - return c.Do(method, url, opts, body.Bytes()) + return c.request(method, url, opts, body.Bytes()) } // requestFiles sends an uploading request for multiple multipart-encoded files. @@ -149,7 +149,7 @@ func requestFiles(c *Client, method, url string, opts *Options) (*Response, erro } opts.Headers.Set("Content-Type", bodyWriter.FormDataContentType()) opts.Body = body - return c.Do(method, url, opts, body.Bytes()) + return c.request(method, url, opts, body.Bytes()) } type bodyType int @@ -171,8 +171,3 @@ var dispatchers map[bodyType]dispatcher = map[bodyType]dispatcher{ bodyTypeJSON: requestJSON, bodyTypeFiles: requestFiles, } - -func (c *Client) callMethod(method, url string, options ...Option) (*Response, error) { - opts := parseOptions(options...) - return dispatchers[opts.bodyType](c, method, url, opts) -}