From 35e1d09ce3b5aebf26ea1d2fbb585ce0b890d399 Mon Sep 17 00:00:00 2001 From: dudaodong Date: Tue, 30 Aug 2022 17:22:17 +0800 Subject: [PATCH] feat: add http client for sending http request --- netutil/http_client.go | 228 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 netutil/http_client.go diff --git a/netutil/http_client.go b/netutil/http_client.go new file mode 100644 index 0000000..030d936 --- /dev/null +++ b/netutil/http_client.go @@ -0,0 +1,228 @@ +package netutil + +import ( + "bytes" + "crypto/tls" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "reflect" + "regexp" + "strings" + "time" + + "github.com/duke-git/lancet/v2/slice" +) + +// HttpRequest struct is a composed http request +type HttpRequest struct { + RawURL string + Method string + Headers http.Header + QueryParams url.Values + FormData url.Values + Body []byte +} + +// HttpClientConfig contains some configurations for http client +type HttpClientConfig struct { + SSLEnabled bool + TLSConfig *tls.Config + Compressed bool + HandshakeTimeout time.Duration + ResponseTimeout time.Duration + Verbose bool +} + +// defaultHttpClientConfig defalut client config +var defaultHttpClientConfig = &HttpClientConfig{ + Compressed: false, + HandshakeTimeout: 20 * time.Second, + ResponseTimeout: 40 * time.Second, +} + +// HttpClient is used for sending http request +type HttpClient struct { + *http.Client + TLS *tls.Config + Request *http.Request + Config HttpClientConfig +} + +// NewHttpClient make a HttpClient instance +func NewHttpClient() *HttpClient { + client := &HttpClient{ + Client: &http.Client{ + Transport: &http.Transport{ + TLSHandshakeTimeout: defaultHttpClientConfig.HandshakeTimeout, + ResponseHeaderTimeout: defaultHttpClientConfig.ResponseTimeout, + DisableCompression: !defaultHttpClientConfig.Compressed, + }, + }, + Config: *defaultHttpClientConfig, + } + + return client +} + +// NewHttpClientWithConfig make a HttpClient instance with pass config +func NewHttpClientWithConfig(config *HttpClientConfig) *HttpClient { + if config == nil { + config = defaultHttpClientConfig + } + + client := &HttpClient{ + Client: &http.Client{ + Transport: &http.Transport{ + TLSHandshakeTimeout: config.HandshakeTimeout, + ResponseHeaderTimeout: config.ResponseTimeout, + DisableCompression: !config.Compressed, + }, + }, + Config: *config, + } + + if config.SSLEnabled { + client.TLS = config.TLSConfig + } + + return client +} + +// SendRequest send http request +func (client *HttpClient) SendRequest(request *HttpRequest) (*http.Response, error) { + err := validateRequest(request) + if err != nil { + return nil, err + } + + rawUrl := request.RawURL + + req, err := http.NewRequest(request.Method, rawUrl, bytes.NewBuffer(request.Body)) + if err != nil { + return nil, err + } + + client.setTLS(rawUrl) + client.setHeader(req, request.Headers) + client.setQueryParam(req, rawUrl, request.QueryParams) + + if request.FormData != nil { + client.setFormData(req, request.FormData) + } + + client.Request = req + + resp, err := client.Client.Do(req) + if err != nil { + return nil, err + } + + return resp, nil +} + +// DecodeResponse decode response into target object +func (client *HttpClient) DecodeResponse(resp *http.Response, target any) error { + if resp == nil { + return errors.New("invalid target param") + } + defer resp.Body.Close() + return json.NewDecoder(resp.Body).Decode(target) +} + +// setTLS set http client transport TLSClientConfig +func (client *HttpClient) setTLS(rawUrl string) { + if strings.HasPrefix(rawUrl, "https") { + if transport, ok := client.Client.Transport.(*http.Transport); ok { + transport.TLSClientConfig = client.TLS + } + } +} + +// setHeader set http rquest header +func (client *HttpClient) setHeader(req *http.Request, headers http.Header) { + if headers == nil { + headers = make(http.Header) + } + + if _, ok := headers["Accept"]; !ok { + headers["Accept"] = []string{"*/*"} + } + if _, ok := headers["Accept-Encoding"]; !ok && client.Config.Compressed { + headers["Accept-Encoding"] = []string{"deflate, gzip"} + } + + req.Header = headers +} + +// setQueryParam set http request query string param +func (client *HttpClient) setQueryParam(req *http.Request, reqUrl string, queryParam url.Values) error { + if queryParam != nil { + if !strings.Contains(reqUrl, "?") { + reqUrl = reqUrl + "?" + queryParam.Encode() + } else { + reqUrl = reqUrl + "&" + queryParam.Encode() + } + u, err := url.Parse(reqUrl) + if err != nil { + return err + } + req.URL = u + } + return nil +} + +func (client *HttpClient) setFormData(req *http.Request, values url.Values) { + formData := []byte(values.Encode()) + req.Body = ioutil.NopCloser(bytes.NewReader(formData)) + req.ContentLength = int64(len(formData)) +} + +// validateRequest check if a request has url, and valid method. +func validateRequest(req *HttpRequest) error { + if req.RawURL == "" { + return errors.New("invalid request url") + } + + // common HTTP methods + methods := []string{"GET", "POST", "PUT", "DELETE", "PATCH", + "HEAD", "CONNECT", "OPTIONS", "TRACE"} + + if !slice.Contain(methods, strings.ToUpper(req.Method)) { + return errors.New("invalid request method") + } + + return nil +} + +// StructToUrlValues convert struct to url valuse, +// only convert the field which is exported and has `json` tag +func StructToUrlValues(targetStruct any) url.Values { + rv := reflect.ValueOf(targetStruct) + rt := reflect.TypeOf(targetStruct) + + if rt.Kind() == reflect.Ptr { + rt = rt.Elem() + } + if rt.Kind() != reflect.Struct { + panic(fmt.Errorf("data type %T not support, shuld be struct or pointer to struct", targetStruct)) + } + + result := url.Values{} + + fieldNum := rt.NumField() + pattern := `^[A-Z]` + regex := regexp.MustCompile(pattern) + for i := 0; i < fieldNum; i++ { + name := rt.Field(i).Name + tag := rt.Field(i).Tag.Get("json") + if regex.MatchString(name) && tag != "" { + result.Add(tag, fmt.Sprintf("%v", rv.Field(i).Interface())) + } + } + + return result +}