Repository: cloudwego/hertz
Pull request #1282 (2025-02-26 06:16:30)

chore: release v0.9.6


Patch:
diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml
index a9c318f..0076422 100644
--- a/.github/workflows/pr-check.yml
+++ b/.github/workflows/pr-check.yml
@@ -4,7 +4,7 @@ on: [ pull_request ]
 
 jobs:
   compliant:
-    runs-on: [ self-hosted, X64 ]
+    runs-on: [ self-hosted, Linux, X64 ]
     steps:
       - uses: actions/checkout@v3
 
@@ -14,33 +14,8 @@ jobs:
       - name: Check Spell
         uses: crate-ci/typos@master
 
-  staticcheck:
-    runs-on: [ self-hosted, X64 ]
-    steps:
-      - uses: actions/checkout@v4
-      - name: Set up Go
-        uses: actions/setup-go@v5
-        with:
-          go-version: stable
-          # For self-hosted, the cache path is shared across projects
-          # and it works well without the cache of github actions
-          # Enable it if we're going to use Github only
-          cache: false
-
-      - uses: reviewdog/action-staticcheck@v1
-        with:
-          github_token: ${{ secrets.github_token }}
-          # Change reviewdog reporter if you need [github-pr-check,github-check,github-pr-review].
-          reporter: github-pr-review
-          # Report all results.
-          filter_mode: nofilter
-          # Exit with 1 when it find at least one finding.
-          fail_on_error: true
-          # Set staticcheck flags
-          staticcheck_flags: -checks=inherit,-SA1029,-SA5008
-
   lint:
-    runs-on: [ self-hosted, X64 ]
+    runs-on: [ self-hosted, Linux, X64 ]
     steps:
       - uses: actions/checkout@v4
       - name: Set up Go
@@ -53,4 +28,5 @@ jobs:
         # https://golangci-lint.run/
         uses: golangci/golangci-lint-action@v6
         with:
-          version: latest
\ No newline at end of file
+          version: latest
+          only-new-issues: true
diff --git a/.github/workflows/release-check.yml b/.github/workflows/release-check.yml
deleted file mode 100644
index fb6cfcd..0000000
--- a/.github/workflows/release-check.yml
+++ /dev/null
@@ -1,25 +0,0 @@
-name: Release Check
-
-on:
-  pull_request:
-    branches:
-      - main
-
-jobs:
-  build:
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v3
-
-      - name: Check Source Branch
-        run: python2 -c "exit(0 if '${{ github.head_ref }}'.startswith('release') or '${{ github.head_ref }}'.startswith('hotfix') else 1)"
-
-      - name: Check Version
-        run: |
-          # get version code, runner not support grep -E here
-          SOURCE_VERSION=`grep 'Version\s*=\s*\"v[0-9]\{1,\}\.[0-9]\{1,\}\.[0-9]\{1,\}\"' *.go | awk -F '\"' '{print $(NF-1)}'`
-          git checkout main
-          MASTER_VERSION=`grep 'Version\s*=\s*\"v[0-9]\{1,\}\.[0-9]\{1,\}\.[0-9]\{1,\}\"' *.go | awk -F '\"' '{print $(NF-1)}'`
-          git checkout ${{Head.SHA}}
-          # check version update
-          python2 -c "exit(0 if list(map(int,'${SOURCE_VERSION#v}'.split('.')[:3])) > list(map(int,'${MASTER_VERSION#v}'.split('.')[:3])) else 1)"
diff --git a/.golangci.yaml b/.golangci.yaml
index 23c8d36..eee75d7 100644
--- a/.golangci.yaml
+++ b/.golangci.yaml
@@ -1,35 +1,23 @@
 # Options for analysis running.
 run:
-  # include `vendor` `third_party` `testdata` `examples` `Godeps` `builtin`
-  skip-dirs-use-default: true
-# output configuration options
+  timeout: 3m
 
-  # Timeout for analysis, e.g. 30s, 5m.
-  # Default: 1m
-  timeout: 30m
-
-output:
-  # Format: colored-line-number|line-number|json|tab|checkstyle|code-climate|junit-xml|github-actions
-  format: colored-line-number
-# All available settings of specific linters.
-# Refer to https://golangci-lint.run/usage/linters
-linters-settings:
-  govet:
-    # Disable analyzers by name.
-    # Run `go tool vet help` to see all analyzers.
-    disable:
-      - stdmethods
-linters:
+linters: # https://golangci-lint.run/usage/linters/
+  disable-all: true
   enable:
-    - gofumpt
-    - goimports
-    - gofmt
+    - gosimple
     - govet
