refactor: improve http client implement

This commit is contained in:
holooooo 2021-04-22 17:11:27 +08:00
parent 6f36998df8
commit 84946743d9
6 changed files with 133 additions and 210 deletions

View File

@ -21,70 +21,62 @@ import (
"time" "time"
) )
type ClientOption func(client *Client) error type ClientOption func(client *Client)
type BeegoHttpRequestOption func(request *BeegoHTTPRequest) error type BeegoHttpRequestOption func(request *BeegoHTTPRequest)
// WithEnableCookie will enable cookie in all subsequent request // WithEnableCookie will enable cookie in all subsequent request
func WithEnableCookie(enable bool) ClientOption { func WithEnableCookie(enable bool) ClientOption {
return func(client *Client) error { return func(client *Client) {
client.Setting.EnableCookie = enable client.Setting.EnableCookie = enable
return nil
} }
} }
// WithEnableCookie will adds UA in all subsequent request // WithEnableCookie will adds UA in all subsequent request
func WithUserAgent(userAgent string) ClientOption { func WithUserAgent(userAgent string) ClientOption {
return func(client *Client) error { return func(client *Client) {
client.Setting.UserAgent = userAgent client.Setting.UserAgent = userAgent
return nil
} }
} }
// WithTLSClientConfig will adds tls config in all subsequent request // WithTLSClientConfig will adds tls config in all subsequent request
func WithTLSClientConfig(config *tls.Config) ClientOption { func WithTLSClientConfig(config *tls.Config) ClientOption {
return func(client *Client) error { return func(client *Client) {
client.Setting.TLSClientConfig = config client.Setting.TLSClientConfig = config
return nil
} }
} }
// WithTransport will set transport field in all subsequent request // WithTransport will set transport field in all subsequent request
func WithTransport(transport http.RoundTripper) ClientOption { func WithTransport(transport http.RoundTripper) ClientOption {
return func(client *Client) error { return func(client *Client) {
client.Setting.Transport = transport client.Setting.Transport = transport
return nil
} }
} }
// WithProxy will set http proxy field in all subsequent request // WithProxy will set http proxy field in all subsequent request
func WithProxy(proxy func(*http.Request) (*url.URL, error)) ClientOption { func WithProxy(proxy func(*http.Request) (*url.URL, error)) ClientOption {
return func(client *Client) error { return func(client *Client) {
client.Setting.Proxy = proxy client.Setting.Proxy = proxy
return nil
} }
} }
// WithCheckRedirect will specifies the policy for handling redirects in all subsequent request // WithCheckRedirect will specifies the policy for handling redirects in all subsequent request
func WithCheckRedirect(redirect func(req *http.Request, via []*http.Request) error) ClientOption { func WithCheckRedirect(redirect func(req *http.Request, via []*http.Request) error) ClientOption {
return func(client *Client) error { return func(client *Client) {
client.Setting.CheckRedirect = redirect client.Setting.CheckRedirect = redirect
return nil
} }
} }
// WithHTTPSetting can replace beegoHTTPSeting // WithHTTPSetting can replace beegoHTTPSeting
func WithHTTPSetting(setting BeegoHTTPSettings) ClientOption { func WithHTTPSetting(setting BeegoHTTPSettings) ClientOption {
return func(client *Client) error { return func(client *Client) {
client.Setting = &setting client.Setting = setting
return nil
} }
} }
// WithEnableGzip will enable gzip in all subsequent request // WithEnableGzip will enable gzip in all subsequent request
func WithEnableGzip(enable bool) ClientOption { func WithEnableGzip(enable bool) ClientOption {
return func(client *Client) error { return func(client *Client) {
client.Setting.Gzip = enable client.Setting.Gzip = enable
return nil
} }
} }
@ -92,73 +84,60 @@ func WithEnableGzip(enable bool) ClientOption {
// WithTimeout sets connect time out and read-write time out for BeegoRequest. // WithTimeout sets connect time out and read-write time out for BeegoRequest.
func WithTimeout(connectTimeout, readWriteTimeout time.Duration) BeegoHttpRequestOption { func WithTimeout(connectTimeout, readWriteTimeout time.Duration) BeegoHttpRequestOption {
return func(request *BeegoHTTPRequest) error { return func(request *BeegoHTTPRequest) {
request.SetTimeout(connectTimeout, readWriteTimeout) request.SetTimeout(connectTimeout, readWriteTimeout)
return nil
} }
} }
// WithHeader adds header item string in request. // WithHeader adds header item string in request.
func WithHeader(key, value string) BeegoHttpRequestOption { func WithHeader(key, value string) BeegoHttpRequestOption {
return func(request *BeegoHTTPRequest) error { return func(request *BeegoHTTPRequest) {
request.Header(key, value) request.Header(key, value)
return nil
} }
} }
// WithCookie adds a cookie to the request. // WithCookie adds a cookie to the request.
func WithCookie(cookie *http.Cookie) BeegoHttpRequestOption { func WithCookie(cookie *http.Cookie) BeegoHttpRequestOption {
return func(request *BeegoHTTPRequest) error { return func(request *BeegoHTTPRequest) {
request.Header("Cookie", cookie.String()) request.Header("Cookie", cookie.String())
return nil
} }
} }
// Withtokenfactory adds a custom function to set Authorization // Withtokenfactory adds a custom function to set Authorization
func WithTokenFactory(tokenFactory func() (string, error)) BeegoHttpRequestOption { func WithTokenFactory(tokenFactory func() string) BeegoHttpRequestOption {
return func(request *BeegoHTTPRequest) error { return func(request *BeegoHTTPRequest) {
t, err := tokenFactory() t := tokenFactory()
if err != nil {
return err
}
request.Header("Authorization", t) request.Header("Authorization", t)
return nil
} }
} }
// WithBasicAuth adds a custom function to set basic auth // WithBasicAuth adds a custom function to set basic auth
func WithBasicAuth(basicAuth func() (string, string, error)) BeegoHttpRequestOption { func WithBasicAuth(basicAuth func() (string, string)) BeegoHttpRequestOption {
return func(request *BeegoHTTPRequest) error { return func(request *BeegoHTTPRequest) {
username, password, err := basicAuth() username, password := basicAuth()
if err != nil {
return err
}
request.SetBasicAuth(username, password) request.SetBasicAuth(username, password)
return nil
} }
} }
// WithFilters will use the filter as the invocation filters // WithFilters will use the filter as the invocation filters
func WithFilters(fcs ...FilterChain) BeegoHttpRequestOption { func WithFilters(fcs ...FilterChain) BeegoHttpRequestOption {
return func(request *BeegoHTTPRequest) error { return func(request *BeegoHTTPRequest) {
request.SetFilters(fcs...) request.SetFilters(fcs...)
return nil
} }
} }
// WithContentType adds ContentType in header // WithContentType adds ContentType in header
func WithContentType(contentType string) BeegoHttpRequestOption { func WithContentType(contentType string) BeegoHttpRequestOption {
return func(request *BeegoHTTPRequest) error { return func(request *BeegoHTTPRequest) {
request.Header("Content-Type", contentType) request.Header(contentTypeKey, contentType)
return nil
} }
} }
// WithParam adds query param in to request. // WithParam adds query param in to request.
func WithParam(key, value string) BeegoHttpRequestOption { func WithParam(key, value string) BeegoHttpRequestOption {
return func(request *BeegoHTTPRequest) error { return func(request *BeegoHTTPRequest) {
request.Param(key, value) request.Param(key, value)
return nil
} }
} }
@ -167,9 +146,8 @@ func WithParam(key, value string) BeegoHttpRequestOption {
// -1 retry indefinitely (forever) // -1 retry indefinitely (forever)
// Other numbers specify the exact retry amount // Other numbers specify the exact retry amount
func WithRetry(times int, delay time.Duration) BeegoHttpRequestOption { func WithRetry(times int, delay time.Duration) BeegoHttpRequestOption {
return func(request *BeegoHTTPRequest) error { return func(request *BeegoHTTPRequest) {
request.Retries(times) request.Retries(times)
request.RetryDelay(delay) request.RetryDelay(delay)
return nil
} }
} }

View File

@ -147,8 +147,8 @@ func TestOption_WithTokenFactory(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
client.CommonOpts = append(client.CommonOpts, client.CommonOpts = append(client.CommonOpts,
WithTokenFactory(func() (string, error) { WithTokenFactory(func() string {
return "testauth", nil return "testauth"
})) }))
var str *string var str *string
@ -172,8 +172,8 @@ func TestOption_WithBasicAuth(t *testing.T) {
var str *string var str *string
err = client.Get(&str, "/basic-auth/user/passwd", err = client.Get(&str, "/basic-auth/user/passwd",
WithBasicAuth(func() (string, string, error) { WithBasicAuth(func() (string, string) {
return "user", "passwd", nil return "user", "passwd"
})) }))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -240,7 +240,9 @@ func TestOption_WithRetry(t *testing.T) {
err = client.Get(nil, "", WithRetry(retryAmount, retryDelay)) err = client.Get(nil, "", WithRetry(retryAmount, retryDelay))
assert.NotNil(t, err) if err != nil {
t.Fatal(err)
}
endTime := time.Now().UnixNano() / int64(time.Millisecond) endTime := time.Now().UnixNano() / int64(time.Millisecond)
elapsedTime := endTime - startTime elapsedTime := endTime - startTime
delayedTime := int64(retryAmount) * retryDelay.Milliseconds() delayedTime := int64(retryAmount) * retryDelay.Milliseconds()

View File

@ -16,9 +16,6 @@ package httplib
import ( import (
"net/http" "net/http"
"strings"
"github.com/beego/beego/v2/core/berror"
) )
// Client provides an HTTP client supporting chain call // Client provides an HTTP client supporting chain call
@ -27,8 +24,8 @@ type Client struct {
Endpoint string Endpoint string
CommonOpts []BeegoHttpRequestOption CommonOpts []BeegoHttpRequestOption
Setting *BeegoHTTPSettings Setting BeegoHTTPSettings
pointer *responsePointer pointer responsePointer
} }
type responsePointer struct { type responsePointer struct {
@ -46,72 +43,43 @@ func NewClient(name string, endpoint string, opts ...ClientOption) (*Client, err
Endpoint: endpoint, Endpoint: endpoint,
} }
setting := GetDefaultSetting() setting := GetDefaultSetting()
res.Setting = &setting res.Setting = setting
for _, o := range opts { for _, o := range opts {
err := o(res) o(res)
if err != nil {
return nil, err
}
} }
return res, nil return res, nil
} }
// Response will set response to the pointer // Response will set response to the pointer
func (c *Client) Response(resp **http.Response) *Client { func (c *Client) Response(resp **http.Response) *Client {
if c.pointer == nil {
newC := *c newC := *c
newC.pointer = &responsePointer{ newC.pointer.response = resp
response: resp,
}
return &newC return &newC
} }
c.pointer.response = resp
return c
}
// StatusCode will set response StatusCode to the pointer // StatusCode will set response StatusCode to the pointer
func (c *Client) StatusCode(code **int) *Client { func (c *Client) StatusCode(code **int) *Client {
if c.pointer == nil {
newC := *c newC := *c
newC.pointer = &responsePointer{ newC.pointer.statusCode = code
statusCode: code,
}
return &newC return &newC
} }
c.pointer.statusCode = code
return c
}
// Headers will set response Headers to the pointer // Headers will set response Headers to the pointer
func (c *Client) Headers(headers **http.Header) *Client { func (c *Client) Headers(headers **http.Header) *Client {
if c.pointer == nil {
newC := *c newC := *c
newC.pointer = &responsePointer{ newC.pointer.header = headers
header: headers,
}
return &newC return &newC
} }
c.pointer.header = headers
return c
}
// HeaderValue will set response HeaderValue to the pointer // HeaderValue will set response HeaderValue to the pointer
func (c *Client) HeaderValue(key string, value **string) *Client { func (c *Client) HeaderValue(key string, value **string) *Client {
if c.pointer == nil {
newC := *c newC := *c
newC.pointer = &responsePointer{ if newC.pointer.headerValues == nil {
headerValues: map[string]**string{ newC.pointer.headerValues = make(map[string]**string)
key: value,
},
} }
newC.pointer.headerValues[key] = value
return &newC return &newC
} }
if c.pointer.headerValues == nil {
c.pointer.headerValues = map[string]**string{}
}
c.pointer.headerValues[key] = value
return c
}
// ContentType will set response ContentType to the pointer // ContentType will set response ContentType to the pointer
func (c *Client) ContentType(contentType **string) *Client { func (c *Client) ContentType(contentType **string) *Client {
@ -120,22 +88,13 @@ func (c *Client) ContentType(contentType **string) *Client {
// ContentLength will set response ContentLength to the pointer // ContentLength will set response ContentLength to the pointer
func (c *Client) ContentLength(contentLength **int64) *Client { func (c *Client) ContentLength(contentLength **int64) *Client {
if c.pointer == nil {
newC := *c newC := *c
newC.pointer = &responsePointer{ newC.pointer.contentLength = contentLength
contentLength: contentLength,
}
return &newC return &newC
} }
c.pointer.contentLength = contentLength
return c
}
// setPointers set the http response value to pointer // setPointers set the http response value to pointer
func (c *Client) setPointers(resp *http.Response) { func (c *Client) setPointers(resp *http.Response) {
if c.pointer == nil {
return
}
if c.pointer.response != nil { if c.pointer.response != nil {
*c.pointer.response = resp *c.pointer.response = resp
} }
@ -156,37 +115,13 @@ func (c *Client) setPointers(resp *http.Response) {
} }
} }
// initRequest will apply all the client setting, common option and request option func (c *Client) customReq(req *BeegoHTTPRequest, opts []BeegoHttpRequestOption) {
func (c *Client) newRequest(method, path string, opts []BeegoHttpRequestOption) (*BeegoHTTPRequest, error) { req.Setting(c.Setting)
var req *BeegoHTTPRequest opts = append(c.CommonOpts, opts...)
switch method {
case http.MethodGet:
req = Get(c.Endpoint + path)
case http.MethodPost:
req = Post(c.Endpoint + path)
case http.MethodPut:
req = Put(c.Endpoint + path)
case http.MethodDelete:
req = Delete(c.Endpoint + path)
case http.MethodHead:
req = Head(c.Endpoint + path)
}
req = req.Setting(*c.Setting)
for _, o := range c.CommonOpts {
err := o(req)
if err != nil {
return nil, err
}
}
for _, o := range opts { for _, o := range opts {
err := o(req) o(req)
if err != nil {
return nil, err
} }
} }
return req, nil
}
// handleResponse try to parse body to meaningful value // handleResponse try to parse body to meaningful value
func (c *Client) handleResponse(value interface{}, req *BeegoHTTPRequest) error { func (c *Client) handleResponse(value interface{}, req *BeegoHTTPRequest) error {
@ -196,69 +131,20 @@ func (c *Client) handleResponse(value interface{}, req *BeegoHTTPRequest) error
return err return err
} }
c.setPointers(resp) c.setPointers(resp)
return req.ResponseForValue(value)
if value == nil {
return nil
}
// handle basic type
switch v := value.(type) {
case **string:
s, err := req.String()
if err != nil {
return nil
}
*v = &s
return nil
case **[]byte:
bs, err := req.Bytes()
if err != nil {
return nil
}
*v = &bs
return nil
}
// try to parse it as content type
switch strings.Split(resp.Header.Get("Content-Type"), ";")[0] {
case "application/json":
return req.ToJSON(value)
case "text/xml", "application/xml":
return req.ToXML(value)
case "text/yaml", "application/x-yaml":
return req.ToYAML(value)
}
// try to parse it anyway
if err := req.ToJSON(value); err == nil {
return nil
}
if err := req.ToYAML(value); err == nil {
return nil
}
if err := req.ToXML(value); err == nil {
return nil
}
// TODO add new error type about can't parse body
return berror.Error(UnsupportedBodyType, "unsupported body data")
} }
// Get Send a GET request and try to give its result value // Get Send a GET request and try to give its result value
func (c *Client) Get(value interface{}, path string, opts ...BeegoHttpRequestOption) error { func (c *Client) Get(value interface{}, path string, opts ...BeegoHttpRequestOption) error {
req, err := c.newRequest(http.MethodGet, path, opts) req := Get(c.Endpoint + path)
if err != nil { c.customReq(req, opts)
return err
}
return c.handleResponse(value, req) return c.handleResponse(value, req)
} }
// Post Send a POST request and try to give its result value // Post Send a POST request and try to give its result value
func (c *Client) Post(value interface{}, path string, body interface{}, opts ...BeegoHttpRequestOption) error { func (c *Client) Post(value interface{}, path string, body interface{}, opts ...BeegoHttpRequestOption) error {
req, err := c.newRequest(http.MethodPost, path, opts) req := Post(c.Endpoint + path)
if err != nil { c.customReq(req, opts)
return err
}
if body != nil { if body != nil {
req = req.Body(body) req = req.Body(body)
} }
@ -267,10 +153,8 @@ func (c *Client) Post(value interface{}, path string, body interface{}, opts ...
// Put Send a Put request and try to give its result value // Put Send a Put request and try to give its result value
func (c *Client) Put(value interface{}, path string, body interface{}, opts ...BeegoHttpRequestOption) error { func (c *Client) Put(value interface{}, path string, body interface{}, opts ...BeegoHttpRequestOption) error {
req, err := c.newRequest(http.MethodPut, path, opts) req := Put(c.Endpoint + path)
if err != nil { c.customReq(req, opts)
return err
}
if body != nil { if body != nil {
req = req.Body(body) req = req.Body(body)
} }
@ -279,18 +163,14 @@ func (c *Client) Put(value interface{}, path string, body interface{}, opts ...B
// Delete Send a Delete request and try to give its result value // Delete Send a Delete request and try to give its result value
func (c *Client) Delete(value interface{}, path string, opts ...BeegoHttpRequestOption) error { func (c *Client) Delete(value interface{}, path string, opts ...BeegoHttpRequestOption) error {
req, err := c.newRequest(http.MethodDelete, path, opts) req := Delete(c.Endpoint + path)
if err != nil { c.customReq(req, opts)
return err
}
return c.handleResponse(value, req) return c.handleResponse(value, req)
} }
// Head Send a Head request and try to give its result value // Head Send a Head request and try to give its result value
func (c *Client) Head(value interface{}, path string, opts ...BeegoHttpRequestOption) error { func (c *Client) Head(value interface{}, path string, opts ...BeegoHttpRequestOption) error {
req, err := c.newRequest(http.MethodHead, path, opts) req := Head(c.Endpoint + path)
if err != nil { c.customReq(req, opts)
return err
}
return c.handleResponse(value, req) return c.handleResponse(value, req)
} }

View File

@ -56,6 +56,7 @@ import (
) )
const contentTypeKey = "Content-Type" const contentTypeKey = "Content-Type"
// it will be the last filter and execute request.Do // it will be the last filter and execute request.Do
var doRequestFilter = func(ctx context.Context, req *BeegoHTTPRequest) (*http.Response, error) { var doRequestFilter = func(ctx context.Context, req *BeegoHTTPRequest) (*http.Response, error) {
return req.doRequest(ctx) return req.doRequest(ctx)
@ -660,6 +661,64 @@ func (b *BeegoHTTPRequest) Response() (*http.Response, error) {
return b.getResponse() return b.getResponse()
} }
// ResponseForValue attempts to resolve the response body to value using an existing method.
// Calls Response inner.
// If value type is **string or **[]byte, the func directly passes response body into the pointer.
// Else if response header contain Content-Type, func will call ToJSON\ToXML\ToYAML.
// Finally it will try to parse body as json\yaml\xml, If all attempts fail, an error will be returned
func (b *BeegoHTTPRequest) ResponseForValue(value interface{}) error {
if value == nil {
return nil
}
// handle basic type
switch v := value.(type) {
case **string:
s, err := b.String()
if err != nil {
return nil
}
*v = &s
return nil
case **[]byte:
bs, err := b.Bytes()
if err != nil {
return nil
}
*v = &bs
return nil
}
resp, err := b.Response()
if err != nil {
return err
}
contentType := strings.Split(resp.Header.Get(contentTypeKey), ";")[0]
// try to parse it as content type
switch contentType {
case "application/json":
return b.ToJSON(value)
case "text/xml", "application/xml":
return b.ToXML(value)
case "text/yaml", "application/x-yaml", "application/x+yaml":
return b.ToYAML(value)
}
// try to parse it anyway
if err := b.ToJSON(value); err == nil {
return nil
}
if err := b.ToYAML(value); err == nil {
return nil
}
if err := b.ToXML(value); err == nil {
return nil
}
// TODO add new error type about can't parse body
return berror.Error(UnsupportedBodyType, "unsupported body data")
}
// TimeoutDialer returns functions of connection dialer with timeout settings for http.Transport Dial field. // TimeoutDialer returns functions of connection dialer with timeout settings for http.Transport Dial field.
// Deprecated // Deprecated
// we will move this at the end of 2021 // we will move this at the end of 2021

View File

@ -433,3 +433,7 @@ func TestBeegoHTTPRequest_XMLBody(t *testing.T) {
assert.Nil(t, err) assert.Nil(t, err)
assert.NotNil(t, req.req.GetBody) assert.NotNil(t, req.req.GetBody)
} }
// TODO
func TestBeegoHTTPRequest_ResponseForValue(t *testing.T) {
}

View File

@ -55,7 +55,7 @@ func SetDefaultSetting(setting BeegoHTTPSettings) {
defaultSetting = setting defaultSetting = setting
} }
// SetDefaultSetting return current default setting // GetDefaultSetting return current default setting
func GetDefaultSetting() BeegoHTTPSettings { func GetDefaultSetting() BeegoHTTPSettings {
return defaultSetting return defaultSetting
} }