Repository: cloudwego/hertz
Pull request #938 (2023-09-07 11:18:19)

feat: add config hook for host client
#### What type of PR is this?
feat

#### Check the PR title.
<!--
The description of the title will be attached in Release Notes, 
so please describe it from user-oriented, what this PR does / why we need it.
Please check your PR title with the below requirements:
-->
- [x] This PR title match the format: \<type\>(optional scope): \<description\>
- [x] The description of this PR title is user-oriented and clear enough for others to understand.
- [ ] Attach the PR updating the user documentation if the current PR requires user awareness at the usage level. [User docs repo](https://github.com/cloudwego/cloudwego.github.io)


#### (Optional) Translate the PR title into Chinese.
给 Host client 增加一个 hook 函数提供灵活配置的能力

#### (Optional) More detailed description for this PR(en: English/zh: Chinese).
<!--
Provide more detailed info for review(e.g., it's recommended to provide perf data if this is a perf type PR).
-->
en:
After the host client is initialized, before the host client takes effect, the user hopes to have the ability to modify the fields on the host client, which can be easily realized through this hook.

zh(optional):
在初始化host client之后，在host client正式生效前，业务存在类似修改host client上字段的能力，通过这个hook可以轻松实现

#### (Optional) Which issue(s) this PR fixes:
<!--
Automatically closes linked issue when PR is merged.
Eg: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
-->

#### (Optional) The PR that updates user documentation:
<!--
If the current PR requires user awareness at the usage level, please submit a PR to update user docs. [User docs repo](https://github.com/cloudwego/cloudwego.github.io)
-->

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/cloudwego/hertz/pull/938?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego) Report
Patch coverage: **`100.00%`** and project coverage change: **`+0.03%`** :tada:
> Comparison is base [(`03376c9`)](https://app.codecov.io/gh/cloudwego/hertz/commit/03376c92b0cc8aaa3016aedfb8c2954fb5ac34c5?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego) 81.95% compared to head [(`2adc039`)](https://app.codecov.io/gh/cloudwego/hertz/pull/938?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego) 81.99%.

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##           develop     #938      +/-   ##
===========================================
+ Coverage    81.95%   81.99%   +0.03%     
===========================================
  Files           97       97              
  Lines         9625     9634       +9     
===========================================
+ Hits          7888     7899      +11     
+ Misses        1263     1262       -1     
+ Partials       474      473       -1     
```


| [Files Changed](https://app.codecov.io/gh/cloudwego/hertz/pull/938?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego) | Coverage Δ | |
|---|---|---|
| [pkg/common/config/client\_option.go](https://app.codecov.io/gh/cloudwego/hertz/pull/938?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego#diff-cGtnL2NvbW1vbi9jb25maWcvY2xpZW50X29wdGlvbi5nbw==) | `100.00% <ø> (ø)` | |
| [pkg/app/client/client.go](https://app.codecov.io/gh/cloudwego/hertz/pull/938?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego#diff-cGtnL2FwcC9jbGllbnQvY2xpZW50Lmdv) | `76.66% <100.00%> (+0.66%)` | :arrow_up: |
| [pkg/app/client/option.go](https://app.codecov.io/gh/cloudwego/hertz/pull/938?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego#diff-cGtnL2FwcC9jbGllbnQvb3B0aW9uLmdv) | `96.84% <100.00%> (+0.13%)` | :arrow_up: |

... and [1 file with indirect coverage changes](https://app.codecov.io/gh/cloudwego/hertz/pull/938/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego)


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/cloudwego/hertz/pull/938?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego).


Patch:
diff --git a/cmd/hz/app/app.go b/cmd/hz/app/app.go
index 269e5ba..a6c44c0 100644
--- a/cmd/hz/app/app.go
+++ b/cmd/hz/app/app.go
@@ -321,8 +321,6 @@ func Init() *cli.App {
 				&snakeNameFlag,
 				&rmTagFlag,
 				&excludeFilesFlag,
-				&customLayout,
-				&customLayoutData,
 				&customPackage,
 				&protoPluginsFlag,
 				&thriftPluginsFlag,
diff --git a/cmd/hz/generator/layout.go b/cmd/hz/generator/layout.go
index d007fa8..ea189f5 100644
--- a/cmd/hz/generator/layout.go
+++ b/cmd/hz/generator/layout.go
@@ -124,11 +124,22 @@ func (lg *LayoutGenerator) GenerateByService(service Layout) error {
 
 	if !service.NeedGoMod {
 		gomodFile := "go.mod"
-		if _, exist := lg.tpls["go.mod"]; exist {
+		if _, exist := lg.tpls[gomodFile]; exist {
 			delete(lg.tpls, gomodFile)
 		}
 	}
 
+	if util.IsWindows() {
+		buildSh := "build.sh"
+		bootstrapSh := defaultScriptDir + sp + "bootstrap.sh"
+		if _, exist := lg.tpls[buildSh]; exist {
+			delete(lg.tpls, buildSh)
+		}
+		if _, exist := lg.tpls[bootstrapSh]; exist {
+			delete(lg.tpls, bootstrapSh)
+		}
+	}
+
 	sd, err := serviceToLayoutData(service)
 	if err != nil {
 		return err
diff --git a/cmd/hz/generator/package.go b/cmd/hz/generator/package.go
index 9eb00ad..01a6884 100644
--- a/cmd/hz/generator/package.go
+++ b/cmd/hz/generator/package.go
@@ -167,6 +167,9 @@ func (pkgGen *HttpPackageGenerator) Generate(pkg *HttpPackage) error {
 		if err := pkgGen.genClient(pkg, clientDir); err != nil {
 			return err
 		}
+		if err := pkgGen.genCustomizedFile(pkg); err != nil {
+			return err
+		}
 		return nil
 	}
 
diff --git a/cmd/hz/protobuf/ast.go b/cmd/hz/protobuf/ast.go
index 264dc4e..d607a83 100644
--- a/cmd/hz/protobuf/ast.go
+++ b/cmd/hz/protobuf/ast.go
@@ -354,7 +354,7 @@ func parseAnnotationToClient(clientMethod *generator.ClientMethod, gen *protogen
 			clientMethod.FormFileCode += fmt.Sprintf("%q: req.Get%s(),\n", val, f.GoName)
 		}
 		if !hasAnnotation && strings.EqualFold(clientMethod.HTTPMethod, "get") {
-			clientMethod.QueryParamsCode += fmt.Sprintf("%q: req.Get%s(),\n", checkSnakeName(f.GoName), f.GoName)
+			clientMethod.QueryParamsCode += fmt.Sprintf("%q: req.Get%s(),\n", checkSnakeName(string(f.Desc.Name())), f.GoName)
 		}
 	}
 	clientMethod.BodyParamsCode = meta.SetBodyParam
diff --git a/cmd/hz/thrift/ast.go b/cmd/hz/thrift/ast.go
index f634d96..8372235 100644
--- a/cmd/hz/thrift/ast.go
+++ b/cmd/hz/thrift/ast.go
@@ -312,7 +312,7 @@ func parseAnnotationToClient(clientMethod *generator.ClientMethod, p *parser.Typ
 			clientMethod.FormFileCode += fmt.Sprintf("%q: req.Get%s(),\n", fileName, field.GoName().String())
 		}
 		if !hasAnnotation && strings.EqualFold(clientMethod.HTTPMethod, "get") {
-			clientMethod.QueryParamsCode += fmt.Sprintf("%q: req.Get%s(),\n", checkSnakeName(field.GoName().String()), field.GoName().String())
+			clientMethod.QueryParamsCode += fmt.Sprintf("%q: req.Get%s(),\n", checkSnakeName(field.GetName()), field.GoName().String())
 		}
 	}
 	clientMethod.BodyParamsCode = meta.SetBodyParam
diff --git a/go.mod b/go.mod
index 78039a7..b5eeb52 100644
--- a/go.mod
+++ b/go.mod
@@ -7,7 +7,7 @@ require (
 	github.com/bytedance/gopkg v0.0.0-20220413063733-65bf48ffb3a7
 	github.com/bytedance/mockey v1.2.1
 	github.com/bytedance/sonic v1.8.1
-	github.com/cloudwego/netpoll v0.3.2
+	github.com/cloudwego/netpoll v0.4.2-0.20230807055039-52fd5fb7b00f
 	github.com/fsnotify/fsnotify v1.5.4
 	github.com/tidwall/gjson v1.13.0 // indirect
 	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
diff --git a/go.sum b/go.sum
index fe32d2b..59e21cf 100644
--- a/go.sum
+++ b/go.sum
@@ -10,8 +10,8 @@ github.com/bytedance/sonic v1.8.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZX
 github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
 github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
 github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
-github.com/cloudwego/netpoll v0.3.2 h1:/998ICrNMVBo4mlul4j7qcIeY7QnEfuCCPPwck9S3X4=
-github.com/cloudwego/netpoll v0.3.2/go.mod h1:xVefXptcyheopwNDZjDPcfU6kIjZXZ4nY550k1yH9eQ=
+github.com/cloudwego/netpoll v0.4.2-0.20230807055039-52fd5fb7b00f h1:8iWPKjHdXl4tjcSxUJTavnhRL5JPupYvxbtsAlm2Igw=
+github.com/cloudwego/netpoll v0.4.2-0.20230807055039-52fd5fb7b00f/go.mod h1:xVefXptcyheopwNDZjDPcfU6kIjZXZ4nY550k1yH9eQ=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 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=
diff --git a/pkg/app/client/client.go b/pkg/app/client/client.go
index 33f9fb5..614b5b5 100644
--- a/pkg/app/client/client.go
+++ b/pkg/app/client/client.go
@@ -513,6 +513,16 @@ func (c *Client) do(ctx context.Context, req *protocol.Request, resp *protocol.R
 			ProxyURI: proxyURI,
 			IsTLS:    isTLS,
 		})
+
+		// re-configure hook
+		if c.options.HostClientConfigHook != nil {
+			err = c.options.HostClientConfigHook(hc)
+			if err != nil {
+				c.mLock.Unlock()
+				return err
+			}
+		}
+
 		m[h] = hc
 		if len(m) == 1 {
 			startCleaner = true
diff --git a/pkg/app/client/option.go b/pkg/app/client/option.go
index 3e03c44..c65cc76 100644
--- a/pkg/app/client/option.go
+++ b/pkg/app/client/option.go
@@ -99,6 +99,13 @@ func WithResponseBodyStream(b bool) config.ClientOption {
 	}}
 }
 
+// WithHostClientConfigHook is used to set the function hook for re-configure the host client.
+func WithHostClientConfigHook(h func(hc interface{}) error) config.ClientOption {
+	return config.ClientOption{F: func(o *config.ClientOptions) {
+		o.HostClientConfigHook = h
+	}}
+}
+
 // WithDisableHeaderNamesNormalizing is used to set whether disable header names normalizing.
 func WithDisableHeaderNamesNormalizing(disable bool) config.ClientOption {
 	return config.ClientOption{F: func(o *config.ClientOptions) {
diff --git a/pkg/app/context.go b/pkg/app/context.go
index 559b84d..9a6e454 100644
--- a/pkg/app/context.go
+++ b/pkg/app/context.go
@@ -80,48 +80,72 @@ type ClientIP func(ctx *RequestContext) string
 
 type ClientIPOptions struct {
 	RemoteIPHeaders []string
-	TrustedProxies  map[string]bool
+	TrustedCIDRs    []*net.IPNet
 }
 
-var defaultClientIPOptions = ClientIPOptions{
-	RemoteIPHeaders: []string{"X-Real-IP", "X-Forwarded-For"},
-	TrustedProxies: map[string]bool{
-		"0.0.0.0": true,
+var defaultTrustedCIDRs = []*net.IPNet{
+	{ // 0.0.0.0/0 (IPv4)
+		IP:   net.IP{0x0, 0x0, 0x0, 0x0},
+		Mask: net.IPMask{0x0, 0x0, 0x0, 0x0},
+	},
+	{ // ::/0 (IPv6)
+		IP:   net.IP{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
+		Mask: net.IPMask{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
 	},
 }
 
+var defaultClientIPOptions = ClientIPOptions{
+	RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
+	TrustedCIDRs:    defaultTrustedCIDRs,
+}
+
 // ClientIPWithOption used to generate custom ClientIP function and set by engine.SetClientIPFunc
 func ClientIPWithOption(opts ClientIPOptions) ClientIP {
 	return func(ctx *RequestContext) string {
 		RemoteIPHeaders := opts.RemoteIPHeaders
-		TrustedProxies := opts.TrustedProxies
+		TrustedCIDRs := opts.TrustedCIDRs
 
-		remoteIP, _, err := net.SplitHostPort(strings.TrimSpace(ctx.RemoteAddr().String()))
+		remoteIPStr, _, err := net.SplitHostPort(strings.TrimSpace(ctx.RemoteAddr().String()))
 		if err != nil {
 			return ""
 		}
-		trusted := isTrustedProxy(TrustedProxies, remoteIP)
+
+		remoteIP := net.ParseIP(remoteIPStr)
+		if remoteIP == nil {
+			return ""
+		}
+
+		trusted := isTrustedProxy(TrustedCIDRs, remoteIP)
 
 		if trusted {
 			for _, headerName := range RemoteIPHeaders {
-				ip, valid := validateHeader(TrustedProxies, ctx.Request.Header.Get(headerName))
+				ip, valid := validateHeader(TrustedCIDRs, ctx.Request.Header.Get(headerName))
 				if valid {
 					return ip
 				}
 			}
 		}
 
-		return remoteIP
+		return remoteIPStr
 	}
 }
 
-// isTrustedProxy will check whether the IP address is included in the trusted list according to TrustedProxies
-func isTrustedProxy(trustedProxies map[string]bool, remoteIP string) bool {
-	return trustedProxies[remoteIP]
+// isTrustedProxy will check whether the IP address is included in the trusted list according to trustedCIDRs
+func isTrustedProxy(trustedCIDRs []*net.IPNet, remoteIP net.IP) bool {
+	if trustedCIDRs == nil {
+		return false
+	}
+
+	for _, cidr := range trustedCIDRs {
+		if cidr.Contains(remoteIP) {
+			return true
+		}
+	}
+	return false
 }
 
 // validateHeader will parse X-Real-IP and X-Forwarded-For header and return the Initial client IP address or an untrusted IP address
-func validateHeader(trustedProxies map[string]bool, header string) (clientIP string, valid bool) {
+func validateHeader(trustedCIDRs []*net.IPNet, header string) (clientIP string, valid bool) {
 	if header == "" {
 		return "", false
 	}
@@ -135,7 +159,7 @@ func validateHeader(trustedProxies map[string]bool, header string) (clientIP str
 
 		// X-Forwarded-For is appended by proxy
 		// Check IPs in reverse order and stop when find untrusted proxy
-		if (i == 0) || (!isTrustedProxy(trustedProxies, ipStr)) {
+		if (i == 0) || (!isTrustedProxy(trustedCIDRs, ip)) {
 			return ipStr, true
 		}
 	}
diff --git a/pkg/common/config/client_option.go b/pkg/common/config/client_option.go
index 4513383..8abc15b 100644
--- a/pkg/common/config/client_option.go
+++ b/pkg/common/config/client_option.go
@@ -128,6 +128,10 @@ type ClientOptions struct {
 
 	// StateObserve execution interval
 	ObservationInterval time.Duration
+
+	// Callback hook for re-configuring host client
+	// If an error is returned, the request will be terminated.
+	HostClientConfigHook func(hc interface{}) error
 }
 
 func NewClientOptions(opts []ClientOption) *ClientOptions {
diff --git a/pkg/common/hlog/default.go b/pkg/common/hlog/default.go
index 6bc624a..bdb2a80 100644
--- a/pkg/common/hlog/default.go
+++ b/pkg/common/hlog/default.go
@@ -149,10 +149,15 @@ func (ll *defaultLogger) logf(lv Level, format *string, v ...interface{}) {
 	}
 	msg := lv.toString()
 	if format != nil {
-		msg += fmt.Sprintf(*format, v...)
+		if len(v) > 0 {
+			msg += fmt.Sprintf(*format, v...)
+		} else {
+			msg += *format
+		}
 	} else {
 		msg += fmt.Sprint(v...)
 	}
+
 	ll.stdlog.Output(ll.depth, msg)
 	if lv == LevelFatal {
 		os.Exit(1)
diff --git a/pkg/protocol/header.go b/pkg/protocol/header.go
index 3ac8801..f7533ef 100644
--- a/pkg/protocol/header.go
+++ b/pkg/protocol/header.go
@@ -1064,6 +1064,12 @@ func (h *RequestHeader) DelBytes(key []byte) {
 	h.del(h.bufKV.key)
 }
 
+// Del deletes header with the given key.
+func (h *RequestHeader) Del(key string) {
+	k := getHeaderKeyBytes(&h.bufKV, key, h.disableNormalizing)
+	h.del(k)
+}
+
 func (h *RequestHeader) SetArgBytes(key, value []byte, noValue bool) {
 	h.h = setArgBytes(h.h, key, value, noValue)
 }
diff --git a/pkg/route/engine.go b/pkg/route/engine.go
index 10cffac..480e092 100644
--- a/pkg/route/engine.go
+++ b/pkg/route/engine.go
@@ -341,8 +341,8 @@ func (engine *Engine) Run() (err error) {
 		return err
 	}
 
-	if !atomic.CompareAndSwapUint32(&engine.status, statusInitialized, statusRunning) {
-		return errAlreadyRunning
+	if err = engine.MarkAsRunning(); err != nil {
+		return err
 	}
 	defer atomic.StoreUint32(&engine.status, statusClosed)
 
@@ -1022,3 +1022,12 @@ func versionToALNP(v uint32) string {
 	}
 	return ""
 }
+
+// MarkAsRunning will mark the status of the hertz engine as "running".
+// Warning: do not call this method by yourself, unless you know what you are doing.
+func (engine *Engine) MarkAsRunning() (err error) {
+	if !atomic.CompareAndSwapUint32(&engine.status, statusInitialized, statusRunning) {
+		return errAlreadyRunning
+	}
+	return nil
+}


Test patch:
diff --git a/pkg/app/client/client_test.go b/pkg/app/client/client_test.go
index e7dcbf5..d231fac 100644
--- a/pkg/app/client/client_test.go
+++ b/pkg/app/client/client_test.go
@@ -2151,6 +2151,44 @@ func TestClientRetry(t *testing.T) {
 	}
 }
 
+func TestClientHostClientConfigHookError(t *testing.T) {
+	client, _ := NewClient(WithHostClientConfigHook(func(hc interface{}) error {
+		hct, ok := hc.(*http1.HostClient)
+		assert.True(t, ok)
+		assert.DeepEqual(t, "foo.bar:80", hct.Addr)
+		return errors.New("hook return")
+	}))
+
+	req := protocol.AcquireRequest()
+	req.SetMethod(consts.MethodGet)
+	req.SetRequestURI("http://foo.bar/")
+	resp := protocol.AcquireResponse()
+	err := client.do(nil, req, resp)
+	assert.DeepEqual(t, "hook return", err.Error())
+}
+
+func TestClientHostClientConfigHook(t *testing.T) {
+	client, _ := NewClient(WithHostClientConfigHook(func(hc interface{}) error {
+		hct, ok := hc.(*http1.HostClient)
+		assert.True(t, ok)
+		assert.DeepEqual(t, "foo.bar:80", hct.Addr)
+		hct.Addr = "FOO.BAR:443"
+		return nil
+	}))
+
+	req := protocol.AcquireRequest()
+	req.SetMethod(consts.MethodGet)
+	req.SetRequestURI("http://foo.bar/")
+	resp := protocol.AcquireResponse()
+	client.do(context.Background(), req, resp)
+	client.mLock.Lock()
+	hc := client.m["foo.bar"]
+	client.mLock.Unlock()
+	hcr, ok := hc.(*http1.HostClient)
+	assert.True(t, ok)
+	assert.DeepEqual(t, "FOO.BAR:443", hcr.Addr)
+}
+
 func TestClientDialerName(t *testing.T) {
 	client, _ := NewClient()
 	dName, err := client.GetDialerName()
diff --git a/pkg/app/context_test.go b/pkg/app/context_test.go
index 7211e39..88cd263 100644
--- a/pkg/app/context_test.go
+++ b/pkg/app/context_test.go
@@ -24,6 +24,7 @@ import (
 	"fmt"
 	"html/template"
 	"io/ioutil"
+	"net"
 	"os"
 	"reflect"
 	"strings"
@@ -802,51 +803,66 @@ func TestContextContentType(t *testing.T) {
 	assert.DeepEqual(t, consts.MIMEApplicationJSONUTF8, bytesconv.B2s(c.ContentType()))
 }
 
-func TestClientIp(t *testing.T) {
+type MockIpConn struct {
+	*mock.Conn
+	RemoteIp string
+	Port     int
+}
+
+func (c *MockIpConn) RemoteAddr() net.Addr {
+	return &net.UDPAddr{
+		IP:   net.ParseIP(c.RemoteIp),
+		Port: c.Port,
+	}
+}
+
+func newContextClientIPTest() *RequestContext {
 	c := NewContext(0)
-	c.conn = mock.NewConn("")
-	// 0.0.0.0 simulates a trusted proxy server
-	c.Request.Header.Set("X-Forwarded-For", "  126.0.0.2, 0.0.0.0 ")
-	val := c.ClientIP()
-	if val != "126.0.0.2" {
-		t.Fatalf("unexpected %v. Expecting %v", val, "126.0.0.2")
-	}
-	// no proxy server
-	c = NewContext(0)
-	c.conn = mock.NewConn("")
-	c.Request.Header.Set("X-Real-Ip", "126.0.0.1")
-	val = c.ClientIP()
-	if val != "126.0.0.1" {
-		t.Fatalf("unexpected %v. Expecting %v", val, "126.0.0.1")
-	}
-	// custom RemoteIPHeaders and TrustedProxies
+	c.conn = &MockIpConn{
+		Conn:     mock.NewConn(""),
+		RemoteIp: "127.0.0.1",
+		Port:     8080,
+	}
+	c.Request.Header.Set("X-Real-IP", " 10.10.10.10  ")
+	c.Request.Header.Set("X-Forwarded-For", "  20.20.20.20, 30.30.30.30")
+	return c
+}
+
+func TestClientIp(t *testing.T) {
+	c := newContextClientIPTest()
+	// default X-Forwarded-For and X-Real-IP behaviour
+	assert.DeepEqual(t, "20.20.20.20", c.ClientIP())
+
+	c.Request.Header.DelBytes([]byte("X-Forwarded-For"))
+	assert.DeepEqual(t, "10.10.10.10", c.ClientIP())
+
+	c.Request.Header.Set("X-Forwarded-For", "30.30.30.30  ")
+	assert.DeepEqual(t, "30.30.30.30", c.ClientIP())
+
+	// No trusted CIDRS
+	c = newContextClientIPTest()
 	opts := ClientIPOptions{
 		RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
-		TrustedProxies: map[string]bool{
-			"0.0.0.0": true,
-		},
+		TrustedCIDRs:    nil,
 	}
-	c = NewContext(0)
 	c.SetClientIPFunc(ClientIPWithOption(opts))
-	c.conn = mock.NewConn("")
-	c.Request.Header.Set("X-Forwarded-For", "  126.0.0.2, 0.0.0.0 ")
-	val = c.ClientIP()
-	if val != "126.0.0.2" {
-		t.Fatalf("unexpected %v. Expecting %v", val, "126.0.0.2")
-	}
-	// no trusted proxy server
+	assert.DeepEqual(t, "127.0.0.1", c.ClientIP())
+
+	_, cidr, _ := net.ParseCIDR("30.30.30.30/32")
 	opts = ClientIPOptions{
 		RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
-		TrustedProxies:  nil,
+		TrustedCIDRs:    []*net.IPNet{cidr},
 	}
-	c = NewContext(0)
 	c.SetClientIPFunc(ClientIPWithOption(opts))
-	c.conn = mock.NewConn("")
-	c.Request.Header.Set("X-Forwarded-For", "  126.0.0.2, 0.0.0.0 ")
-	val = c.ClientIP()
-	if val != "0.0.0.0" {
-		t.Fatalf("unexpected %v. Expecting %v", val, "0.0.0.0")
+	assert.DeepEqual(t, "127.0.0.1", c.ClientIP())
+
+	_, cidr, _ = net.ParseCIDR("127.0.0.1/32")
+	opts = ClientIPOptions{
+		RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
+		TrustedCIDRs:    []*net.IPNet{cidr},
 	}
+	c.SetClientIPFunc(ClientIPWithOption(opts))
+	assert.DeepEqual(t, "30.30.30.30", c.ClientIP())
 }
 
 func TestSetClientIPFunc(t *testing.T) {
diff --git a/pkg/common/hlog/default_test.go b/pkg/common/hlog/default_test.go
index ec8310c..36f063f 100644
--- a/pkg/common/hlog/default_test.go
+++ b/pkg/common/hlog/default_test.go
@@ -107,6 +107,16 @@ func TestCtxLogger(t *testing.T) {
 		"[Error] work failed\n", string(w.b))
 }
 
+func TestFormatLoggerWithEscapedCharacters(t *testing.T) {
+	initTestLogger()
+
+	var w byteSliceWriter
+	SetOutput(&w)
+
+	Infof("http://localhost:8080/ping?f=http://localhost:3000/hello?c=%E5%A4%A7hi%E5%93%A6%E5%95%8A%E8%AF%B4%E5%BE%97%E5%A5%BD")
+	assert.DeepEqual(t, "[Info] http://localhost:8080/ping?f=http://localhost:3000/hello?c=%E5%A4%A7hi%E5%93%A6%E5%95%8A%E8%AF%B4%E5%BE%97%E5%A5%BD\n", string(w.b))
+}
+
 func TestSetLevel(t *testing.T) {
 	setLogger := &defaultLogger{
 		stdlog: log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile|log.Lmicroseconds),
diff --git a/pkg/common/utils/ioutil_test.go b/pkg/common/utils/ioutil_test.go
index c477180..e9a573b 100644
--- a/pkg/common/utils/ioutil_test.go
+++ b/pkg/common/utils/ioutil_test.go
@@ -106,6 +106,23 @@ func TestIoutilCopyBuffer(t *testing.T) {
 	assert.DeepEqual(t, written, srcLen)
 	assert.DeepEqual(t, err, nil)
 	assert.DeepEqual(t, []byte(str), writeBuffer.Bytes())
+
+	// Test when no data is readable
+	writeBuffer.Reset()
+	emptySrc := bytes.NewBufferString("")
+	written, err = CopyBuffer(dst, emptySrc, buf)
+	assert.DeepEqual(t, written, int64(0))
+	assert.Nil(t, err)
+	assert.DeepEqual(t, []byte(""), writeBuffer.Bytes())
+
+	// Test a LimitedReader
+	writeBuffer.Reset()
+	limit := int64(5)
+	limitedSrc := io.LimitedReader{R: bytes.NewBufferString(str), N: limit}
+	written, err = CopyBuffer(dst, &limitedSrc, buf)
+	assert.DeepEqual(t, written, limit)
+	assert.Nil(t, err)
+	assert.DeepEqual(t, []byte(str[:limit]), writeBuffer.Bytes())
 }
 
 func TestIoutilCopyBufferWithIoWriter(t *testing.T) {
@@ -198,7 +215,7 @@ func TestIoutilCopyBufferWithNilBufferAndIoLimitedReader(t *testing.T) {
 
 func TestIoutilCopyZeroAlloc(t *testing.T) {
 	var writeBuffer bytes.Buffer
-	str := string("hertz is very good!!!")
+	str := "hertz is very good!!!"
 	src := bytes.NewBufferString(str)
 	dst := network.NewWriter(&writeBuffer)
 	srcLen := int64(src.Len())
@@ -207,4 +224,49 @@ func TestIoutilCopyZeroAlloc(t *testing.T) {
 	assert.DeepEqual(t, written, srcLen)
 	assert.DeepEqual(t, err, nil)
 	assert.DeepEqual(t, []byte(str), writeBuffer.Bytes())
+
+	// Test when no data is readable
+	writeBuffer.Reset()
+	emptySrc := bytes.NewBufferString("")
+	written, err = CopyZeroAlloc(dst, emptySrc)
+	assert.DeepEqual(t, written, int64(0))
+	assert.Nil(t, err)
+	assert.DeepEqual(t, []byte(""), writeBuffer.Bytes())
+}
+
+func TestIoutilCopyBufferWithEmptyBuffer(t *testing.T) {
+	var writeBuffer bytes.Buffer
+	str := "hertz is very good!!!"
+	src := bytes.NewBufferString(str)
+	dst := network.NewWriter(&writeBuffer)
+	// Use a non-empty buffer of length 0
+	emptyBuf := make([]byte, 0)
+	func() {
+		defer func() {
+			if r := recover(); r != nil {
+				assert.DeepEqual(t, "empty buffer in io.CopyBuffer", r)
+			}
+		}()
+
+		written, err := CopyBuffer(dst, src, emptyBuf)
+		assert.Nil(t, err)
+		assert.DeepEqual(t, written, int64(len(str)))
+		assert.DeepEqual(t, []byte(str), writeBuffer.Bytes())
+	}()
+}
+
+func TestIoutilCopyBufferWithLimitedReader(t *testing.T) {
+	var writeBuffer bytes.Buffer
+	str := "hertz is very good!!!"
+	src := bytes.NewBufferString(str)
+	limit := int64(5)
+	limitedSrc := io.LimitedReader{R: src, N: limit}
+	dst := network.NewWriter(&writeBuffer)
+	var buf []byte
+
+	// Test LimitedReader status
+	written, err := CopyBuffer(dst, &limitedSrc, buf)
+	assert.Nil(t, err)
+	assert.DeepEqual(t, written, limit)
+	assert.DeepEqual(t, []byte(str[:limit]), writeBuffer.Bytes())
 }
diff --git a/pkg/common/utils/network_test.go b/pkg/common/utils/network_test.go
index 232e35a..78be77d 100644
--- a/pkg/common/utils/network_test.go
+++ b/pkg/common/utils/network_test.go
@@ -41,3 +41,14 @@ func TestTLSRecordHeaderLooksLikeHTTP(t *testing.T) {
 		assert.DeepEqual(t, expectedResult, TLSRecordHeaderLooksLikeHTTP(value))
 	}
 }
+
+func TestLocalIP(t *testing.T) {
+	// Mock the localIP variable for testing purposes.
+	localIP = "192.168.0.1"
+
+	// Ensure that LocalIP() returns the expected local IP.
+	expectedIP := "192.168.0.1"
+	if got := LocalIP(); got != expectedIP {
+		assert.DeepEqual(t, got, expectedIP)
+	}
+}
diff --git a/pkg/common/utils/path_test.go b/pkg/common/utils/path_test.go
index 6ebb843..b98a8c7 100644
--- a/pkg/common/utils/path_test.go
+++ b/pkg/common/utils/path_test.go
@@ -82,6 +82,11 @@ func TestPathCleanPath(t *testing.T) {
 	expectedMultiSlashPath := "/Foo/Bar/go/src/github.com/cloudwego"
 	cleanMultiSlashPath := CleanPath(multiSlashPath)
 	assert.DeepEqual(t, expectedMultiSlashPath, cleanMultiSlashPath)
+
+	inputPath := "/Foo/Bar/go/src/github.com/cloudwego/hertz/pkg/common/utils/path_test.go/."
+	expectedPath := "/Foo/Bar/go/src/github.com/cloudwego/hertz/pkg/common/utils/path_test.go/"
+	cleanedPath := CleanPath(inputPath)
+	assert.DeepEqual(t, expectedPath, cleanedPath)
 }
 
 // The Function AddMissingPort can only add the missed port, don't consider the other error case.
diff --git a/pkg/protocol/header_test.go b/pkg/protocol/header_test.go
index 1fb0211..8d768fd 100644
--- a/pkg/protocol/header_test.go
+++ b/pkg/protocol/header_test.go
@@ -209,6 +209,7 @@ func TestRequestHeaderDel(t *testing.T) {
 	var h RequestHeader
 	h.Set("Foo-Bar", "baz")
 	h.Set("aaa", "bbb")
+	h.Set("ccc", "ddd")
 	h.Set(consts.HeaderConnection, "keep-alive")
 	h.Set(consts.HeaderContentType, "aaa")
 	h.Set(consts.HeaderServer, "aaabbb")
@@ -226,11 +227,16 @@ func TestRequestHeaderDel(t *testing.T) {
 	h.del([]byte("Host"))
 	h.del([]byte(consts.HeaderTrailer))
 	h.DelCookie("foo")
+	h.Del("ccc")
 
 	hv := h.Peek("aaa")
 	if string(hv) != "bbb" {
 		t.Fatalf("unexpected header value: %q. Expecting %q", hv, "bbb")
 	}
+	hv = h.Peek("ccc")
+	if string(hv) != "" {
+		t.Fatalf("unexpected header value: %q. Expecting %q", hv, "")
+	}
 	hv = h.Peek("Foo-Bar")
 	if len(hv) > 0 {
 		t.Fatalf("non-zero header value: %q", hv)
diff --git a/pkg/route/engine_test.go b/pkg/route/engine_test.go
index fe528b4..350da81 100644
--- a/pkg/route/engine_test.go
+++ b/pkg/route/engine_test.go
@@ -502,6 +502,14 @@ func TestRenderHtmlOfFilesWithAutoRender(t *testing.T) {
 	assert.DeepEqual(t, "text/html; charset=utf-8", rr.Header().Get("Content-Type"))
 }
 
+func TestSetEngineRun(t *testing.T) {
+	e := NewEngine(config.NewOptions(nil))
+	e.Init()
+	assert.True(t, !e.IsRunning())
+	e.MarkAsRunning()
+	assert.True(t, e.IsRunning())
+}
+
 type mockConn struct{}
 
 func (m *mockConn) SetWriteTimeout(t time.Duration) error {