-  disable:
-    - errcheck
-    - typecheck
-    - deadcode
-    - varcheck
+    - ineffassign
     - staticcheck
+    - unused
+    - unconvert
+    - goimports
+
+linters-settings:
+  staticcheck:
+    checks:
+      - all
+      - "-SA5008" # unknown JSON option "required"
+
 issues:
-  exclude-use-default: true
\ No newline at end of file
+  exclude-use-default: true
diff --git a/README.md b/README.md
index 4fb87b7..181f3f7 100644
--- a/README.md
+++ b/README.md
@@ -31,7 +31,7 @@ Hertz [həːts] is a high-usability, high-performance and high-extensibility Gol
   Hertz adopts a layered design, providing more interfaces and default extension implementations. Users can also extend by themselves. At the same time, thanks to the layered design of the framework, the extensibility of the framework will be much greater. At present, only stable capabilities are open-sourced to the community. More planning refers to [RoadMap](ROADMAP.md).
 - Multi-protocol support
 
-  The Hertz framework provides HTTP/1.1, HTTP/2, HTTP/3, ALPN protocol support natively. In addition, due to the layered design, Hertz even supports custom build protocol resolution logic to meet any needs of protocol layer extensions.
+  The Hertz framework provides HTTP/1.1 and ALPN protocol support natively. In addition, due to the layered design, Hertz even supports custom build protocol resolution logic to meet any needs of protocol layer extensions.
 - Network layer switching capability
 
   Hertz implements the function to switch between Netpoll and Go Net on demand. Users can choose the appropriate network library for different scenarios. And Hertz also supports the extension of network library in the form of plug-ins.
