Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 54 additions & 16 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,39 +4,77 @@ 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)

// Client is an HTTP client which wraps around [http.Client] for elegant APIs and easy use.
type Client struct {
*http.Client
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.
//
// 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
}
}

// WithInterceptor specifies an interceptor for requests made by this client.
// Use `ChainInterceptors` to chain multiple interceptors into one.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Use doc links (see https://tip.golang.org/doc/comment#doclinks):``ChainInterceptors`` -> [ChainInterceptors]
  2. Confusing for requests made by this client. -> for client.

func WithInterceptor(interceptor InterceptorFunc) ClientOption {
return func(c *Client) {
c.interceptor = interceptor
}
}

// Do sends the HTTP request and returns after response is received.
func (c *Client) Do(ctx context.Context, r *Request) (*Response, error) {
// 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
}
if r.opts.DumpRequestOut != nil {
reqDump, err := httputil.DumpRequestOut(r.Request, true)
if err != nil {
return nil, err
}
*r.opts.DumpRequestOut = string(reqDump)
}
if ctx != nil {
r = r.WithContext(ctx)
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)
}
if env.interceptor != nil {
interceptors = append(interceptors, env.interceptor)
if c.interceptor != nil {
interceptors = append(interceptors, c.interceptor)
}
interceptor := ChainInterceptors(interceptors...)
if interceptor != nil {
Expand All @@ -48,7 +86,7 @@ func (c *Client) Do(ctx context.Context, r *Request) (*Response, error) {
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
}
Expand Down
79 changes: 79 additions & 0 deletions client_test.go
Original file line number Diff line number Diff line change
@@ -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())
}
})
}
}
35 changes: 35 additions & 0 deletions default.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package requests

import (
"net/http"
"sync"

"github.com/Wenchy/requests/internal/auth/redirector"
)

var (
once sync.Once
defaultClient *Client
)

func newDefaultClient() *Client {
return &Client{
client: &http.Client{
CheckRedirect: redirector.RedirectPolicyFunc,
},
}
}

func GetDefaultClient() *Client {
once.Do(func() {
defaultClient = newDefaultClient()
})
return defaultClient
}
Comment on lines 15 to 30
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Default client should not be public.
  2. Timeout default to 10s for common use cases.


func InitDefaultClient(options ...ClientOption) {
client := GetDefaultClient()
for _, setter := range options {
setter(client)
}
}
84 changes: 0 additions & 84 deletions env.go

This file was deleted.

38 changes: 38 additions & 0 deletions interceptor.go
Original file line number Diff line number Diff line change
@@ -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))
}
}
Loading
Loading