github.com/wfusion/gofusion@v1.1.14/http/client.go (about)

     1  package http
     2  
     3  import (
     4  	"context"
     5  	"net"
     6  	"net/http"
     7  	"sync"
     8  	"time"
     9  
    10  	"github.com/go-resty/resty/v2"
    11  	"github.com/jarcoal/httpmock"
    12  
    13  	"github.com/wfusion/gofusion/common/utils"
    14  	"github.com/wfusion/gofusion/common/utils/inspect"
    15  	"github.com/wfusion/gofusion/common/utils/serialize/json"
    16  	"github.com/wfusion/gofusion/config"
    17  
    18  	fusCtx "github.com/wfusion/gofusion/context"
    19  )
    20  
    21  var (
    22  	Client *resty.Client
    23  
    24  	defaultClientConf = new(clientConf)
    25  )
    26  
    27  type clientOption struct {
    28  	mu sync.Mutex
    29  
    30  	appName         string
    31  	name            string
    32  	retryConditions []resty.RetryConditionFunc
    33  	retryHooks      []resty.OnRetryFunc
    34  }
    35  
    36  func CName(name string) utils.OptionFunc[clientOption] {
    37  	return func(o *clientOption) {
    38  		o.name = name
    39  	}
    40  }
    41  
    42  func RetryCondition(fn resty.RetryConditionFunc) utils.OptionFunc[clientOption] {
    43  	return func(o *clientOption) {
    44  		o.mu.Lock()
    45  		defer o.mu.Unlock()
    46  		o.retryConditions = append(o.retryConditions, fn)
    47  	}
    48  }
    49  
    50  func RetryHook(fn resty.OnRetryFunc) utils.OptionFunc[clientOption] {
    51  	return func(o *clientOption) {
    52  		o.mu.Lock()
    53  		defer o.mu.Unlock()
    54  		o.retryHooks = append(o.retryHooks, fn)
    55  	}
    56  }
    57  
    58  func New(opts ...utils.OptionExtender) *resty.Client {
    59  	opt := utils.ApplyOptions[clientOption](opts...)
    60  	opt.appName = utils.ApplyOptions[useOption](opts...).appName
    61  
    62  	cli := useClient(opts...)
    63  	if cli != nil {
    64  		return applyClientOptions(cli, opt)
    65  	}
    66  
    67  	locker.Lock()
    68  	defer locker.Unlock()
    69  
    70  	c := resty.New().
    71  		OnBeforeRequest(traceHeaderMiddleware).
    72  		SetTransport(http.DefaultTransport).
    73  		SetJSONMarshaler(json.Marshal).
    74  		SetJSONUnmarshaler(json.Unmarshal).
    75  		SetDebug(true)
    76  
    77  	cfg, ok := appClientCfgMap[opt.appName][opt.name]
    78  	if !ok {
    79  		cfg = appClientCfgMap[opt.appName][config.DefaultInstanceKey]
    80  	}
    81  
    82  	if cfg.logger != nil {
    83  		c.SetLogger(cfg.logger)
    84  	}
    85  
    86  	if cliCfg := cfg.c; cliCfg == nil || !cliCfg.Mock {
    87  		c.EnableTrace()
    88  	} else {
    89  		c.SetTimeout(utils.Must(time.ParseDuration(cliCfg.Timeout)))
    90  		c.SetRetryCount(cliCfg.RetryCount)
    91  		c.SetRetryWaitTime(utils.Must(time.ParseDuration(cliCfg.RetryWaitTime)))
    92  		c.SetRetryMaxWaitTime(utils.Must(time.ParseDuration(cliCfg.RetryMaxWaitTime)))
    93  		for _, funcName := range cliCfg.RetryConditionFuncs {
    94  			c.AddRetryCondition(*(*resty.RetryConditionFunc)(inspect.FuncOf(funcName)))
    95  		}
    96  		for _, hookName := range cliCfg.RetryHooks {
    97  			c.AddRetryHook(*(*resty.OnRetryFunc)(inspect.FuncOf(hookName)))
    98  		}
    99  
   100  		dialer := &net.Dialer{
   101  			Timeout:   utils.Must(time.ParseDuration(cliCfg.DialTimeout)),
   102  			KeepAlive: utils.Must(time.ParseDuration(cliCfg.DialKeepaliveTime)),
   103  		}
   104  
   105  		c.SetTransport(&http.Transport{
   106  			Proxy:                 http.ProxyFromEnvironment,
   107  			DialContext:           dialer.DialContext,
   108  			ForceAttemptHTTP2:     cliCfg.ForceAttemptHTTP2,
   109  			DisableCompression:    cliCfg.DisableCompression,
   110  			IdleConnTimeout:       utils.Must(time.ParseDuration(cliCfg.IdleConnTimeout)),
   111  			TLSHandshakeTimeout:   utils.Must(time.ParseDuration(cliCfg.TLSHandshakeTimeout)),
   112  			ExpectContinueTimeout: utils.Must(time.ParseDuration(cliCfg.TLSHandshakeTimeout)),
   113  			MaxIdleConns:          cliCfg.MaxIdleConns,
   114  			MaxIdleConnsPerHost:   cliCfg.MaxIdleConnsPerHost,
   115  			MaxConnsPerHost:       cliCfg.MaxConnsPerHost,
   116  		})
   117  
   118  		if cliCfg.Mock {
   119  			httpmock.ActivateNonDefault(c.GetClient())
   120  		}
   121  	}
   122  
   123  	if _, ok := appClientMap[opt.appName]; !ok {
   124  		appClientMap[opt.appName] = make(map[string]*resty.Client)
   125  	}
   126  	appClientMap[opt.appName][opt.name] = c
   127  	return applyClientOptions(c, opt)
   128  }
   129  
   130  func NewRequest(ctx context.Context, opts ...utils.OptionExtender) *resty.Request {
   131  	return New(opts...).R().SetContext(ctx)
   132  }
   133  
   134  func traceHeaderMiddleware(cli *resty.Client, req *resty.Request) (err error) {
   135  	ctx := req.Context()
   136  	if userID := fusCtx.GetUserID(ctx); utils.IsStrNotBlank(userID) {
   137  		req.SetHeader("User-ID", userID)
   138  	}
   139  	if traceID := fusCtx.GetTraceID(ctx); utils.IsStrNotBlank(traceID) {
   140  		req.SetHeader("Trace-ID", traceID)
   141  	}
   142  	return
   143  }
   144  
   145  func applyClientOptions(src *resty.Client, opt *clientOption) (dst *resty.Client) {
   146  RetryConditionLoop:
   147  	for _, cond := range opt.retryConditions {
   148  		condName := utils.GetFuncName(cond)
   149  		for _, exist := range src.RetryConditions {
   150  			if condName == utils.GetFuncName(exist) {
   151  				break RetryConditionLoop
   152  			}
   153  		}
   154  		src.AddRetryCondition(cond)
   155  	}
   156  
   157  RetryHookLoop:
   158  	for _, hook := range opt.retryHooks {
   159  		condName := utils.GetFuncName(hook)
   160  		for _, exist := range src.RetryHooks {
   161  			if condName == utils.GetFuncName(exist) {
   162  				break RetryHookLoop
   163  			}
   164  		}
   165  		src.AddRetryHook(hook)
   166  	}
   167  
   168  	return src
   169  }
   170  
   171  func useClient(opts ...utils.OptionExtender) (cli *resty.Client) {
   172  	locker.RLock()
   173  	defer locker.RUnlock()
   174  
   175  	opt := utils.ApplyOptions[clientOption](opts...)
   176  	opt.appName = utils.ApplyOptions[useOption](opts...).appName
   177  	locker.RLock()
   178  	defer locker.RUnlock()
   179  	appClients, ok := appClientMap[opt.appName]
   180  	if !ok {
   181  		return
   182  	}
   183  	return appClients[opt.name]
   184  }
   185  
   186  func init() {
   187  	_ = utils.ParseTag(defaultClientConf,
   188  		utils.ParseTagName("default"),
   189  		utils.ParseTagUnmarshalType(utils.UnmarshalTypeYaml))
   190  }