Compare commits

...

16 Commits

Author SHA1 Message Date
0e6bd9e669 update parse content type 2025-05-29 23:19:12 +08:00
0b2546217c 兼容响应数据非200没有响应Body的情况 2025-05-29 23:16:17 +08:00
42b9f26a9a 适配支持从Header或者Body解析请求结果 2025-05-29 23:11:56 +08:00
5ddab034f3 修复流控逻辑判断相反的BUG 2025-05-29 22:54:44 +08:00
a56528f2ee 修复逻辑缺陷导致NPE问题 2025-05-29 22:36:35 +08:00
7b1a36a7c3 Merge pull request '升级RateLimiter接口约束' (#11) from feature/upgrade_rate_limiter into master
Reviewed-on: #11
2025-05-09 11:08:20 +08:00
21060e4913 升级RateLimiter接口约束 2025-05-09 10:58:39 +08:00
70d044e3a3 修复lock key生成的BUG 2025-05-09 09:51:10 +08:00
8ddb1f72b2 Merge pull request '支持自适应请求类型, 根据不同请求类型设置正确Body格式' (#10) from feature/support_any_request_content_type into master
Reviewed-on: #10
2025-05-08 15:13:04 +08:00
1403693fda 支持自适应请求类型, 根据不同请求类型设置正确Body格式 2025-05-08 15:12:37 +08:00
1abe6c1c29 Merge pull request 'resty由 v2 -> v3升级' (#9) from feature/upgrade_resty_v3 into master
Reviewed-on: #9
2025-05-08 11:39:03 +08:00
4767aeb504 限流器使用中间件完成 2025-05-08 11:37:34 +08:00
bfbcecb498 支持static参数的合并 2025-05-07 22:33:59 +08:00
2cb3d44ec9 清理一些无效代码 2025-05-07 22:19:44 +08:00
e02ee1fef9 修复path param处理的BUG 2025-05-07 21:59:25 +08:00
ba47891d3c resty_v2 -> resty_v3 2025-05-07 21:42:48 +08:00
18 changed files with 337 additions and 197 deletions

2
go.mod
View File

@ -8,10 +8,10 @@ require (
git.zhangdeman.cn/zhangdeman/serialize v0.0.0-20250504055908-8d68e6106ea9
git.zhangdeman.cn/zhangdeman/wrapper v0.0.0-20250321102712-1cbfbe959740
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1
github.com/go-resty/resty/v2 v2.16.5
github.com/mssola/user_agent v0.6.0
github.com/tidwall/gjson v1.18.0
go.uber.org/zap v1.27.0
resty.dev/v3 v3.0.0-beta.2
)
require (

34
go.sum
View File

@ -1,29 +1,13 @@
git.zhangdeman.cn/gateway/validate v0.0.0-20250331140536-3477e2ec106d h1:rEnbN/j9/xqDAqdjAwIxE1UxoQoZVBl5Qdj+R/zEdCQ=
git.zhangdeman.cn/gateway/validate v0.0.0-20250331140536-3477e2ec106d/go.mod h1:Bwb9ks9pUp0Em+7CDTCUfnI+o6jRxMLifSHICsWmWXU=
git.zhangdeman.cn/gateway/validate v0.0.0-20250428044112-b3b4cc8e571e h1:5zb/LEL24dk6/yEU+5RQ3PO1jbHpszDumDw2buVV9Rk=
git.zhangdeman.cn/gateway/validate v0.0.0-20250428044112-b3b4cc8e571e/go.mod h1:UuNLo2MniMXCJPr5AT+31H/3VwxyzmpZBQnr1rffMos=
git.zhangdeman.cn/gateway/validate v0.0.0-20250506091017-1d2dfd10f600 h1:o+ggli1YCXiiB/SwlPqb34DePkP6mh+KqhQ/lYhBISE=
git.zhangdeman.cn/gateway/validate v0.0.0-20250506091017-1d2dfd10f600/go.mod h1:DHrM753rWJ7Dvrm7CqWq1Q05jMhiGPNMKXYcKp11BHg=
git.zhangdeman.cn/zhangdeman/consts v0.0.0-20250328040304-7e4a6f9f148c h1:cl3gQGXQpJ8ugDs0C/hQLfcvF4lGBm5BeABLvROFDoM=
git.zhangdeman.cn/zhangdeman/consts v0.0.0-20250328040304-7e4a6f9f148c/go.mod h1:IXXaZkb7vGzGnGM5RRWrASAuwrVSNxuoe0DmeXx5g6k=
git.zhangdeman.cn/zhangdeman/consts v0.0.0-20250425024726-cc17224cb995 h1:LmPRAf0AsxRVFPibdpZR89ajlsz8hof2IvMMyTqiEq4=
git.zhangdeman.cn/zhangdeman/consts v0.0.0-20250425024726-cc17224cb995/go.mod h1:5p8CEKGBxi7qPtTXDI3HDmqKAfIm5i/aBWdrbkbdNjc=
git.zhangdeman.cn/zhangdeman/dynamic-struct v0.0.0-20250401073534-5d4788e0fd86 h1:RvEOgfkutynvRjIbSWoB7tHCjJ8wl62O3rqUb9OuUd4=
git.zhangdeman.cn/zhangdeman/dynamic-struct v0.0.0-20250401073534-5d4788e0fd86/go.mod h1:XR0ElBVxzFGVh3aE9KOSEbjZLO/VRw2vxxFJVKWCuNk=
git.zhangdeman.cn/zhangdeman/dynamic-struct v0.0.0-20250428024209-cd85d80da6e7 h1:mDt8CqHHymPaDsaXJBl13fh+cqTjf4qgUhwAHMUFI1M=
git.zhangdeman.cn/zhangdeman/dynamic-struct v0.0.0-20250428024209-cd85d80da6e7/go.mod h1:24/T5zwlvo9Cgx5tzHbBZlSpcKDZYbEtKtXB0oX4p7E=
git.zhangdeman.cn/zhangdeman/dynamic-struct v0.0.0-20250429065800-fc340b9417cf h1:xCPM3U6i62UvLo9VNvDP45Ue3dPl7ratHu1rSEJRE2k=
git.zhangdeman.cn/zhangdeman/dynamic-struct v0.0.0-20250429065800-fc340b9417cf/go.mod h1:onY+qrB+Uwfuv75JlgHlGdkirAfYcINrvCashtVoBX0=
git.zhangdeman.cn/zhangdeman/json_filter v0.0.0-20250329080213-587a9bbe9dbc h1:jqaoktpx+gLiBLGpW/X03ZZ+oIsrQIlvimLSaHQo0iA=
git.zhangdeman.cn/zhangdeman/json_filter v0.0.0-20250329080213-587a9bbe9dbc/go.mod h1:KuVC2+wQ4dXMgqy4RF+wrsDxa+FWYuc7x17Y+/2b1YE=
git.zhangdeman.cn/zhangdeman/json_filter v0.0.0-20250506090824-b0db389ca3d7 h1:fBf+lN63axb8zYwqCVbexzPm8x9W4aWxzzIJIXIxS2U=
git.zhangdeman.cn/zhangdeman/json_filter v0.0.0-20250506090824-b0db389ca3d7/go.mod h1:KuVC2+wQ4dXMgqy4RF+wrsDxa+FWYuc7x17Y+/2b1YE=
git.zhangdeman.cn/zhangdeman/op_type v0.0.0-20240122104027-4928421213c0 h1:gUDlQMuJ4xNfP2Abl1Msmpa3fASLWYkNlqDFF/6GN0Y=
git.zhangdeman.cn/zhangdeman/op_type v0.0.0-20240122104027-4928421213c0/go.mod h1:VHb9qmhaPDAQDcS6vUiDCamYjZ4R5lD1XtVsh55KsMI=
git.zhangdeman.cn/zhangdeman/serialize v0.0.0-20241223084948-de2e49144fcd h1:q7GG14qgXKB4MEXQFOe7/UYebsqMfPaSX80TcPdOosI=
git.zhangdeman.cn/zhangdeman/serialize v0.0.0-20241223084948-de2e49144fcd/go.mod h1:+D6uPSljwHywjVY5WSBY4TRVMj26TN5f5cFGEYMldjs=
git.zhangdeman.cn/zhangdeman/serialize v0.0.0-20250428041157-135850ee8a58 h1:fTkmucGaUoKocoX+ASM4AnwsAVJOtOOLUFSqA+uwVzg=
git.zhangdeman.cn/zhangdeman/serialize v0.0.0-20250428041157-135850ee8a58/go.mod h1:Ig3GZC2hJDkQp7F8Tm53GvMWLh9bdbbauow/vxGO4YA=
git.zhangdeman.cn/zhangdeman/serialize v0.0.0-20250504055908-8d68e6106ea9 h1:/GLQaFoLb+ciHOtAS2BIyPNnf4O5ME3AC5PUaJY9kfs=
git.zhangdeman.cn/zhangdeman/serialize v0.0.0-20250504055908-8d68e6106ea9/go.mod h1:ABJ655C5QenQNOzf7LjCe4sSB52CXvaWLX2Zg4uwDJY=
git.zhangdeman.cn/zhangdeman/util v0.0.0-20240618042405-6ee2c904644e h1:Q973S6CcWr1ICZhFI1STFOJ+KUImCl2BaIXm6YppBqI=
@ -40,8 +24,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1 h1:CaO/zOnF8VvUfEbhRatPcwKVWamvbYd8tQGRWacE9kU=
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod h1:+hnT3ywWDTAFrW5aE+u2Sa/wT555ZqwoCS+pk3p6ry4=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
@ -54,8 +36,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
@ -102,24 +84,14 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@ -127,3 +99,5 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8X
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
resty.dev/v3 v3.0.0-beta.2 h1:xu4mGAdbCLuc3kbk7eddWfWm4JfhwDtdapwss5nCjnQ=
resty.dev/v3 v3.0.0-beta.2/go.mod h1:OgkqiPvTDtOuV4MGZuUDhwOpkY8enjOsjjMzeOHefy4=

View File

@ -0,0 +1,15 @@
// Package abstract ...
//
// Description : abstract ...
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 2025-05-07 21:13
package abstract
import "git.zhangdeman.cn/zhangdeman/network/httpclient/define"
// RateLimiter v2 流控口约束, v3移除了, 人工补齐
type RateLimiter interface {
Allow(reqCfg *define.Request) bool
}

View File

@ -0,0 +1,16 @@
// Package abstract ...
//
// Description : abstract ...
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 2025-05-08 14:18
package abstract
import "resty.dev/v3"
// IRequestBodyWrite 请求信息写入Body的接口约束
type IRequestBodyWrite interface {
// Write 写入请求Body
Write(request *resty.Request, bodyData map[string]any) error
}

View File

@ -10,7 +10,6 @@ package httpclient
import (
"fmt"
"git.zhangdeman.cn/zhangdeman/network/httpclient/implement"
"git.zhangdeman.cn/zhangdeman/wrapper"
"net"
"strings"
"time"
@ -20,7 +19,7 @@ import (
"git.zhangdeman.cn/zhangdeman/network/httpclient/log"
"git.zhangdeman.cn/zhangdeman/network/httpclient/validate"
"git.zhangdeman.cn/zhangdeman/serialize"
"github.com/go-resty/resty/v2"
"resty.dev/v3"
)
// NewHttpClient 获取http client
@ -97,17 +96,22 @@ func NewHttpClient(reqConfig *define.Request, reqOption *RequestOption) (*HttpCl
reqConfig.Header = make(map[string]any)
}
if ua, exist := reqConfig.Header[consts.HeaderKeyUserAgent.String()]; !exist || nil == ua || fmt.Sprintf("%v", ua) == "" {
reqConfig.Header[consts.HeaderKeyUserAgent.String()] = "resty-v2@network/httpclient"
reqConfig.Header[consts.HeaderKeyUserAgent.String()] = "resty-v3@network/httpclient"
}
restyClient, restyRequest := NewRestyClient(reqConfig)
restyClient, restyRequest, err := NewRestyClient(reqConfig, reqOption)
if nil != err {
if nil != restyClient {
_ = restyClient.Close()
}
return nil, err
}
defer restyClient.Close()
hc := &HttpClient{
Client: restyClient,
request: restyRequest,
reqOption: reqOption,
reqCfg: reqConfig,
http4xxHandler: make([]define.Http4xxHandler, 0),
http5xxHandler: make([]define.Http5xxHandler, 0),
httpBusinessErrorHandler: make([]define.HttpBusinessErrorHandler, 0),
requestFinishHandler: make([]define.RequestFinishHandler, 0),
}
hc.OnRequestFinish(func(req *define.Request, rep *define.Response) {
@ -132,40 +136,9 @@ type HttpClient struct {
request *resty.Request
reqOption *RequestOption
reqCfg *define.Request
http4xxHandler []define.Http4xxHandler
http5xxHandler []define.Http5xxHandler
httpBusinessErrorHandler []define.HttpBusinessErrorHandler
requestSendErrorHandler []define.RequestSendErrorHandler
requestFinishHandler []define.RequestFinishHandler
}
// OnResponse4xx 4xx处理逻辑
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 18:20 2024/6/1
func (hc *HttpClient) OnResponse4xx(handlerList ...define.Http4xxHandler) {
hc.http4xxHandler = append(hc.http4xxHandler, handlerList...)
}
// OnResponse5xx 5xx处理逻辑
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 18:21 2024/6/1
func (hc *HttpClient) OnResponse5xx(handlerList ...define.Http5xxHandler) {
hc.http5xxHandler = append(hc.http5xxHandler, handlerList...)
}
// OnResponseBusinessError 业务错误出路逻辑
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 18:22 2024/6/1
func (hc *HttpClient) OnResponseBusinessError(handlerList ...define.HttpBusinessErrorHandler) {
hc.httpBusinessErrorHandler = append(hc.httpBusinessErrorHandler, handlerList...)
}
// OnRequestFinish 请求完成事件
//
// Author : go_developer@163.com<白茶清欢>
@ -175,37 +148,6 @@ func (hc *HttpClient) OnRequestFinish(handlerList ...define.RequestFinishHandler
hc.requestFinishHandler = append(hc.requestFinishHandler, handlerList...)
}
// getRequestValidateMiddleware 请求验证的Middleware
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 15:40 2024/5/31
func (hc *HttpClient) getRequestValidateMiddleware() resty.RequestMiddleware {
return func(client *resty.Client, request *resty.Request) error {
return nil
}
}
// getResponseValidateMiddleware 获取响应数据验证的middleware
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 15:42 2024/5/31
func (hc *HttpClient) getResponseValidateMiddleware() resty.ResponseMiddleware {
return func(client *resty.Client, response *resty.Response) error {
return nil
}
}
// SetRestyClient 设置client
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 15:54 2024/5/31
func (hc *HttpClient) SetRestyClient(restyClient *resty.Client) {
hc.Client = restyClient
}
// GetRestyClient 获取 resty client
//
// Author : go_developer@163.com<白茶清欢>
@ -298,8 +240,8 @@ func (hc *HttpClient) Request() *define.Response {
"remaining_ttl": ttl,
}, hc.reqCfg)
// 配置了最小剩余时间,并且key剩余有效期小于最小剩余时间
// 预热加锁, 并发请求触发预热, 仅触发一个即可, 使用接口key + query参数做key, 按照一般约定, 写请求不会做缓存, 只有读请求会
lockKey := wrapper.String(hc.reqCfg.FullUrl + serialize.JSON.MarshalForStringIgnoreError(hc.reqCfg.Query)).Md5().Value
// 预热加锁, 并发请求触发预热, 仅触发一个即可, 使用接口缓存key + LOCK做锁的key {{CACHE_KEY}}_LOCK, 按照一般约定, 写请求不会做缓存, 只有读请求会
lockKey := hc.reqOption.CacheInstance.GetKey(hc.reqCfg) + "_LOCK"
if err := hc.reqOption.CacheInstance.Lock(lockKey); err != nil {
log.RecordWarn("接口请求命中缓存, 缓存结果有效期大于剩余时长小于配置阈值, 触发预热, 加锁失败, 未执行预热", map[string]any{
"min_ttl": cachePreHeatConfig.MinTTL,
@ -341,8 +283,6 @@ func (hc *HttpClient) Request() *define.Response {
//
// Date : 18:47 2024/10/9
func (hc *HttpClient) requestBackendApi() *define.Response {
hc.Client.OnBeforeRequest(hc.getRequestValidateMiddleware()) // 请求参数验证中间件必注册
hc.Client.OnAfterResponse(hc.getResponseValidateMiddleware()) // 响应验证中间件必注册
var (
err error
)
@ -365,7 +305,7 @@ func (hc *HttpClient) requestBackendApi() *define.Response {
response.RequestCount = i + 1
if response.RestyResponse, err = hc.request.Send(); nil != err {
errType := define.RequestFailTypeSend
if err.Error() == resty.ErrRateLimitExceeded.Error() {
if err.Error() == define.ErrRateLimitExceeded.Error() {
// 命中限流
errType = define.RequestFailTypeRateLimit
} else if netErr, ok := err.(net.Error); ok {
@ -385,11 +325,6 @@ func (hc *HttpClient) requestBackendApi() *define.Response {
"err_type": errType,
"err_msg": err.Error(),
}, hc.reqCfg)
if errType == define.RequestFailTypeRateLimit {
// 命中限流就不重试了
log.RecordDebug("请求命中限流, 忽略重试策略, 不进行重试", nil, hc.reqCfg)
break
}
if errType == define.RequestFailTypeTimeoutError && !hc.reqOption.ResponseParser.NeedRetry(hc.reqCfg, response) {
// 未配置超时重试
@ -520,7 +455,7 @@ func (hc *HttpClient) newResponse() *define.Response {
//
// Date : 16:04 2024/6/3
func (hc *HttpClient) getCacheResult() *define.Response {
if nil == hc.reqOption.CacheInstance {
if nil == hc.reqOption || nil == hc.reqOption.CacheInstance {
log.RecordDebug("接口请求前缓存检测, 未设置缓存实例", map[string]any{}, hc.reqCfg)
return nil
}

View File

@ -16,3 +16,8 @@ var (
ErrMethodIsEmpty = errors.New("METHOD_IS_EMPTY") // 请求 方法为空
ErrMethodIsNotSupport = errors.New("METHOD_IS_NOT_SUPPORT") // 请求 method不支持
)
var (
// ErrRateLimitExceeded v2版本中命中流控的错误信息
ErrRateLimitExceeded = errors.New("rate limit exceeded")
)

View File

@ -7,34 +7,6 @@
// Date : 2024-05-31 14:51
package define
// Http4xxHandler 4xx handler
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 14:54 2024/5/31
type Http4xxHandler func(req *Request, rep *Response)
// Http5xxHandler 5xx handler
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 14:55 2024/5/31
type Http5xxHandler func(req *Request, rep *Response)
// HttpBusinessErrorHandler 接口请求业务错误
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 18:04 2024/6/1
type HttpBusinessErrorHandler func(req *Request, rep *Response)
// RequestSendErrorHandler 请求发送失败的处理逻辑
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 18:23 2024/6/1
type RequestSendErrorHandler func(req *Request)
// RequestFinishHandler 请求最终完成事件, 不区分成功 OR 失败
//
// Author : go_developer@163.com<白茶清欢>

View File

@ -9,7 +9,6 @@ package define
import (
"context"
"github.com/go-resty/resty/v2"
"go.uber.org/zap"
)
@ -31,6 +30,7 @@ type Request struct {
Method string `json:"method"` // 请求方法
DataField string `json:"data_field"` // 数据字段
CodeField string `json:"code_field"` // 业务状态码字段
CodeLocation string `json:"code_location"` // 业务状态码位置
MessageField string `json:"message_field"` // code描述字段
DataReceiver any `json:"-"` // 响应data部分数据解析
SuccessHttpCodeList []int `json:"success_http_code_list"` // 哪些http状态码视为成功, 不配置, 默认2xx
@ -39,7 +39,6 @@ type Request struct {
ReadTimeout int64 `json:"read_timeout"` // 读取超时时间
RetryRule *RequestRetryRule `json:"retry_rule"` // 重试规则
Logger *zap.Logger `json:"-"` // 日志记录器
RateLimiter resty.RateLimiter `json:"-"` // 流控实例
}
// RequestRetryRule 重试规则

View File

@ -8,7 +8,7 @@
package define
import (
"github.com/go-resty/resty/v2"
"resty.dev/v3"
)
// Response 响应的数据结构定义

View File

@ -0,0 +1,55 @@
// Package request ...
//
// Description : request ...
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 2025-05-08 14:21
package request
import (
"errors"
"git.zhangdeman.cn/zhangdeman/network/httpclient/abstract"
"resty.dev/v3"
"strings"
)
var (
// WriteBodyInstanceTable 请求类型 => 请求类型写入的实现
WriteBodyInstanceTable = map[string]abstract.IRequestBodyWrite{
"json": &WriteJson{},
"xml": nil,
"x-www-form-urlencoded": &WriteForm{},
}
)
func SetWriteBodyInstance(cType string, instance abstract.IRequestBodyWrite) {
WriteBodyInstanceTable[cType] = instance
}
func DeleteWriteBodyInstance(cType string) {
delete(WriteBodyInstanceTable, cType)
}
// WriteBody 数据写入body
func WriteBody(contentType string, request *resty.Request, bodyData map[string]any) error {
if nil == bodyData {
// body为nil, 无需写入, 否则即使为空map也要正常写入
return nil
}
if nil == request {
return errors.New("request is nil")
}
contentType = strings.TrimSpace(strings.Split(contentType, ";")[0])
if len(contentType) == 0 {
return errors.New("contentType is empty or invalid")
}
contentTypeArr := strings.Split(contentType, "/")
realType := contentTypeArr[len(contentTypeArr)-1]
writeInstance, ok := WriteBodyInstanceTable[realType]
if !ok || writeInstance == nil {
return errors.New(realType + " is not support, writeInstance is nil")
}
writeInstance.Write(request, bodyData)
return nil
}

View File

@ -0,0 +1,33 @@
// Package request ...
//
// Description : request ...
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 2025-05-08 14:39
package request
import (
"git.zhangdeman.cn/zhangdeman/serialize"
"github.com/tidwall/gjson"
"resty.dev/v3"
)
// WriteForm application/x-www-form-urlencoded 实现
type WriteForm struct {
}
func (w *WriteForm) Write(request *resty.Request, bodyData map[string]any) error {
if len(bodyData) == 0 {
return nil
}
bodyStr := serialize.JSON.MarshalForStringIgnoreError(bodyData)
formatBodyData := map[string]string{}
jsonObj := gjson.Parse(bodyStr)
jsonObj.ForEach(func(key, value gjson.Result) bool {
formatBodyData[key.String()] = value.String()
return true
})
request.SetFormData(formatBodyData)
return nil
}

View File

@ -0,0 +1,21 @@
// Package request ...
//
// Description : request ...
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 2025-05-08 14:21
package request
import (
"resty.dev/v3"
)
// WriteJson application/json写入
type WriteJson struct {
}
func (w *WriteJson) Write(request *resty.Request, bodyData map[string]any) error {
request.SetBody(bodyData)
return nil
}

View File

@ -0,0 +1,46 @@
// Package request ...
//
// Description : request ...
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 2025-05-08 14:21
package request
import (
"bytes"
"errors"
"git.zhangdeman.cn/zhangdeman/serialize"
"resty.dev/v3"
"git.zhangdeman.cn/zhangdeman/dynamic-struct/wrapper"
)
// WriteXml application/xml body写入
type WriteXml struct{}
func (w *WriteXml) Write(request *resty.Request, bodyData map[string]any) error {
if nil == bodyData {
return nil
}
var (
err error
bodyBytes []byte
xmlBodyByte []byte
)
if bodyBytes, err = serialize.JSON.MarshalForByte(bodyData); nil != err {
return err
}
instance, err := wrapper.NewJson(string(bodyBytes), &wrapper.Option{XmlName: "RequestBody"})
if nil != err {
return err
}
if nil == instance {
return errors.New("xml generate instance is nil")
}
if xmlBodyByte, err = instance.Marshal("xml"); nil != err {
return err
}
request.SetBody(bytes.NewReader(xmlBodyByte))
return nil
}

View File

@ -14,6 +14,7 @@ import (
"git.zhangdeman.cn/zhangdeman/network/httpclient/define"
"git.zhangdeman.cn/zhangdeman/serialize"
"github.com/tidwall/gjson"
"net/http"
"strings"
)
@ -67,6 +68,9 @@ func (r *Response) fillResponseBody(reqCfg *define.Request, response *define.Res
responseContentType := response.RestyResponse.Header().Get(consts.HeaderKeyContentType.String())
if responseContentType == "" {
// 返回数据未说明 Content-Type
if response.RestyResponse.StatusCode() != http.StatusOK {
return nil
}
return errors.New("response content type is empty")
}
typeArr := strings.Split(strings.Split(responseContentType, ";")[0], "/")
@ -87,15 +91,27 @@ func (r *Response) fillResponseBody(reqCfg *define.Request, response *define.Res
jsonByte []byte
)
if err = parser.Unmarshal(response.RestyResponse.Body(), &res); nil != err {
if err = parser.Unmarshal(response.RestyResponse.Bytes(), &res); nil != err {
return errors.New("response parse body error :" + err.Error())
}
if jsonByte, err = parser.MarshalForByte(res); nil != err {
return errors.New("response body Marshal error :" + err.Error())
}
response.Data = string(jsonByte)
if strings.ToLower(reqCfg.CodeLocation) == "header" {
if reqCfg.CodeField == "code" {
response.Code = fmt.Sprintf("%v", response.HttpCode)
response.Message = response.RestyResponse.Status()
} else {
response.Code = response.RestyResponse.Header().Get(reqCfg.CodeField)
response.Message = response.RestyResponse.Header().Get(reqCfg.MessageField)
}
} else {
// 统一认为Body
response.Code = gjson.Get(response.Data, reqCfg.CodeField).String()
response.Message = gjson.Get(response.Data, reqCfg.MessageField).String()
}
businessData := gjson.Get(response.Data, reqCfg.DataField)
if businessData.Value() == nil {
// data为空指针, 归一化成空对象

View File

@ -39,7 +39,9 @@ type RequestConfigGroupItem struct {
FailBehavior *RequestConfigGroupItemFailBehavior `json:"fail_behavior"` // 失败的行为, 不配置, 默认失败break
FinalFailureAllow bool `json:"final_failure_allow"` // 已经确定当前请求是最终失败了,当前请求是否允许执行
CacheInstance abstract.ICache `json:"-"` // 数据缓存实例
RateLimiter abstract.RateLimiter `json:"-"` // 流控实例
ResponseParser abstract.IResponse `json:"-"` // 响应数据解析
RequestBodyWrite abstract.IRequestBodyWrite `json:"-"` // 请求Body数据写入的实现
Condition any `json:"condition"` // TODO: 请求条件, 特定条件下不执行当前请求
}

View File

@ -149,6 +149,7 @@ func (c *client) doRequest(apiList []*RequestConfigGroupItem) bool {
apiCfg.RequestCfg.Query = param[strings.ToLower(consts.RequestDataLocationQuery.String())] // query
if httpClient, err = httpclient.NewHttpClient(apiCfg.RequestCfg, &httpclient.RequestOption{
CacheInstance: apiCfg.CacheInstance,
RateLimiter: apiCfg.RateLimiter,
ResponseParser: apiCfg.ResponseParser,
}); nil != err {
// 此处获取客户端实例即发生异常, 忽略一切配置, 直接作为全局失败, 后续也不请求了

View File

@ -14,5 +14,7 @@ import (
// RequestOption 请求一些选项
type RequestOption struct {
CacheInstance abstract.ICache `json:"-"` // 数据结果缓存实例
RateLimiter abstract.RateLimiter `json:"-"` // 流控实例
RequestBodyWrite abstract.IRequestBodyWrite `json:"-"` // 将请求Body写入请求实例
ResponseParser abstract.IResponse `json:"-"` // 返回结果解析, 不配置使用内置实现
}

View File

@ -8,38 +8,96 @@
package httpclient
import (
"encoding/json"
"errors"
"fmt"
"git.zhangdeman.cn/zhangdeman/consts"
"git.zhangdeman.cn/zhangdeman/network/httpclient/define"
"git.zhangdeman.cn/zhangdeman/serialize"
requestBoodyWriter "git.zhangdeman.cn/zhangdeman/network/httpclient/implement/request"
"git.zhangdeman.cn/zhangdeman/wrapper"
"github.com/go-resty/resty/v2"
"github.com/tidwall/gjson"
"net/http"
"net/textproto"
"resty.dev/v3"
"strings"
)
// 初始化请求配置, 包括:
// 1. 初始化空指针
// 2. 合并static参数
// 3. 使用 textproto.CanonicalMIMEHeaderKey 统一格式化header key
func initRequestConfig(reqConfig *define.Request) {
if nil == reqConfig.Header {
reqConfig.Header = map[string]any{}
}
if nil == reqConfig.Body {
reqConfig.Body = map[string]any{}
}
if nil == reqConfig.Cookie {
reqConfig.Cookie = map[string]any{}
}
if reqConfig.Query == nil {
reqConfig.Query = map[string]any{}
}
if reqConfig.PathParam == nil {
reqConfig.PathParam = map[string]string{}
}
// 合并静态参数, 传入参数与静态参数存在相同参数, 以静态参数为准
for loc, paramTable := range reqConfig.Static {
switch strings.ToUpper(loc) {
case consts.RequestDataLocationBody.String():
for name, val := range paramTable {
reqConfig.Body[name] = val
}
case consts.RequestDataLocationQuery.String():
for name, val := range paramTable {
reqConfig.Query[name] = val
}
case consts.RequestDataLocationCookie.String():
for name, val := range paramTable {
reqConfig.Cookie[name] = val
}
case consts.RequestDataLocationHeader.String():
for name, val := range paramTable {
reqConfig.Header[name] = val
}
case consts.RequestDataLocationUriPath.String():
for name, val := range paramTable {
reqConfig.PathParam[name] = fmt.Sprintf("%v", val)
}
}
}
formatHeader(reqConfig)
}
// NewRestyClient 获取resty client
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 15:00 2024/5/31
func NewRestyClient(reqConfig *define.Request) (*resty.Client, *resty.Request) {
func NewRestyClient(reqConfig *define.Request, reqOption *RequestOption) (*resty.Client, *resty.Request, error) {
client := resty.New()
request := client.R()
if nil == reqConfig {
return client, request
return nil, nil, errors.New("request config is nil")
}
if nil != reqConfig.RateLimiter {
// 设置流控实例
client.SetRateLimiter(reqConfig.RateLimiter)
// 限流处理, 增加限流中间件
client.AddRequestMiddleware(func(client *resty.Client, request *resty.Request) error {
if nil == reqOption || nil == reqOption.RateLimiter {
// 未配置流控
return nil
}
formatHeader(reqConfig)
client.SetAllowGetMethodPayload(true) // 配置 GET 请求允许带 Body
client.SetJSONMarshaler(json.Marshal) // 序列化方法
if !reqOption.RateLimiter.Allow(reqConfig) {
// 命中流控
return define.ErrRateLimitExceeded
}
return nil
})
initRequestConfig(reqConfig) // 初始化 + 格式化配置
client.SetAllowMethodGetPayload(true) // 配置 GET 请求允许带 Body
client.SetAllowMethodDeletePayload(true) // 配置 DELETE 请求允许带 Body
client.SetJSONEscapeHTML(true) // 处理html实体字符
client.SetJSONUnmarshaler(serialize.JSON.UnmarshalWithNumber) // 反序列化方法
request.SetPathParams(reqConfig.PathParam) // 设置path中的参数
query := map[string]string{}
@ -59,12 +117,8 @@ func NewRestyClient(reqConfig *define.Request) (*resty.Client, *resty.Request) {
}
request.SetHeaders(header) // 设置header
request.URL = reqConfig.FullUrl // 请求接口
for pathParamName, pathParamValue := range reqConfig.PathParam {
if len(pathParamValue) == 0 {
continue
}
reqConfig.FullUrl = strings.ReplaceAll(reqConfig.FullUrl, "{"+pathParamName+"}", pathParamValue)
}
// 设置url参数, url path格式需要是 /user/{user_id}/detail格式, {user_id} 会自动替换
request.SetPathParams(reqConfig.PathParam)
request.Method = reqConfig.Method // 请求方法
cookieList := make([]*http.Cookie, 0)
for cookieName, cookieValue := range reqConfig.Cookie {
@ -77,8 +131,10 @@ func NewRestyClient(reqConfig *define.Request) (*resty.Client, *resty.Request) {
})
}
request.SetCookies(cookieList) // 设置cookie
setRestyBody(reqConfig, request) // 设置请求Body
return client, request
if err := setRestyBody(reqConfig, reqOption, request); nil != err {
return nil, nil, err
}
return client, request, nil
}
// setRestyBody 设置请求BODY
@ -86,25 +142,17 @@ func NewRestyClient(reqConfig *define.Request) (*resty.Client, *resty.Request) {
// Author : go_developer@163.com<白茶清欢>
//
// Date : 17:18 2024/5/31
func setRestyBody(reqConfig *define.Request, request *resty.Request) {
if nil == reqConfig.Body || len(reqConfig.Body) == 0 {
return
func setRestyBody(reqConfig *define.Request, requestOption *RequestOption, request *resty.Request) error {
if nil == reqConfig.Body {
return nil
}
if strings.Contains(strings.ToLower(reqConfig.ContentType), consts.MimeTypeJson) {
request.SetBody(reqConfig.Body)
return
if nil != requestOption && nil != requestOption.RequestBodyWrite {
// 外部传入的实现
requestOption.RequestBodyWrite.Write(request, reqConfig.Body)
return nil
}
if strings.Contains(strings.ToLower(reqConfig.ContentType), consts.MimeTypeXWWWFormUrlencoded) {
bodyStr := serialize.JSON.MarshalForStringIgnoreError(reqConfig.Body)
bodyData := map[string]string{}
jsonObj := gjson.Parse(bodyStr)
jsonObj.ForEach(func(key, value gjson.Result) bool {
bodyData[key.String()] = value.String()
return true
})
request.SetFormData(bodyData)
}
return
// 外部没传入, 使用内置实现, 内置默认实现支持: xml/json/form, 如需其他, 自行扩展
return requestBoodyWriter.WriteBody(reqConfig.ContentType, request, reqConfig.Body)
}
// formatHeader 格式化header