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 }