@@ -42,13 +42,11 @@ Hertz [həːts] is a high-usability, high-performance and high-extensibility Gol
 ### Basic Features
   Contains introduction and use of general middleware, context selection, data binding, data rendering, direct access, logging, error handling. [more](https://www.cloudwego.io/zh/docs/hertz/tutorials/basic-feature/)
 ### Observability
-  Contains instrumentation, logging, tracing, monitoring, OpenTelemetry integration. [more](https://www.cloudwego.io/docs/hertz/tutorials/observability/)
-### Service Governance
-  Contains service registration and discovery extensions, Sentinel integration. [more](https://www.cloudwego.io/zh/docs/hertz/tutorials/service-governance/)
+  Contains instrumentation, logging, tracing. [more](https://www.cloudwego.io/docs/hertz/tutorials/observability/)
 ### Framework Extension
-  Contains network library extensions. [more](https://www.cloudwego.io/zh/docs/hertz/tutorials/framework-exten/)
+  Contains network library extensions, protocol extensions, logger extensions, monitoring extensions. [more](https://www.cloudwego.io/zh/docs/hertz/tutorials/framework-exten/)
 ### Reference
-  Apidoc, framework configurable items list. [more](https://www.cloudwego.io/zh/docs/hertz/reference/)
+  Framework configurable items list. [more](https://www.cloudwego.io/zh/docs/hertz/reference/)
 ### FAQ
   Frequently Asked Questions. [more](https://www.cloudwego.io/zh/docs/hertz/faq/)
 ## Performance
@@ -56,9 +54,9 @@ Hertz [həːts] is a high-usability, high-performance and high-extensibility Gol
   We provide the [hertz-benchmark](https://github.com/cloudwego/hertz-benchmark) project to track and compare the performance of Hertz and other frameworks in different situations for reference.
 ## Related Projects
 - [Netpoll](https://github.com/cloudwego/netpoll): A high-performance network library. Hertz integrated by default.
-- [Hertz-contrib](https://github.com/hertz-contrib): A partial extension library of Hertz, which users can integrate into Hertz through options according to their needs.
 - [Example](https://github.com/cloudwego/hertz-examples): Use examples of Hertz.
 ## Extensions
+[Hertz-contrib](https://github.com/hertz-contrib) is a partial extension library of Hertz, which users can integrate into Hertz through options according to their needs, built and maintained by the community.
 
 | Extensions                                                                                         | Description                                                                                                                                                                    |
 |----------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
diff --git a/README_cn.md b/README_cn.md
index 812c659..47fe662 100644
--- a/README_cn.md
+++ b/README_cn.md
@@ -30,7 +30,7 @@ Hertz[həːts] 是一个 Golang 微服务 HTTP 框架，在设计之初参考了
   Hertz 采用了分层设计，提供了较多的接口以及默认的扩展实现，用户也可以自行扩展。同时得益于框架的分层设计，框架的扩展性也会大很多。目前仅将稳定的能力开源给社区，更多的规划参考 [RoadMap](ROADMAP.md)。
 - 多协议支持
 
-  Hertz 框架原生提供 HTTP/1.1、HTTP/2、HTTP/3 及 ALPN 协议支持。除此之外，由于分层设计，Hertz 甚至支持自定义构建协议解析逻辑，以满足协议层扩展的任意需求。
+  Hertz 框架原生提供 HTTP/1.1 及 ALPN 协议支持。除此之外，由于分层设计，Hertz 甚至支持自定义构建协议解析逻辑，以满足协议层扩展的任意需求。
 - 网络层切换能力
 
   Hertz 实现了 Netpoll 和 Golang 原生网络库 间按需切换能力，用户可以针对不同的场景选择合适的网络库，同时也支持以插件的方式为 Hertz 扩展网络库实现。
@@ -42,13 +42,11 @@ Hertz[həːts] 是一个 Golang 微服务 HTTP 框架，在设计之初参考了
 ### 基本特性
   包含通用中间件的介绍和使用，上下文选择，数据绑定，数据渲染，直连访问，日志，错误处理，[详见文档](https://www.cloudwego.io/zh/docs/hertz/tutorials/basic-feature/)
 ### 可观测性
-  包含日志，链路追踪，埋点，监控，OpenTelemetry 集成，[详见文档](https://www.cloudwego.io/zh/docs/hertz/tutorials/observability/)
-### 治理特性
-  包含服务注册与发现扩展，Sentinel 集成，[详见文档](https://www.cloudwego.io/zh/docs/hertz/tutorials/service-governance/)
+  包含日志，链路追踪，埋点，[详见文档](https://www.cloudwego.io/zh/docs/hertz/tutorials/observability/)
 ### 框架扩展
-  包含网络库扩展，[详见文档](https://www.cloudwego.io/zh/docs/hertz/tutorials/framework-exten/)
+  包含网络库扩展，协议扩展，日志扩展，监控扩展，服务注册与发现扩展，[详见文档](https://www.cloudwego.io/zh/docs/hertz/tutorials/framework-exten/)
 ### 参考
-  apidoc、框架可配置项一览，[详见文档](https://www.cloudwego.io/zh/docs/hertz/reference/)
+  框架可配置项一览，[详见文档](https://www.cloudwego.io/zh/docs/hertz/reference/)
 ### FAQ
   常见问题排查，[详见文档](https://www.cloudwego.io/zh/docs/hertz/faq/)
 ## 框架性能
@@ -56,9 +54,10 @@ Hertz[həːts] 是一个 Golang 微服务 HTTP 框架，在设计之初参考了
   我们提供了 [hertz-benchmark](https://github.com/cloudwego/hertz-benchmark) 项目用来长期追踪和比较 Hertz 与其他框架在不同情况下的性能数据以供参考
 ## 相关项目
 - [Netpoll](https://github.com/cloudwego/netpoll): 自研高性能网络库，Hertz 默认集成
-- [hertz-Contrib](https://github.com/hertz-contrib): Hertz 扩展仓库，提供可观测、安全、流量治理、协议、HTTP 通用能力等扩展
 - [Example](https://github.com/cloudwego/hertz-examples): Hertz 使用例子
+
 ## 相关拓展
+[hertz-Contrib](https://github.com/hertz-contrib) 是 Hertz 扩展生态所在组织，提供服务注册发现、可观测、安全、流量治理、协议、HTTP 通用能力等扩展，由社区共建与维护
 
 | 拓展                                                                                                 | 描述                                                                                                |
 |----------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------|
diff --git a/cmd/hz/generator/template.go b/cmd/hz/generator/template.go
index b339fbb..3e6927e 100644
--- a/cmd/hz/generator/template.go
+++ b/cmd/hz/generator/template.go
@@ -247,15 +247,13 @@ func (tg *TemplateGenerator) Persist() error {
 		}
 
 		err = func() error {
-			file, err := os.OpenFile(abPath, os.O_CREATE|os.O_TRUNC|os.O_RDWR, os.FileMode(0o755))
-			defer file.Close()
-			if err != nil {
-				return fmt.Errorf("open file '%s' failed, err: %v", abPath, err.Error())
+			fileMode := os.FileMode(0o644)
+			if strings.HasSuffix(abPath, ".sh") {
+				fileMode = os.FileMode(0o755)
 			}
-			if _, err = file.WriteString(data.Content); err != nil {
-				return fmt.Errorf("write file '%s' failed, err: %v", abPath, err.Error())
+			if err := os.WriteFile(abPath, []byte(data.Content), fileMode); err != nil {
+				return fmt.Errorf("write file '%s' failed, err: %v", abPath, err)
 			}
-
 			return nil
 		}()
 		if err != nil {
diff --git a/cmd/hz/thrift/resolver.go b/cmd/hz/thrift/resolver.go
index 118d0fa..c7a3c89 100644
--- a/cmd/hz/thrift/resolver.go
+++ b/cmd/hz/thrift/resolver.go
@@ -269,10 +269,12 @@ func (resolver *Resolver) ResolveIdentifier(id string) (ret ResolvedSymbol, err
 		return
 	}
 
-	sp := strings.SplitN(id, ".", 2)
-	if ref, ok := resolver.refPkgs[sp[0]]; ok {
+	idx := strings.LastIndex(id, ".")
+	depName := id[:idx]
+	typeName := id[idx+1:]
+	if ref, ok := resolver.refPkgs[depName]; ok {
 		ref.Referred = true
-		ret.Base = sp[1]
+		ret.Base = typeName
 		ret.Src = ref.Model.PackageName
 		ret.Type.Scope = ref.Model
 	} else {
@@ -334,9 +336,11 @@ func (resolver *Resolver) Get(name string) *Symbol {
 		return s
 	}
 	if strings.Contains(name, ".") {
-		sp := strings.SplitN(name, ".", 2)
-		if ref, ok := resolver.deps[sp[0]]; ok {
-			if ss, ok := ref[sp[1]]; ok {
+		idx := strings.LastIndex(name, ".")
+		depName := name[:idx]
+		typeName := name[idx+1:]
+		if ref, ok := resolver.deps[depName]; ok {
+			if ss, ok := ref[typeName]; ok {
 				return ss
 			}
 		}
diff --git a/go.mod b/go.mod
index 31972f9..48d18ca 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,7 @@ go 1.17
 require (
 	github.com/bytedance/gopkg v0.1.0
 	github.com/bytedance/mockey v1.2.12
-	github.com/bytedance/sonic v1.12.0
+	github.com/bytedance/sonic v1.12.7
 	github.com/cloudwego/netpoll v0.6.4
 	github.com/fsnotify/fsnotify v1.5.4
 	github.com/nyaruka/phonenumbers v1.0.55
@@ -17,9 +17,8 @@ require (
 )
 
 require (
-	github.com/bytedance/sonic/loader v0.2.0 // indirect
+	github.com/bytedance/sonic/loader v0.2.2 // indirect
 	github.com/cloudwego/base64x v0.1.4 // indirect
-	github.com/cloudwego/iasm v0.2.0 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/golang/protobuf v1.5.0 // indirect
 	github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 // indirect
diff --git a/go.sum b/go.sum
index fe72a68..1e0a861 100644
--- a/go.sum
+++ b/go.sum
@@ -2,14 +2,13 @@ github.com/bytedance/gopkg v0.1.0 h1:aAxB7mm1qms4Wz4sp8e1AtKDOeFLtdqvGiUe7aonRJs
 github.com/bytedance/gopkg v0.1.0/go.mod h1:FtQG3YbQG9L/91pbKSw787yBQPutC+457AvDW77fgUQ=
 github.com/bytedance/mockey v1.2.12 h1:aeszOmGw8CPX8CRx1DZ/Glzb1yXvhjDh6jdFBNZjsU4=
 github.com/bytedance/mockey v1.2.12/go.mod h1:3ZA4MQasmqC87Tw0w7Ygdy7eHIc2xgpZ8Pona5rsYIk=
-github.com/bytedance/sonic v1.12.0 h1:YGPgxF9xzaCNvd/ZKdQ28yRovhfMFZQjuk6fKBzZ3ls=
-github.com/bytedance/sonic v1.12.0/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
+github.com/bytedance/sonic v1.12.7 h1:CQU8pxOy9HToxhndH0Kx/S1qU/CuS9GnKYrGioDcU1Q=
+github.com/bytedance/sonic v1.12.7/go.mod h1:tnbal4mxOMju17EGfknm2XyYcpyCnIROYOEYuemj13I=
 github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
-github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM=
-github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
+github.com/bytedance/sonic/loader v0.2.2 h1:jxAJuN9fOot/cyz5Q6dUuMJF5OqQ6+5GfA8FjjQ0R4o=
+github.com/bytedance/sonic/loader v0.2.2/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
 github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
 github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
-github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
 github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
 github.com/cloudwego/netpoll v0.6.4 h1:z/dA4sOTUQof6zZIO4QNnLBXsDFFFEos9OOGloR6kno=
 github.com/cloudwego/netpoll v0.6.4/go.mod h1:BtM+GjKTdwKoC8IOzD08/+8eEn2gYoiNLipFca6BVXQ=
diff --git a/pkg/app/context.go b/pkg/app/context.go
index 738c123..32e6785 100644
--- a/pkg/app/context.go
+++ b/pkg/app/context.go
@@ -99,43 +99,43 @@ var defaultClientIPOptions = ClientIPOptions{
 	TrustedCIDRs:    defaultTrustedCIDRs,
 }
 
+var loopbackIP = net.ParseIP("127.0.0.1")
+
 // 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
-		TrustedCIDRs := opts.TrustedCIDRs
-
-		remoteIPStr, _, err := net.SplitHostPort(strings.TrimSpace(ctx.RemoteAddr().String()))
-		if err != nil {
-			return ""
-		}
-
-		remoteIP := net.ParseIP(remoteIPStr)
-		if remoteIP == nil {
-			return ""
+		remoteIPStr := ""
+		trustedProxy := false
+		if addr := ctx.RemoteAddr(); strings.HasPrefix(addr.Network(), "unix") {
+			// unix, unixgram, unixpacket is considered same as "127.0.0.1"
+			remoteIPStr = addr.String()
+			trustedProxy = isTrustedProxy(opts.TrustedCIDRs, loopbackIP)
+		} else {
+			h, _, err := net.SplitHostPort(strings.TrimSpace(addr.String()))
+			if err != nil {
+				return ""
+			}
+			remoteIPStr = h
+			trustedProxy = isTrustedProxy(opts.TrustedCIDRs, net.ParseIP(h))
 		}
 
-		trusted := isTrustedProxy(TrustedCIDRs, remoteIP)
-
-		if trusted {
-			for _, headerName := range RemoteIPHeaders {
-				ip, valid := validateHeader(TrustedCIDRs, ctx.Request.Header.Get(headerName))
+		if trustedProxy {
+			for _, headerName := range opts.RemoteIPHeaders {
+				ip, valid := validateHeader(opts.TrustedCIDRs, ctx.Request.Header.Get(headerName))
 				if valid {
 					return ip
 				}
 			}
 		}
-
 		return remoteIPStr
 	}
 }
 
 // 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 {
+	if trustedCIDRs == nil || remoteIP == nil {
 		return false
 	}
-
 	for _, cidr := range trustedCIDRs {
 		if cidr.Contains(remoteIP) {
 			return true
diff --git a/pkg/app/server/hertz.go b/pkg/app/server/hertz.go
index 46baef1..1115ac8 100644
--- a/pkg/app/server/hertz.go
+++ b/pkg/app/server/hertz.go
@@ -25,7 +25,6 @@ import (
 
 	"github.com/cloudwego/hertz/pkg/app/middlewares/server/recovery"
 	"github.com/cloudwego/hertz/pkg/common/config"
-	"github.com/cloudwego/hertz/pkg/common/errors"
 	"github.com/cloudwego/hertz/pkg/common/hlog"
 	"github.com/cloudwego/hertz/pkg/route"
 )
@@ -106,10 +105,7 @@ func waitSignal(errCh chan error) error {
 	select {
 	case sig := <-signals:
 		switch sig {
-		case syscall.SIGTERM:
-			// force exit
-			return errors.NewPublic(sig.String()) // nolint
-		case syscall.SIGHUP, syscall.SIGINT:
+		case syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM:
 			hlog.SystemLogger().Infof("Received signal: %s\n", sig)
 			// graceful shutdown
 			return nil
diff --git a/pkg/common/config/client_option.go b/pkg/common/config/client_option.go
index 8abc15b..ed45c57 100644
--- a/pkg/common/config/client_option.go
+++ b/pkg/common/config/client_option.go
@@ -34,6 +34,8 @@ type ConnPoolState struct {
 	WaitConnNum int
 	// HostClient Addr
 	Addr string
+	// Maximum number of connections
+	MaxConns int
 }
 
 type HostClientState interface {
diff --git a/pkg/common/json/sonic.go b/pkg/common/json/sonic.go
index dad1e63..20d6e04 100644
--- a/pkg/common/json/sonic.go
+++ b/pkg/common/json/sonic.go
@@ -13,10 +13,7 @@
 // limitations under the License.
 //
 
-//go:build (linux || windows || darwin) && amd64 && !stdjson
-// +build linux windows darwin
-// +build amd64
-// +build !stdjson
+//go:build !stdjson
 
 package json
 
diff --git a/pkg/common/json/std.go b/pkg/common/json/std.go
index 5f0f71a..9d4bfd3 100644
--- a/pkg/common/json/std.go
+++ b/pkg/common/json/std.go
@@ -13,8 +13,7 @@
 // limitations under the License.
 //
 
-//go:build stdjson || !(amd64 && (linux || windows || darwin))
-// +build stdjson !amd64 !linux,!windows,!darwin
+//go:build stdjson
 
 package json
 
diff --git a/pkg/protocol/http1/client.go b/pkg/protocol/http1/client.go
index 758d164..5bac3e9 100644
--- a/pkg/protocol/http1/client.go
+++ b/pkg/protocol/http1/client.go
@@ -194,6 +194,7 @@ func (c *HostClient) ConnPoolState() config.ConnPoolState {
 		PoolConnNum:  len(c.conns),
 		TotalConnNum: c.connsCount,
 		Addr:         c.Addr,
+		MaxConns:     c.MaxConns,
 	}
 
 	if c.connsWait != nil {
diff --git a/version.go b/version.go
index 25a5bdc..1173b73 100644
--- a/version.go
+++ b/version.go
@@ -19,5 +19,5 @@ package hertz
 // Name and Version info of this framework, used for statistics and debug
 const (
 	Name    = "Hertz"
-	Version = "v0.9.5"
+	Version = "v0.9.6"
 )


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 000f1d6..4eac500 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -7,7 +7,7 @@ jobs:
     strategy:
       matrix:
         version: ["1.17", "1.18", "1.19"]
-    runs-on: [self-hosted, X64]
+    runs-on: [self-hosted, Linux, X64]
     steps:
       - uses: actions/checkout@v4
       - name: Set up Go
@@ -24,7 +24,7 @@ jobs:
     strategy:
       matrix:
         version: ["1.20", "1.21", "1.22", "1.23"]
-    runs-on: [self-hosted, X64]
+    runs-on: [self-hosted, Linux, X64]
     steps:
       - uses: actions/checkout@v4
       - name: Set up Go
@@ -37,25 +37,20 @@ jobs:
         run: go test -race ./...
 
   ut-windows:
-    strategy:
-      matrix:
-        version: ["1.20", "1.21", "1.22", "1.23"]
-    runs-on: windows-latest
-    env: # Fixes https://github.com/actions/setup-go/issues/240
-      GOMODCACHE: 'D:\go\pkg\mod'
-      GOCACHE: 'D:\go\go-build'
+    runs-on: [self-hosted, Windows]
     steps:
       - uses: actions/checkout@v4
       - name: Set up Go
         uses: actions/setup-go@v5
         with:
-          go-version: ${{ matrix.version }}
+          go-version: stable
+          cache: false # don't use cache for self-hosted runners
 
       - name: Unit Test
         run: go test -race ./...
 
   code-cov:
-    runs-on: [self-hosted, X64]
+    runs-on: [self-hosted, Linux, X64]
     steps:
       - uses: actions/checkout@v4
       - name: Set up Go
@@ -71,7 +66,7 @@ jobs:
         run: bash <(curl -s https://codecov.io/bash)
 
   hz-test-unix:
-    runs-on: [ self-hosted, X64 ]
+    runs-on: [ self-hosted, Linux, X64 ]
     steps:
       - uses: actions/checkout@v4
       - name: Set up Go
diff --git a/cmd/hz/test_hz_unix.sh b/cmd/hz/test_hz_unix.sh
index 133f90b..b01d09e 100644
--- a/cmd/hz/test_hz_unix.sh
+++ b/cmd/hz/test_hz_unix.sh
@@ -22,6 +22,11 @@ compile_hz() {
   judge_exit "$?"
 }
 
+
+PATH_BIN=$PWD/bin
+mkdir -p $PATH_BIN
+export PATH=$PATH_BIN:$PATH
+
 install_dependent_tools() {
   # install thriftgo
   go install github.com/cloudwego/thriftgo@latest
@@ -29,8 +34,8 @@ install_dependent_tools() {
   # install protoc
   wget https://github.com/protocolbuffers/protobuf/releases/download/v3.19.4/protoc-3.19.4-linux-x86_64.zip
   unzip -d protoc-3.19.4-linux-x86_64 protoc-3.19.4-linux-x86_64.zip
-  cp protoc-3.19.4-linux-x86_64/bin/protoc /usr/local/bin/protoc
-  cp -r protoc-3.19.4-linux-x86_64/include/google /usr/local/include/google
+  cp protoc-3.19.4-linux-x86_64/bin/protoc $PATH_BIN
+  cp -r protoc-3.19.4-linux-x86_64/include/google $PATH_BIN
 }
 
 test_thrift() {
diff --git a/internal/tagexpr/spec_test.go b/internal/tagexpr/spec_test.go
index 07f93d1..b73e9b0 100644
--- a/internal/tagexpr/spec_test.go
+++ b/internal/tagexpr/spec_test.go
@@ -54,7 +54,7 @@ func TestReadBoolExprNode(t *testing.T) {
 		{expr: "true ", val: true, lastExprNode: " "},
 		{expr: "!true&", val: false, lastExprNode: "&"},
 		{expr: "!false|", val: true, lastExprNode: "|"},
-		{expr: "!!!!false =", val: !!!!false, lastExprNode: " ="},
+		{expr: "!!!!false =", val: !!!!false, lastExprNode: " ="}, //nolint:staticcheck // SA4013: negating a boolean twice has no effect
 	}
 	for _, c := range cases {
 		t.Log(c.expr)
diff --git a/pkg/app/client/client_test.go b/pkg/app/client/client_test.go
index c9332a2..e466809 100644
--- a/pkg/app/client/client_test.go
+++ b/pkg/app/client/client_test.go
@@ -57,6 +57,7 @@ import (
 	"path/filepath"
 	"reflect"
 	"regexp"
+	"runtime"
 	"strconv"
 	"strings"
 	"sync"
@@ -457,7 +458,12 @@ func TestClientReadTimeout(t *testing.T) {
 	engine := route.NewEngine(opt)
 
 	readtimeout := 50 * time.Millisecond
-	sleeptime := 75 * time.Millisecond // must > readtimeout
+	if runtime.GOOS == "windows" {
+		// XXX: The windows CI instance powered by Github is quite unstable.
+		// Increase readtimeout here for better testing stability.
+		readtimeout = 2 * readtimeout
+	}
+	sleeptime := readtimeout + readtimeout/2 // must > readtimeout
 
 	engine.GET("/normal", func(c context.Context, ctx *app.RequestContext) {
 		ctx.String(201, "ok")
@@ -1884,24 +1890,28 @@ func TestClientLastMiddleware(t *testing.T) {
 	}
 	mw1 := func(next Endpoint) Endpoint {
 		return func(ctx context.Context, req *protocol.Request, resp *protocol.Response) (err error) {
+			//nolint:staticcheck // SA1029 no built-in type string as key
 			ctx = context.WithValue(ctx, "final0", "final0")
 			return next(ctx, req, resp)
 		}
 	}
 	mw2 := func(next Endpoint) Endpoint {
 		return func(ctx context.Context, req *protocol.Request, resp *protocol.Response) (err error) {
+			//nolint:staticcheck // SA1029 no built-in type string as key
 			ctx = context.WithValue(ctx, "final1", "final1")
 			return next(ctx, req, resp)
 		}
 	}
 	mw3 := func(next Endpoint) Endpoint {
 		return func(ctx context.Context, req *protocol.Request, resp *protocol.Response) (err error) {
+			//nolint:staticcheck // SA1029 no built-in type string as key
 			ctx = context.WithValue(ctx, "final2", "final2")
 			return next(ctx, req, resp)
 		}
 	}
 	mw4 := func(next Endpoint) Endpoint {
 		return func(ctx context.Context, req *protocol.Request, resp *protocol.Response) (err error) {
+			//nolint:staticcheck // SA1029 no built-in type string as key
 			ctx = context.WithValue(ctx, "final0", "final3")
 			return next(ctx, req, resp)
 		}
@@ -2379,12 +2389,14 @@ func TestClientState(t *testing.T) {
 			case int32(0):
 				assert.DeepEqual(t, 1, hcs.ConnPoolState().TotalConnNum)
 				assert.DeepEqual(t, 1, hcs.ConnPoolState().PoolConnNum)
+				assert.DeepEqual(t, consts.DefaultMaxConnsPerHost, hcs.ConnPoolState().MaxConns)
 				assert.DeepEqual(t, "127.0.0.1:10037", hcs.ConnPoolState().Addr)
 				atomic.StoreInt32(&state, int32(1))
 				wg.Done()
 			case int32(1):
 				assert.DeepEqual(t, 0, hcs.ConnPoolState().TotalConnNum)
 				assert.DeepEqual(t, 0, hcs.ConnPoolState().PoolConnNum)
+				assert.DeepEqual(t, consts.DefaultMaxConnsPerHost, hcs.ConnPoolState().MaxConns)
 				assert.DeepEqual(t, "127.0.0.1:10037", hcs.ConnPoolState().Addr)
 				atomic.StoreInt32(&state, int32(2))
 				wg.Done()
diff --git a/pkg/app/context_test.go b/pkg/app/context_test.go
index b2aef63..a5cb00a 100644
--- a/pkg/app/context_test.go
+++ b/pkg/app/context_test.go
@@ -848,32 +848,27 @@ func TestContextContentType(t *testing.T) {
 	assert.DeepEqual(t, consts.MIMEApplicationJSONUTF8, bytesconv.B2s(c.ContentType()))
 }
 
-type MockIpConn struct {
+type MockConn struct {
 	*mock.Conn
-	RemoteIp string
-	Port     int
+
+	remote net.Addr
 }
 
-func (c *MockIpConn) RemoteAddr() net.Addr {
-	return &net.UDPAddr{
-		IP:   net.ParseIP(c.RemoteIp),
-		Port: c.Port,
-	}
+func (c *MockConn) RemoteAddr() net.Addr {
+	return c.remote
 }
 
 func newContextClientIPTest() *RequestContext {
 	c := NewContext(0)
-	c.conn = &MockIpConn{
-		Conn:     mock.NewConn(""),
-		RemoteIp: "127.0.0.1",
-		Port:     8080,
+	c.conn = &MockConn{
+		remote: &net.TCPAddr{IP: net.ParseIP("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) {
+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())
@@ -908,6 +903,14 @@ func TestClientIp(t *testing.T) {
 	}
 	c.SetClientIPFunc(ClientIPWithOption(opts))
 	assert.DeepEqual(t, "30.30.30.30", c.ClientIP())
+
+	// UDS
+	c.conn = &MockConn{remote: &net.UnixAddr{Net: "unix", Name: "/tmp/test.sock"}}
+	assert.DeepEqual(t, "30.30.30.30", c.ClientIP())
+
+	// err: Addr not host:port
+	c.conn = &MockConn{remote: &net.UnixAddr{Net: "tcp", Name: "/tmp/test.sock"}}
+	assert.DeepEqual(t, "", c.ClientIP())
 }
 
 func TestSetClientIPFunc(t *testing.T) {
diff --git a/pkg/app/server/hertz_test.go b/pkg/app/server/hertz_test.go
index b66043d..83b40eb 100644
--- a/pkg/app/server/hertz_test.go
+++ b/pkg/app/server/hertz_test.go
@@ -814,6 +814,7 @@ func (t testTracer) Start(ctx context.Context, c *app.RequestContext) context.Co
 		value = v.(int)
 		value++
 	}
+	//nolint:staticcheck // SA1029 no built-in type string as key
 	return context.WithValue(ctx, "testKey", value)
 }
 
diff --git a/pkg/common/test/mock/writer_test.go b/pkg/common/test/mock/writer_test.go
index f03d69b..1727c7a 100644
--- a/pkg/common/test/mock/writer_test.go
+++ b/pkg/common/test/mock/writer_test.go
@@ -44,7 +44,7 @@ func TestExtWriter(t *testing.T) {
 
 	// setbody
 	b2 := []byte("abc")
-	w.SetBody([]byte(b2))
+	w.SetBody(b2)
 	err = w.Flush()
 	assert.DeepEqual(t, nil, err)
 	assert.DeepEqual(t, b2, w.Buf.Bytes())
diff --git a/pkg/protocol/http1/resp/response_test.go b/pkg/protocol/http1/resp/response_test.go
index cff0a57..3c206e4 100644
--- a/pkg/protocol/http1/resp/response_test.go
+++ b/pkg/protocol/http1/resp/response_test.go
@@ -780,6 +780,10 @@ func TestResponseReadBodyStream(t *testing.T) {
 	testResponseReadBodyStreamSuccess(t, resp, "HTTP/1.1 300 OK\r\nTransfer-Encoding: chunked\r\nTrailer: Foo\r\nContent-Type: bar\r\n\r\n5\r\n56789\r\n0\r\nfoo: bar\r\n\r\n",
 		consts.StatusMultipleChoices, -1, "bar", "56789", map[string]string{"Foo": "bar"}, consts.HTTP11)
 
+	bodyWithLongLength := strings.Repeat("1", 8*1024+1)
+	testResponseReadBodyStreamSuccess(t, resp, "HTTP/1.1 200 OK\r\nContent-Length: 8193\r\nContent-Type: foo/bar\r\n\r\n"+bodyWithLongLength,
+		consts.StatusOK, 8193, "foo/bar", bodyWithLongLength, nil, consts.HTTP11)
+
 	// response with trailer disableNormalizing
 	resp.Header.DisableNormalizing()
 	resp.Header.Trailer().DisableNormalizing()