Repository: cloudwego/hertz
Pull request #717 (2023-04-10 11:30:31)

chore: release v0.6.2


Patch:
diff --git a/cmd/hz/config/argument.go b/cmd/hz/config/argument.go
index 6e42b01..27e376c 100644
--- a/cmd/hz/config/argument.go
+++ b/cmd/hz/config/argument.go
@@ -68,6 +68,7 @@ type Argument struct {
 	NoRecurse            bool
 	HandlerByMethod      bool
 	ForceNew             bool
+	SnakeStyleMiddleware bool
 
 	CustomizeLayout     string
 	CustomizeLayoutData string
diff --git a/cmd/hz/generator/package.go b/cmd/hz/generator/package.go
index 8613a5b..af8e950 100644
--- a/cmd/hz/generator/package.go
+++ b/cmd/hz/generator/package.go
@@ -46,22 +46,25 @@ type Service struct {
 	BaseDomain    string         // base domain for client code
 }
 
+// HttpPackageGenerator is used to record the configuration related to generating hertz http code.
 type HttpPackageGenerator struct {
-	ConfigPath      string
-	Backend         meta.Backend
-	Options         []Option
-	CmdType         string
-	ProjPackage     string
-	HandlerDir      string
-	RouterDir       string
-	ModelDir        string
-	UseDir          string
-	ClientDir       string
-	IdlClientDir    string
-	ForceClientDir  string
-	NeedModel       bool
-	HandlerByMethod bool
-	BaseDomain      string
+	ConfigPath     string       // package template path
+	Backend        meta.Backend // model template
+	Options        []Option
+	CmdType        string
+	ProjPackage    string // go module for project
+	HandlerDir     string
+	RouterDir      string
+	ModelDir       string
+	UseDir         string // model dir for third repo
+	ClientDir      string // client dir for "new"/"update" command
+	IdlClientDir   string // client dir for "client" command
+	ForceClientDir string // client dir without namespace for "client" command
+	BaseDomain     string // request domain for "client" command
+
+	NeedModel            bool
+	HandlerByMethod      bool // generate handler files with method dimension
+	SnakeStyleMiddleware bool // use snake name style for middleware
 
 	loadedBackend   Backend
 	curModel        *model.Model
diff --git a/cmd/hz/generator/package_tpl.go b/cmd/hz/generator/package_tpl.go
index f2a0f2e..1b9bcdc 100644
--- a/cmd/hz/generator/package_tpl.go
+++ b/cmd/hz/generator/package_tpl.go
@@ -120,10 +120,10 @@ import (
 
 {{define "G"}}
 {{- if ne .Handler ""}}
-	{{- .GroupName}}.{{.HttpMethod}}("{{.Path}}", append({{.MiddleWare}}Mw(), {{.Handler}})...)
+	{{- .GroupName}}.{{.HttpMethod}}("{{.Path}}", append({{.HandlerMiddleware}}Mw(), {{.Handler}})...)
 {{- end}}
 {{- if ne (len .Children) 0}}
-{{.MiddleWare}} := {{template "g" .}}.Group("{{.Path}}", {{.MiddleWare}}Mw()...)
+{{.MiddleWare}} := {{template "g" .}}.Group("{{.Path}}", {{.GroupMiddleware}}Mw()...)
 {{- end}}
 {{- range $_, $router := .Children}}
 {{- if ne .Handler ""}}
@@ -177,10 +177,18 @@ import (
 )
 
 {{define "M"}}
-func {{.MiddleWare}}Mw() []app.HandlerFunc {
+{{- if ne .Children.Len 0}}
+func {{.GroupMiddleware}}Mw() []app.HandlerFunc {
 	// your code...
 	return nil
 }
+{{end}}
+{{- if ne .Handler ""}}
+func {{.HandlerMiddleware}}Mw() []app.HandlerFunc {
+	// your code...
+	return nil
+}
+{{end}}
 {{range $_, $router := $.Children}}{{template "M" $router}}{{end}}
 {{- end}}
 
diff --git a/cmd/hz/generator/router.go b/cmd/hz/generator/router.go
index e335a26..16378a8 100644
--- a/cmd/hz/generator/router.go
+++ b/cmd/hz/generator/router.go
@@ -36,10 +36,14 @@ type Router struct {
 }
 
 type RouterNode struct {
-	GroupName  string
-	MiddleWare string
+	GroupName         string // current group name(the parent middleware name), used to register route. example: {{.GroupName}}.{{HttpMethod}}
+	MiddleWare        string // current node middleware, used to be group name for children.
+	HandlerMiddleware string
+	GroupMiddleware   string
+	PathPrefix        string
 
 	Path     string
+	Parent   *RouterNode
 	Children childrenRouterInfo
 
 	Handler             string // {{HandlerPackage}}.{{HandlerName}}
@@ -57,9 +61,11 @@ type RegisterInfo struct {
 // NewRouterTree contains "/" as root node
 func NewRouterTree() *RouterNode {
 	return &RouterNode{
-		GroupName:  "root",
-		MiddleWare: "root",
-		Path:       "/",
+		GroupName:       "root",
+		MiddleWare:      "root",
+		GroupMiddleware: "root",
+		Path:            "/",
+		Parent:          nil,
 	}
 }
 
@@ -85,8 +91,15 @@ func (routerNode *RouterNode) Update(method *HttpMethod, handlerType, handlerPkg
 	return nil
 }
 
-// DyeGroupName traverses the routing tree in depth and names the middleware for each node.
-func (routerNode *RouterNode) DyeGroupName() error {
+func (routerNode *RouterNode) RawHandlerName() string {
+	parts := strings.Split(routerNode.Handler, ".")
+	handlerName := parts[len(parts)-1]
+	return handlerName
+}
+
+// DyeGroupName traverses the routing tree in depth and names the handler/group middleware for each node.
+// If snakeStyleMiddleware is set to true, the name style of the middleware will use snake name style.
+func (routerNode *RouterNode) DyeGroupName(snakeStyleMiddleware bool) error {
 	groups := []string{"root"}
 
 	hook := func(layer int, node *RouterNode) error {
@@ -96,18 +109,56 @@ func (routerNode *RouterNode) DyeGroupName() error {
 			if len(pname) > 1 && pname[0] == '/' {
 				pname = pname[1:]
 			}
-			if len(node.Children) == 0 && node.Handler != "" {
-				handleName := strings.Split(node.Handler, ".")
-				pname = handleName[len(handleName)-1]
+
+			if node.Parent != nil {
+				node.PathPrefix = node.Parent.PathPrefix + "_" + util.ToGoFuncName(pname)
+			} else {
+				node.PathPrefix = "_" + util.ToGoFuncName(pname)
 			}
-			pname = util.ToVarName([]string{pname})
-			// The tolow operation is placed here, unifying the middleware raw name
-			pname = strings.ToLower(pname)
-			pname, err := util.GetMiddlewareUniqueName(pname)
-			if err != nil {
-				return fmt.Errorf("get unique name for middleware '%s' failed, err: %v", pname, err)
+
+			handlerMiddlewareName := ""
+			isLeafNode := false
+			if len(node.Handler) != 0 {
+				handlerMiddlewareName = node.RawHandlerName()
+				// If it is a leaf node, then "group middleware name" and "handler middleware name" are the same
+				if len(node.Children) == 0 {
+					pname = handlerMiddlewareName
+					isLeafNode = true
+				}
+			}
+
+			pname = convertToMiddlewareName(pname)
+			handlerMiddlewareName = convertToMiddlewareName(handlerMiddlewareName)
+
+			if isLeafNode {
+				name, err := util.GetMiddlewareUniqueName(pname)
+				if err != nil {
+					return fmt.Errorf("get unique name for middleware '%s' failed, err: %v", name, err)
+				}
+				pname = name
+				handlerMiddlewareName = name
+			} else {
+				var err error
+				pname, err = util.GetMiddlewareUniqueName(pname)
+				if err != nil {
+					return fmt.Errorf("get unique name for middleware '%s' failed, err: %v", pname, err)
+				}
+				handlerMiddlewareName, err = util.GetMiddlewareUniqueName(handlerMiddlewareName)
+				if err != nil {
+					return fmt.Errorf("get unique name for middleware '%s' failed, err: %v", handlerMiddlewareName, err)
+				}
 			}
 			node.MiddleWare = "_" + pname
+			if len(node.Handler) != 0 {
+				node.HandlerMiddleware = "_" + handlerMiddlewareName
+				if snakeStyleMiddleware {
+					node.HandlerMiddleware = "_" + node.RawHandlerName()
+				}
+			}
+			node.GroupMiddleware = node.MiddleWare
+			if snakeStyleMiddleware {
+				node.GroupMiddleware = node.PathPrefix
+			}
 		}
 		if layer >= len(groups)-1 {
 			groups = append(groups, node.MiddleWare)
@@ -145,7 +196,8 @@ func (routerNode *RouterNode) Insert(name string, method *HttpMethod, handlerTyp
 	cur := routerNode
 	for i, p := range paths {
 		c := &RouterNode{
-			Path: "/" + p,
+			Path:   "/" + p,
+			Parent: cur,
 		}
 		if i == len(paths)-1 {
 			// generate handler by method
@@ -285,7 +337,7 @@ func (pkgGen *HttpPackageGenerator) updateRegister(pkg, rDir, pkgName string) er
 }
 
 func (pkgGen *HttpPackageGenerator) genRouter(pkg *HttpPackage, root *RouterNode, handlerPackage, routerDir, routerPackage string) error {
-	err := root.DyeGroupName()
+	err := root.DyeGroupName(pkgGen.SnakeStyleMiddleware)
 	if err != nil {
 		return err
 	}
@@ -334,10 +386,16 @@ func (pkgGen *HttpPackageGenerator) updateMiddlewareReg(router interface{}, midd
 	if !isExist {
 		return pkgGen.TemplateGenerator.Generate(router, middlewareTpl, filePath, false)
 	}
-	middlewareList := make([]string, 1)
+	var middlewareList []string
 
 	_ = router.(Router).Router.DFS(0, func(layer int, node *RouterNode) error {
-		middlewareList = append(middlewareList, node.MiddleWare)
+		// non-leaf node will generate group middleware
+		if node.Children.Len() > 0 && len(node.GroupMiddleware) > 0 {
+			middlewareList = append(middlewareList, node.GroupMiddleware)
+		}
+		if len(node.HandlerMiddleware) > 0 {
+			middlewareList = append(middlewareList, node.HandlerMiddleware)
+		}
 		return nil
 	})
 
@@ -347,7 +405,7 @@ func (pkgGen *HttpPackageGenerator) updateMiddlewareReg(router interface{}, midd
 	}
 
 	for _, mw := range middlewareList {
-		if bytes.Contains(file, []byte(mw+"Mw")) {
+		if bytes.Contains(file, []byte(mw)) {
 			continue
 		}
 		middlewareSingleTpl := pkgGen.tpls[middlewareSingleTplName]
@@ -378,3 +436,10 @@ func (pkgGen *HttpPackageGenerator) updateMiddlewareReg(router interface{}, midd
 
 	return nil
 }
+
+// convertToMiddlewareName converts a route path to a middleware name
+func convertToMiddlewareName(path string) string {
+	path = util.ToVarName([]string{path})
+	path = strings.ToLower(path)
+	return path
+}
diff --git a/cmd/hz/meta/const.go b/cmd/hz/meta/const.go
index 8e9813c..7e5e5db 100644
--- a/cmd/hz/meta/const.go
+++ b/cmd/hz/meta/const.go
@@ -19,7 +19,7 @@ package meta
 import "runtime"
 
 // Version hz version
-const Version = "v0.6.1"
+const Version = "v0.6.2"
 
 // Mode hz run modes
 type Mode int
diff --git a/cmd/hz/meta/manifest.go b/cmd/hz/meta/manifest.go
index a4a7434..0c6ecd0 100644
--- a/cmd/hz/meta/manifest.go
+++ b/cmd/hz/meta/manifest.go
@@ -61,12 +61,12 @@ func (manifest *Manifest) InitAndValidate(dir string) error {
 	return nil
 }
 
-const hzTitle = "// Code generated by hz. DO NOT EDIT.\n"
+const hzTitle = "// Code generated by hz. DO NOT EDIT."
 
 func (manifest *Manifest) String() string {
 	conf, _ := yaml.Marshal(*manifest)
 
-	return hzTitle + "\n" +
+	return hzTitle + "\n\n" +
 		string(conf)
 }
 
diff --git a/cmd/hz/protobuf/plugin.go b/cmd/hz/protobuf/plugin.go
index 6d9afc7..82b01cd 100644
--- a/cmd/hz/protobuf/plugin.go
+++ b/cmd/hz/protobuf/plugin.go
@@ -600,13 +600,14 @@ func (plugin *Plugin) genHttpPackage(ast *descriptorpb.FileDescriptorProto, deps
 			OutputDir: args.OutDir,
 			Excludes:  args.Excludes,
 		},
-		ProjPackage:     pkg,
-		Options:         options,
-		HandlerByMethod: args.HandlerByMethod,
-		CmdType:         args.CmdType,
-		IdlClientDir:    plugin.IdlClientDir,
-		ForceClientDir:  args.ForceClientDir,
-		BaseDomain:      args.BaseDomain,
+		ProjPackage:          pkg,
+		Options:              options,
+		HandlerByMethod:      args.HandlerByMethod,
+		CmdType:              args.CmdType,
+		IdlClientDir:         plugin.IdlClientDir,
+		ForceClientDir:       args.ForceClientDir,
+		BaseDomain:           args.BaseDomain,
+		SnakeStyleMiddleware: args.SnakeStyleMiddleware,
 	}
 
 	if args.ModelBackend != "" {
diff --git a/cmd/hz/thrift/ast.go b/cmd/hz/thrift/ast.go
index 9622ed8..bbee88d 100644
--- a/cmd/hz/thrift/ast.go
+++ b/cmd/hz/thrift/ast.go
@@ -98,19 +98,19 @@ func astToService(ast *parser.Thrift, resolver *Resolver, cmdType string) ([]*ge
 				if len(m.Arguments) > 1 {
 					logs.Warnf("function '%s' has more than one argument, but only the first can be used in hertz now", m.GetName())
 				}
-				rt, err := resolver.ResolveIdentifier(m.Arguments[0].GetType().GetName())
+				var err error
+				reqName, err = resolver.ResolveTypeName(m.Arguments[0].GetType())
 				if err != nil {
 					return nil, err
 				}
-				reqName = rt.Expression()
 			}
 			var respName string
 			if !m.Oneway {
-				respType, err := resolver.ResolveIdentifier(m.GetFunctionType().GetName())
+				var err error
+				respName, err = resolver.ResolveTypeName(m.GetFunctionType())
 				if err != nil {
 					return nil, err
 				}
-				respName = respType.Expression()
 			}
 
 			sr, _ := util.GetFirstKV(getAnnotations(m.Annotations, SerializerTags))
diff --git a/cmd/hz/thrift/plugin.go b/cmd/hz/thrift/plugin.go
index 9392553..b290398 100644
--- a/cmd/hz/thrift/plugin.go
+++ b/cmd/hz/thrift/plugin.go
@@ -138,13 +138,14 @@ func (plugin *Plugin) Run() int {
 			OutputDir: args.OutDir,
 			Excludes:  args.Excludes,
 		},
-		ProjPackage:     pkg,
-		Options:         options,
-		HandlerByMethod: args.HandlerByMethod,
-		CmdType:         args.CmdType,
-		IdlClientDir:    util.SubDir(modelDir, pkgInfo.Package),
-		ForceClientDir:  args.ForceClientDir,
-		BaseDomain:      args.BaseDomain,
+		ProjPackage:          pkg,
+		Options:              options,
+		HandlerByMethod:      args.HandlerByMethod,
+		CmdType:              args.CmdType,
+		IdlClientDir:         util.SubDir(modelDir, pkgInfo.Package),
+		ForceClientDir:       args.ForceClientDir,
+		BaseDomain:           args.BaseDomain,
+		SnakeStyleMiddleware: args.SnakeStyleMiddleware,
 	}
 	if args.ModelBackend != "" {
 		sg.Backend = meta.Backend(args.ModelBackend)
diff --git a/cmd/hz/thrift/resolver.go b/cmd/hz/thrift/resolver.go
index 9b40b5c..0bcb4e3 100644
--- a/cmd/hz/thrift/resolver.go
+++ b/cmd/hz/thrift/resolver.go
@@ -282,6 +282,44 @@ func (resolver *Resolver) ResolveIdentifier(id string) (ret ResolvedSymbol, err
 	return
 }
 
+func (resolver *Resolver) ResolveTypeName(typ *parser.Type) (string, error) {
+	switch typ.GetCategory() {
+	case parser.Category_Map:
+		keyType, err := resolver.ResolveTypeName(typ.GetKeyType())
+		if err != nil {
+			return "", err
+		}
+		if typ.GetKeyType().GetCategory().IsStruct() {
+			keyType = "*" + keyType
+		}
+		valueType, err := resolver.ResolveTypeName(typ.GetValueType())
+		if err != nil {
+			return "", err
+		}
+		if typ.GetValueType().GetCategory().IsStruct() {
+			valueType = "*" + valueType
+		}
+		return fmt.Sprintf("map[%s]%s", keyType, valueType), nil
+	case parser.Category_List, parser.Category_Set:
+		// list/set -> []element for thriftgo
+		// valueType refers the element type for list/set
+		elemType, err := resolver.ResolveTypeName(typ.GetValueType())
+		if err != nil {
+			return "", err
+		}
+		if typ.GetValueType().GetCategory().IsStruct() {
+			elemType = "*" + elemType
+		}
+		return fmt.Sprintf("[]%s", elemType), err
+	}
+	rt, err := resolver.ResolveIdentifier(typ.GetName())
+	if err != nil {
+		return "", err
+	}
+
+	return rt.Expression(), nil
+}
+
 func (resolver *Resolver) Get(name string) *Symbol {
 	s, ok := resolver.root[name]
 	if ok {
@@ -348,8 +386,8 @@ func (resolver *Resolver) LoadAll(ast *parser.Thrift) error {
 	return nil
 }
 
-func LoadBaseIdentifier() map[string]*Symbol {
-	ret := make(NameSpace, 13)
+func LoadBaseIdentifier() NameSpace {
+	ret := make(NameSpace, 16)
 
 	ret["true"] = &ConstTrue
 	ret["false"] = &ConstFalse
@@ -394,6 +432,18 @@ func LoadBaseIdentifier() map[string]*Symbol {
 		Type:  model.TypeBinary,
 		Scope: &BaseThrift,
 	}
+	ret["list"] = &Symbol{
+		Type:  model.TypeBaseList,
+		Scope: &BaseThrift,
+	}
+	ret["set"] = &Symbol{
+		Type:  model.TypeBaseSet,
+		Scope: &BaseThrift,
+	}
+	ret["map"] = &Symbol{
+		Type:  model.TypeBaseMap,
+		Scope: &BaseThrift,
+	}
 	return ret
 }
 
diff --git a/cmd/hz/util/data.go b/cmd/hz/util/data.go
index ad19158..52c3674 100644
--- a/cmd/hz/util/data.go
+++ b/cmd/hz/util/data.go
@@ -22,6 +22,7 @@ import (
 	"net/url"
 	"path/filepath"
 	"reflect"
+	"regexp"
 	"strconv"
 	"strings"
 
@@ -307,7 +308,7 @@ func ToVarName(paths []string) string {
 		if c == ':' || c == '*' {
 			continue
 		}
-		if (c >= '0' && c <= '9' && i != 0) || (c >= 'a' && c <= 'z') || (c > 'A' && c <= 'Z') || (c == '_') {
+		if (c >= '0' && c <= '9' && i != 0) || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c == '_') {
 			out = append(out, c)
 		} else {
 			out = append(out, '_')
@@ -420,3 +421,16 @@ func SubPackageDir(path string) string {
 	}
 	return path[:index]
 }
+
+var validFuncReg = regexp.MustCompile("[_0-9a-zA-Z]")
+
+// ToGoFuncName converts a string to a function naming style for go
+func ToGoFuncName(s string) string {
+	ss := []byte(s)
+	for i := range ss {
+		if !validFuncReg.Match([]byte{s[i]}) {
+			ss[i] = '_'
+		}
+	}
+	return string(ss)
+}
diff --git a/go.mod b/go.mod
index 2c7a975..79b5fab 100644
--- a/go.mod
+++ b/go.mod
@@ -5,6 +5,7 @@ go 1.16
 require (
 	github.com/bytedance/go-tagexpr/v2 v2.9.2
 	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.1
 	github.com/fsnotify/fsnotify v1.5.4
diff --git a/go.sum b/go.sum
index fe7ecbf..f9ea5da 100644
--- a/go.sum
+++ b/go.sum
@@ -2,6 +2,8 @@ github.com/bytedance/go-tagexpr/v2 v2.9.2 h1:QySJaAIQgOEDQBLS3x9BxOWrnhqu5sQ+f6H
 github.com/bytedance/go-tagexpr/v2 v2.9.2/go.mod h1:5qsx05dYOiUXOUgnQ7w3Oz8BYs2qtM/bJokdLb79wRM=
 github.com/bytedance/gopkg v0.0.0-20220413063733-65bf48ffb3a7 h1:PtwsQyQJGxf8iaPptPNaduEIu9BnrNms+pcRdHAxZaM=
 github.com/bytedance/gopkg v0.0.0-20220413063733-65bf48ffb3a7/go.mod h1:2ZlV9BaUH4+NXIBF0aMdKKAnHTzqH+iMU4KUjAbL23Q=
+github.com/bytedance/mockey v1.2.1 h1:g84ngI88hz1DR4wZTL3yOuqlEcq67MretBfQUdXwrmw=
+github.com/bytedance/mockey v1.2.1/go.mod h1:+Jm/fzWZAuhEDrPXVjDf/jLM2BlLXJkwk94zf2JZ3X4=
 github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
 github.com/bytedance/sonic v1.8.1 h1:NqAHCaGaTzro0xMmnTCLUyRlbEP6r8MCA1cJUrH3Pu4=
 github.com/bytedance/sonic v1.8.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
@@ -20,17 +22,25 @@ github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
 github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
 github.com/henrylee2cn/ameda v1.4.8/go.mod h1:liZulR8DgHxdK+MEwvZIylGnmcjzQ6N6f2PlWe7nEO4=
 github.com/henrylee2cn/ameda v1.4.10 h1:JdvI2Ekq7tapdPsuhrc4CaFiqw6QXFvZIULWJgQyCAk=
 github.com/henrylee2cn/ameda v1.4.10/go.mod h1:liZulR8DgHxdK+MEwvZIylGnmcjzQ6N6f2PlWe7nEO4=
 github.com/henrylee2cn/goutil v0.0.0-20210127050712-89660552f6f8 h1:yE9ULgp02BhYIrO6sdV/FPe0xQM6fNHkVQW2IAymfM0=
 github.com/henrylee2cn/goutil v0.0.0-20210127050712-89660552f6f8/go.mod h1:Nhe/DM3671a5udlv2AdV2ni/MZzgfv2qrPL5nIi3EGQ=
+github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
+github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
 github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
 github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
 github.com/nyaruka/phonenumbers v1.0.55 h1:bj0nTO88Y68KeUQ/n3Lo2KgK7lM1hF7L9NFuwcCl3yg=
 github.com/nyaruka/phonenumbers v1.0.55/go.mod h1:sDaTZ/KPX5f8qyV9qN+hIm+4ZBARJrupC6LuhshJq1U=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
+github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -50,13 +60,19 @@ github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
 github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
 github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
 github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
+golang.org/x/arch v0.0.0-20201008161808-52c3e6f60cff/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4=
 golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
 golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20220110181412-a018aaa089fe/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0=
 golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 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=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
diff --git a/pkg/common/ut/context.go b/pkg/common/ut/context.go
new file mode 100644
index 0000000..30023dd
--- /dev/null
+++ b/pkg/common/ut/context.go
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2023 CloudWeGo Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ut
+
+import (
+	"io"
+	"io/ioutil"
+
+	"github.com/cloudwego/hertz/pkg/app"
+	"github.com/cloudwego/hertz/pkg/common/config"
+	"github.com/cloudwego/hertz/pkg/protocol"
+	"github.com/cloudwego/hertz/pkg/route"
+)
+
+// CreateUtRequestContext returns an app.RequestContext for testing purposes
+func CreateUtRequestContext(method, url string, body *Body, headers ...Header) *app.RequestContext {
+	engine := route.NewEngine(config.NewOptions([]config.Option{}))
+	return createUtRequestContext(engine, method, url, body, headers...)
+}
+
+func createUtRequestContext(engine *route.Engine, method, url string, body *Body, headers ...Header) *app.RequestContext {
+	ctx := engine.NewContext()
+
+	var r *protocol.Request
+	if body != nil && body.Body != nil {
+		r = protocol.NewRequest(method, url, body.Body)
+		r.CopyTo(&ctx.Request)
+		if engine.IsStreamRequestBody() || body.Len == -1 {
+			ctx.Request.SetBodyStream(body.Body, body.Len)
+		} else {
+			buf, err := ioutil.ReadAll(&io.LimitedReader{R: body.Body, N: int64(body.Len)})
+			ctx.Request.SetBody(buf)
+			if err != nil && err != io.EOF {
+				panic(err)
+			}
+		}
+	} else {
+		r = protocol.NewRequest(method, url, nil)
+		r.CopyTo(&ctx.Request)
+	}
+
+	for _, v := range headers {
+		if ctx.Request.Header.Get(v.Key) != "" {
+			ctx.Request.Header.Add(v.Key, v.Value)
+		} else {
+			ctx.Request.Header.Set(v.Key, v.Value)
+		}
+	}
+
+	return ctx
+}
diff --git a/pkg/common/ut/request.go b/pkg/common/ut/request.go
index f6dcfc4..93f8f91 100644
--- a/pkg/common/ut/request.go
+++ b/pkg/common/ut/request.go
@@ -20,9 +20,7 @@ package ut
 import (
 	"context"
 	"io"
-	"io/ioutil"
 
-	"github.com/cloudwego/hertz/pkg/protocol"
 	"github.com/cloudwego/hertz/pkg/route"
 )
 
@@ -49,34 +47,7 @@ type Body struct {
 //
 // See ./request_test.go for more examples
 func PerformRequest(engine *route.Engine, method, url string, body *Body, headers ...Header) *ResponseRecorder {
-	ctx := engine.NewContext()
-
-	var r *protocol.Request
-	if body != nil && body.Body != nil {
-		r = protocol.NewRequest(method, url, body.Body)
-		r.CopyTo(&ctx.Request)
-		if engine.IsStreamRequestBody() || body.Len == -1 {
-			ctx.Request.SetBodyStream(body.Body, body.Len)
-		} else {
-			buf, err := ioutil.ReadAll(&io.LimitedReader{R: body.Body, N: int64(body.Len)})
-			ctx.Request.SetBody(buf)
-			if err != nil && err != io.EOF {
-				panic(err)
-			}
-		}
-	} else {
-		r = protocol.NewRequest(method, url, nil)
-		r.CopyTo(&ctx.Request)
-	}
-
-	for _, v := range headers {
-		if ctx.Request.Header.Get(v.Key) != "" {
-			ctx.Request.Header.Add(v.Key, v.Value)
-		} else {
-			ctx.Request.Header.Set(v.Key, v.Value)
-		}
-	}
-
+	ctx := createUtRequestContext(engine, method, url, body, headers...)
 	engine.ServeHTTP(context.Background(), ctx)
 
 	w := NewRecorder()
diff --git a/pkg/network/standard/connection.go b/pkg/network/standard/connection.go
index 5ab9a8c..65fd08f 100644
--- a/pkg/network/standard/connection.go
+++ b/pkg/network/standard/connection.go
@@ -21,6 +21,7 @@ import (
 	"errors"
 	"io"
 	"net"
+	"runtime"
 	"strconv"
 	"syscall"
 	"time"
@@ -174,9 +175,6 @@ func (c *Conn) ReadFrom(r io.Reader) (n int64, err error) {
 
 // Close closes the connection
 func (c *Conn) Close() error {
-	c.inputBuffer.release()
-	c.outputBuffer.release()
-	c.inputBuffer, c.outputBuffer = nil, nil
 	return c.c.Close()
 }
 
@@ -548,20 +546,27 @@ func newConn(c net.Conn, size int) network.Conn {
 	if size > maxSize {
 		maxSize = size
 	}
-	inputNode := newBufferNode(maxSize)
+
+	node := newBufferNode(maxSize)
+	inputBuffer := &linkBuffer{
+		head:  node,
+		read:  node,
+		write: node,
+	}
+	runtime.SetFinalizer(inputBuffer, (*linkBuffer).release)
+
 	outputNode := newBufferNode(0)
+	outputBuffer := &linkBuffer{
+		head:  outputNode,
+		write: outputNode,
+	}
+	runtime.SetFinalizer(outputBuffer, (*linkBuffer).release)
+
 	return &Conn{
-		c: c,
-		inputBuffer: &linkBuffer{
-			head:  inputNode,
-			read:  inputNode,
-			write: inputNode,
-		},
-		outputBuffer: &linkBuffer{
-			head:  outputNode,
-			write: outputNode,
-		},
-		maxSize: maxSize,
+		c:            c,
+		inputBuffer:  inputBuffer,
+		outputBuffer: outputBuffer,
+		maxSize:      maxSize,
 	}
 }
 
@@ -570,21 +575,28 @@ func newTLSConn(c net.Conn, size int) network.Conn {
 	if size > maxSize {
 		maxSize = size
 	}
+
 	node := newBufferNode(maxSize)
+	inputBuffer := &linkBuffer{
+		head:  node,
+		read:  node,
+		write: node,
+	}
+	runtime.SetFinalizer(inputBuffer, (*linkBuffer).release)
+
 	outputNode := newBufferNode(0)
+	outputBuffer := &linkBuffer{
+		head:  outputNode,
+		write: outputNode,
+	}
+	runtime.SetFinalizer(outputBuffer, (*linkBuffer).release)
+
 	return &TLSConn{
 		Conn{
-			c: c,
-			inputBuffer: &linkBuffer{
-				head:  node,
-				read:  node,
-				write: node,
-			},
-			outputBuffer: &linkBuffer{
-				head:  outputNode,
-				write: outputNode,
-			},
-			maxSize: maxSize,
+			c:            c,
+			inputBuffer:  inputBuffer,
+			outputBuffer: outputBuffer,
+			maxSize:      maxSize,
 		},
 	}
 }
diff --git a/pkg/protocol/http1/client.go b/pkg/protocol/http1/client.go
index 8a35aed..353ecc8 100644
--- a/pkg/protocol/http1/client.go
+++ b/pkg/protocol/http1/client.go
@@ -619,11 +619,21 @@ func (c *HostClient) doNonNilReqResp(req *protocol.Request, resp *protocol.Respo
 	}
 	zr := c.acquireReader(conn)
 
+	// init here for passing in ReadBodyStream's closure
+	// and this value will be assigned after reading Response's Header
+	//
+	// This is to solve the circular dependency problem of Response and BodyStream
+	shouldCloseConn := false
+
 	if !c.ResponseBodyStream {
 		err = respI.ReadHeaderAndLimitBody(resp, zr, c.MaxResponseBodySize)
 	} else {
 		err = respI.ReadBodyStream(resp, zr, c.MaxResponseBodySize, func() error {
-			c.releaseConn(cc)
+			if shouldCloseConn {
+				c.closeConn(cc)
+			} else {
+				c.releaseConn(cc)
+			}
 			return nil
 		})
 	}
@@ -638,11 +648,13 @@ func (c *HostClient) doNonNilReqResp(req *protocol.Request, resp *protocol.Respo
 
 	zr.Release() //nolint:errcheck
 
+	shouldCloseConn = resetConnection || req.ConnectionClose() || resp.ConnectionClose()
+
 	if c.ResponseBodyStream {
 		return false, err
 	}
 
-	if resetConnection || req.ConnectionClose() || resp.ConnectionClose() {
+	if shouldCloseConn {
 		c.closeConn(cc)
 	} else {
 		c.releaseConn(cc)
diff --git a/version.go b/version.go
index b0b607b..208f61c 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.6.1"
+	Version = "v0.6.2"
 )


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index b3e284c..0325bef 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -1,13 +1,30 @@
 name: Tests
 
-on: [ push, pull_request ]
+on: [push, pull_request]
 
 jobs:
+  compat-test:
+    strategy:
+      matrix:
+        version: ["1.16", "1.17"]
+    runs-on: [self-hosted, X64]
+    steps:
+      - uses: actions/checkout@v3
+
+      - name: Set up Go
+        uses: actions/setup-go@v3
+        with:
+          go-version: ${{ matrix.version }}
+
+      # Just build tests without running them
+      - name: Build Test
+        run: go test -run=nope ./...
+
   lint-and-ut:
     strategy:
       matrix:
-        version: [ '1.18', '1.19', '1.20' ]
-    runs-on: [ self-hosted, X64 ]
+        version: ["1.18", "1.19", "1.20"]
+    runs-on: [self-hosted, X64]
     steps:
       - uses: actions/checkout@v3
 
@@ -31,7 +48,7 @@ jobs:
   ut-windows:
     strategy:
       matrix:
-        version: [ '1.18', '1.19', '1.20' ]
+        version: ["1.18", "1.19", "1.20"]
     runs-on: windows-latest
     steps:
       - uses: actions/checkout@v3
@@ -43,3 +60,65 @@ jobs:
 
       - name: Unit Test
         run: go test -race -covermode=atomic  ./...
+
+  hz-test-unix:
+    strategy:
+      matrix:
+        version: [ '1.20' ]
+    runs-on: [ self-hosted, X64 ]
+    steps:
+      - uses: actions/checkout@v3
+
+      - name: Set up Go
+        uses: actions/setup-go@v3
+        with:
+          go-version: ${{ matrix.version }}
+
+      - name: Setup Environment
+        run: |
+          echo "GOPATH=$(go env GOPATH)" >> $GITHUB_ENV
+          echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
+
+      - name: Get changed files
+        id: changed-files
+        uses: tj-actions/changed-files@v35
+
+      - name: Hz Test
+        if: contains(steps.changed-files.outputs.all_changed_files, 'cmd/hz')
+        run: |
+          cd cmd/hz
+          sh test_hz_unix.sh
+
+
+  hz-test-windows:
+    strategy:
+      matrix:
+        version: [ '1.20']
+    runs-on: windows-latest
+    steps:
+      - uses: actions/checkout@v3
+
+      - name: Set up Go
+        uses: actions/setup-go@v3
+        with:
+          go-version: ${{ matrix.version }}
+
+      - name: Get changed files
+        id: changed-files
+        uses: tj-actions/changed-files@v35
+
+      - name: Install Protobuf
+        shell: pwsh
+        run: |
+          Invoke-WebRequest https://github.com/protocolbuffers/protobuf/releases/download/v3.19.4/protoc-3.19.4-win64.zip -OutFile protoc-3.19.4-win64.zip
+          Expand-Archive protoc-3.19.4-win64.zip -DestinationPath protoc-3.19.4-win64
+          $GOPATH=go env GOPATH
+          Copy-Item -Path protoc-3.19.4-win64\bin\protoc.exe -Destination $GOPATH\bin
+          Copy-Item -Path protoc-3.19.4-win64\include\* -Destination cmd\hz\testdata\include\google -Recurse
+          protoc --version
+
+      - name: Hz Test
+        if: contains(steps.changed-files.outputs.all_changed_files, 'cmd/hz')
+        run: |
+          cd cmd/hz
+          sh test_hz_windows.sh
diff --git a/cmd/hz/test_hz_unix.sh b/cmd/hz/test_hz_unix.sh
new file mode 100644
index 0000000..133f90b
--- /dev/null
+++ b/cmd/hz/test_hz_unix.sh
@@ -0,0 +1,105 @@
+#! /usr/bin/env bash
+
+# const value define
+moduleName="github.com/cloudwego/hertz/cmd/hz/test"
+curDir=`pwd`
+thriftIDL=$curDir"/testdata/thrift/psm.thrift"
+protobuf2IDL=$curDir"/testdata/protobuf2/psm/psm.proto"
+proto2Search=$curDir"/testdata/protobuf2"
+protobuf3IDL=$curDir"/testdata/protobuf3/psm/psm.proto"
+proto3Search=$curDir"/testdata/protobuf3"
+protoSearch="/usr/local/include"
+
+judge_exit() {
+  code=$1
+  if [ $code != 0 ]; then
+    exit $code
+  fi
+}
+
+compile_hz() {
+  go build -o hz
+  judge_exit "$?"
+}
+
+install_dependent_tools() {
+  # install thriftgo
+  go install github.com/cloudwego/thriftgo@latest
+
+  # 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
+}
+
+test_thrift() {
+  mkdir -p test
+  cd test
+  ../hz new --idl=$thriftIDL --mod=$moduleName -f --model_dir=hertz_model --handler_dir=hertz_handler --router_dir=hertz_router
+  judge_exit "$?"
+  go mod tidy && go build .
+  judge_exit "$?"
+  ../hz update --idl=$thriftIDL
+  judge_exit "$?"
+  ../hz model --idl=$thriftIDL --model_dir=hertz_model
+  judge_exit "$?"
+  ../hz client --idl=$thriftIDL --client_dir=hertz_client
+  judge_exit "$?"
+  cd ..
+  rm -rf test
+}
+
+test_protobuf2() {
+  # test protobuf2
+  mkdir -p test
+  cd test
+  ../hz new -I=$protoSearch -I=$proto2Search --idl=$protobuf2IDL --mod=$moduleName -f --model_dir=hertz_model --handler_dir=hertz_handler --router_dir=hertz_router
+  judge_exit "$?"
+  go mod tidy && go build .
+  judge_exit "$?"
+  ../hz update -I=$protoSearch -I=$proto2Search --idl=$protobuf2IDL
+  judge_exit "$?"
+  ../hz model -I=$protoSearch -I=$proto2Search --idl=$protobuf2IDL --model_dir=hertz_model
+  judge_exit "$?"
+  ../hz client -I=$protoSearch -I=$proto2Search --idl=$protobuf2IDL --client_dir=hertz_client
+  judge_exit "$?"
+  cd ..
+  rm -rf test
+}
+
+test_protobuf3() {
+  # test protobuf2
+  mkdir -p test
+  cd test
+  ../hz new -I=$protoSearch -I=$proto3Search --idl=$protobuf3IDL --mod=$moduleName -f --model_dir=hertz_model --handler_dir=hertz_handler --router_dir=hertz_router
+  judge_exit "$?"
+  go mod tidy && go build .
+  judge_exit "$?"
+  ../hz update -I=$protoSearch -I=$proto3Search --idl=$protobuf3IDL
+  judge_exit "$?"
+  ../hz model -I=$protoSearch -I=$proto3Search --idl=$protobuf3IDL --model_dir=hertz_model
+  judge_exit "$?"
+  ../hz client -I=$protoSearch -I=$proto3Search --idl=$protobuf3IDL --client_dir=hertz_client
+  judge_exit "$?"
+  cd ..
+  rm -rf test
+}
+
+main() {
+  compile_hz
+  judge_exit "$?"
+  install_dependent_tools
+  judge_exit "$?"
+  echo "test thrift......"
+  test_thrift
+  judge_exit "$?"
+  echo "test protobuf2......"
+  test_protobuf2
+  judge_exit "$?"
+  echo "test protobuf3......"
+  test_protobuf3
+  judge_exit "$?"
+  echo "hz execute success"
+}
+main
diff --git a/cmd/hz/test_hz_windows.sh b/cmd/hz/test_hz_windows.sh
new file mode 100644
index 0000000..b421bab
--- /dev/null
+++ b/cmd/hz/test_hz_windows.sh
@@ -0,0 +1,101 @@
+#! /usr/bin/env bash
+
+# const value define
+moduleName="github.com/cloudwego/hertz/cmd/hz/test"
+curDir=`pwd`
+thriftIDL=$curDir"/testdata/thrift/psm.thrift"
+protobuf2IDL=$curDir"/testdata/protobuf2/psm/psm.proto"
+proto2Search=$curDir"/testdata/protobuf2"
+protobuf3IDL=$curDir"/testdata/protobuf3/psm/psm.proto"
+proto3Search=$curDir"/testdata/protobuf3"
+protoSearch=$curDir"/testdata/include"
+
+judge_exit() {
+  code=$1
+  if [ $code != 0 ]; then
+    exit $code
+  fi
+}
+
+compile_hz() {
+  go build -o hz
+  judge_exit "$?"
+}
+
+install_dependent_tools() {
+ # install thriftgo
+ go install github.com/cloudwego/thriftgo@latest
+}
+
+test_thrift() {
+  # test thrift
+  mkdir -p test
+  cd test
+  ../hz new --idl=$thriftIDL --mod=$moduleName -f --model_dir=hertz_model --handler_dir=hertz_handler --router_dir=hertz_router
+  judge_exit "$?"
+  go mod tidy && go build .
+  judge_exit "$?"
+  ../hz update --idl=$thriftIDL
+  judge_exit "$?"
+  ../hz model --idl=$thriftIDL --model_dir=hertz_model
+  judge_exit "$?"
+  ../hz client --idl=$thriftIDL --client_dir=hertz_client
+  judge_exit "$?"
+  cd ..
+  rm -rf test
+}
+
+test_protobuf2() {
+  # test protobuf2
+  mkdir -p test
+  cd test
+  ../hz new -I=$protoSearch -I=$proto2Search --idl=$protobuf2IDL --mod=$moduleName -f --model_dir=hertz_model --handler_dir=hertz_handler --router_dir=hertz_router
+  judge_exit "$?"
+  go mod tidy && go build .
+  judge_exit "$?"
+  ../hz update -I=$protoSearch -I=$proto2Search --idl=$protobuf2IDL
+  judge_exit "$?"
+  ../hz model -I=$protoSearch -I=$proto2Search --idl=$protobuf2IDL --model_dir=hertz_model
+  judge_exit "$?"
+  ../hz client -I=$protoSearch -I=$proto2Search --idl=$protobuf2IDL --client_dir=hertz_client
+  judge_exit "$?"
+  cd ..
+  rm -rf test
+}
+
+test_protobuf3() {
+  # test protobuf2
+  mkdir -p test
+  cd test
+  ../hz new -I=$protoSearch -I=$proto3Search --idl=$protobuf3IDL --mod=$moduleName -f --model_dir=hertz_model --handler_dir=hertz_handler --router_dir=hertz_router
+  judge_exit "$?"
+  go mod tidy && go build .
+  judge_exit "$?"
+  ../hz update -I=$protoSearch -I=$proto3Search --idl=$protobuf3IDL
+  judge_exit "$?"
+  ../hz model -I=$protoSearch -I=$proto3Search --idl=$protobuf3IDL --model_dir=hertz_model
+  judge_exit "$?"
+  ../hz client -I=$protoSearch -I=$proto3Search --idl=$protobuf3IDL --client_dir=hertz_client
+  judge_exit "$?"
+  cd ..
+  rm -rf test
+}
+
+main() {
+  compile_hz
+  judge_exit "$?"
+  install_dependent_tools
+  judge_exit "$?"
+# todo: add thrift test when thriftgo fixed windows
+  echo "test thrift......"
+  test_thrift
+  judge_exit "$?"
+  echo "test protobuf2......"
+  test_protobuf2
+  judge_exit "$?"
+  echo "test protobuf3......"
+  test_protobuf3
+  judge_exit "$?"
+  echo "hz execute success"
+}
+main
diff --git a/cmd/hz/testdata/protobuf2/api.proto b/cmd/hz/testdata/protobuf2/api.proto
new file mode 100644
index 0000000..15330da
--- /dev/null
+++ b/cmd/hz/testdata/protobuf2/api.proto
@@ -0,0 +1,57 @@
+syntax = "proto2";
+
+package api;
+
+import "google/protobuf/descriptor.proto";
+
+option go_package = "github.com/cloudwego/hertz/cmd/hz/test/hertz_model/api";
+
+extend google.protobuf.FieldOptions {
+  optional string raw_body = 50101;
+  optional string query = 50102;
+  optional string header = 50103;
+  optional string cookie = 50104;
+  optional string body = 50105;
+  optional string path = 50106;
+  optional string vd = 50107;
+  optional string form = 50108;
+  optional string go_tag = 51001;
+  optional string js_conv = 50109;
+  optional string file_name = 50110;
+}
+
+extend google.protobuf.MethodOptions {
+  optional string get = 50201;
+  optional string post = 50202;
+  optional string put = 50203;
+  optional string delete = 50204;
+  optional string patch = 50205;
+  optional string options = 50206;
+  optional string head = 50207;
+  optional string any = 50208;
+  optional string re_get = 50211;
+  optional string re_post = 50212;
+  optional string re_put = 50213;
+  optional string re_delete = 50214;
+  optional string re_patch = 50215;
+  optional string re_options = 50216;
+  optional string re_head = 50217;
+  optional string re_any = 50218;
+  optional string gen_path = 50301;
+  optional string api_version = 50302;
+  optional string tag = 50303;
+  optional string name = 50304;
+  optional string api_level = 50305;
+  optional string serializer = 50306;
+  optional string param = 50307;
+  optional string baseurl = 50308;
+  optional string handler_path = 50309;
+}
+
+extend google.protobuf.EnumValueOptions {
+  optional int32 http_code = 50401;
+}
+
+extend google.protobuf.ServiceOptions {
+  optional string base_domain = 50402;
+}
\ No newline at end of file
diff --git a/cmd/hz/testdata/protobuf2/other/other.proto b/cmd/hz/testdata/protobuf2/other/other.proto
new file mode 100644
index 0000000..d5d883d
--- /dev/null
+++ b/cmd/hz/testdata/protobuf2/other/other.proto
@@ -0,0 +1,12 @@
+syntax = "proto2";
+
+package hertz.other;
+
+import "other/other_base.proto";
+
+option go_package = "github.com/cloudwego/hertz/cmd/hz/test/hertz_model/other";
+
+message OtherType {
+  optional string IsBaseString = 1;
+  optional OtherBaseType IsOtherBaseType = 2;
+}
\ No newline at end of file
diff --git a/cmd/hz/testdata/protobuf2/other/other_base.proto b/cmd/hz/testdata/protobuf2/other/other_base.proto
new file mode 100644
index 0000000..32c4b32
--- /dev/null
+++ b/cmd/hz/testdata/protobuf2/other/other_base.proto
@@ -0,0 +1,9 @@
+syntax = "proto2";
+
+package hertz.other;
+
+option go_package = "github.com/cloudwego/hertz/cmd/hz/test/hertz_model/other";
+
+message OtherBaseType {
+  optional string IsOtherBaseTypeString = 1;
+}
\ No newline at end of file
diff --git a/cmd/hz/testdata/protobuf2/psm/base.proto b/cmd/hz/testdata/protobuf2/psm/base.proto
new file mode 100644
index 0000000..6489492
--- /dev/null
+++ b/cmd/hz/testdata/protobuf2/psm/base.proto
@@ -0,0 +1,14 @@
+syntax = "proto2";
+
+package base;
+
+option go_package = "github.com/cloudwego/hertz/cmd/hz/test/hertz_model/psm";
+
+message Base {
+  optional string IsBaseString = 1;
+}
+
+enum BaseEnumType {
+  TWEET = 0;
+  RETWEET = 1;
+}
\ No newline at end of file
diff --git a/cmd/hz/testdata/protobuf2/psm/psm.proto b/cmd/hz/testdata/protobuf2/psm/psm.proto
new file mode 100644
index 0000000..0561d60
--- /dev/null
+++ b/cmd/hz/testdata/protobuf2/psm/psm.proto
@@ -0,0 +1,148 @@
+syntax = "proto2";
+
+package psm;
+
+option go_package = "github.com/cloudwego/hertz/cmd/hz/test/hertz_model/psm";
+
+import "api.proto";
+import "base.proto";
+import "other/other.proto";
+
+enum EnumType {
+    TWEET = 0;
+    RETWEET = 1;
+}
+message UnusedMessageType {
+  optional string IsUnusedMessageType = 1;
+}
+
+message BaseType {
+  optional base.Base IsBaseType = 1;
+}
+
+message MultiTypeReq {
+  // basic type (leading comments)
+  optional bool IsBoolOpt = 1;
+  required bool IsBoolReq = 2;
+  optional int32 IsInt32Opt = 3;
+  required int32 IsInt32Req = 4;
+  optional int64 IsInt64Opt = 5;
+  optional uint32 IsUInt32Opt = 6;
+  optional uint64 IsUInt64Opt = 7;
+  optional sint32 IsSInt32Opt = 8;
+  optional sint64 IsSInt64Opt = 9;
+  optional fixed32 IsFix32Opt = 10;
+  optional fixed64 IsFix64Opt = 11;
+  optional sfixed32 IsSFix32Opt = 12;
+  optional sfixed64 IsSFix64Opt = 13;
+  optional double IsDoubleOpt = 14;
+  required double IsDoubleReq = 15;
+  optional float IsFloatOpt = 16;
+  optional string IsStringOpt = 17;
+  required string IsStringReq = 18;
+  optional bytes IsBytesOpt = 19;
+  optional bytes IsBytesReq = 20;
+
+  // slice
+  repeated string IsRepeatedString = 21;
+  repeated BaseType IsRepeatedBaseType = 22;
+
+  // map
+  map<string, string> IsStringMap = 23;
+  map<string, BaseType> IsBaseTypeMap = 24;
+
+  // oneof
+  // multiple comments
+  oneof TestOneof {
+    string IsOneofString = 25;
+    BaseType IsOneofBaseType = 26;
+    int32 IsOneofInt = 100;
+    bool IsOneofBool = 101;
+    double IsOneoDouble = 102;
+    bytes IsOneofBytes = 103;
+  }
+
+  // this is oneof2, one field in oneof
+  oneof TestOneof2 {
+    string IsOneof2String = 104;
+  }
+
+  message NestedMessageType {
+    optional string IsNestedString = 1;
+    optional BaseType IsNestedBaseType = 2;
+    repeated BaseType IsNestedRepeatedBaseType = 3;
+    // nested oneof
+    oneof NestedMsgOneof {
+      string IsNestedMsgOneofString = 4;
+      EnumType IsNestedMsgOneofEnumType =5;
+    }
+  }
+  // nested message
+  optional NestedMessageType IsNestedType = 27;
+
+  // other dependency
+  optional base.Base IsCurrentPackageBase = 28;
+  optional hertz.other.OtherType IsOtherType = 29;
+
+  // enum
+  optional EnumType IsEnumTypeOpt = 30;
+  required EnumType IsEnumTypeReq = 31;
+  repeated EnumType IsEnumTypeList = 32;
+  optional base.BaseEnumType IsBaseEnumType = 33;
+}
+
+message MultiTagReq {
+  optional string QueryTag = 1 [(api.query)="query"];
+  optional string RawBodyTag = 2 [(api.raw_body)="raw_body"];
+  optional string CookieTag = 3 [(api.cookie)="cookie"];
+  optional string BodyTag = 4 [(api.body)="body"];
+  optional string PathTag = 5 [(api.path)="path"];
+  optional string VdTag = 6 [(api.vd)="$!='?'"];
+  optional string FormTag = 7 [(api.form)="form"];
+  optional string DefaultTag = 8 [(api.go_tag)="FFF:\"fff\" json:\"json\""];
+}
+
+message Resp {
+  optional string Resp = 1;
+}
+
+message MultiNameStyleMessage {
+  optional string hertz = 1;
+  optional string Hertz = 2;
+  optional string hertz_demo = 3;
+  optional string hertz_demo_idl = 4;
+  optional string hertz_Idl = 5;
+  optional string hertzDemo = 6;
+  optional string h = 7;
+  optional string H = 8;
+  optional string hertz_ = 9;
+}
+
+service Hertz {
+  rpc Method1(MultiTypeReq) returns(Resp) {
+    option (api.get)="/company/department/group/user:id/name";
+  }
+  rpc Method2(MultiTypeReq) returns(Resp) {
+    option (api.post)="/company/department/group/user:id/sex";
+  }
+  rpc Method3(MultiTypeReq) returns(Resp) {
+    option (api.put)="/company/department/group/user:id/number";
+  }
+  rpc Method4(MultiTypeReq) returns(Resp) {
+    option (api.delete)="/company/department/group/user:id/age";
+  }
+
+
+  rpc Method5(MultiTagReq) returns(Resp) {
+    option (api.options)="/school/class/student/name";
+  }
+  rpc Method6(MultiTagReq) returns(Resp) {
+    option (api.head)="/school/class/student/number";
+  }
+  rpc Method7(MultiTagReq) returns(Resp) {
+    option (api.patch)="/school/class/student/sex";
+  }
+  rpc Method8(MultiTagReq) returns(Resp) {
+    option (api.any)="/school/class/student/grade/*subjects";
+  }
+}
\ No newline at end of file
diff --git a/cmd/hz/testdata/protobuf3/api.proto b/cmd/hz/testdata/protobuf3/api.proto
new file mode 100644
index 0000000..15330da
--- /dev/null
+++ b/cmd/hz/testdata/protobuf3/api.proto
@@ -0,0 +1,57 @@
+syntax = "proto2";
+
+package api;
+
+import "google/protobuf/descriptor.proto";
+
+option go_package = "github.com/cloudwego/hertz/cmd/hz/test/hertz_model/api";
+
+extend google.protobuf.FieldOptions {
+  optional string raw_body = 50101;
+  optional string query = 50102;
+  optional string header = 50103;
+  optional string cookie = 50104;
+  optional string body = 50105;
+  optional string path = 50106;
+  optional string vd = 50107;
+  optional string form = 50108;
+  optional string go_tag = 51001;
+  optional string js_conv = 50109;
+  optional string file_name = 50110;
+}
+
+extend google.protobuf.MethodOptions {
+  optional string get = 50201;
+  optional string post = 50202;
+  optional string put = 50203;
+  optional string delete = 50204;
+  optional string patch = 50205;
+  optional string options = 50206;
+  optional string head = 50207;
+  optional string any = 50208;
+  optional string re_get = 50211;
+  optional string re_post = 50212;
+  optional string re_put = 50213;
+  optional string re_delete = 50214;
+  optional string re_patch = 50215;
+  optional string re_options = 50216;
+  optional string re_head = 50217;
+  optional string re_any = 50218;
+  optional string gen_path = 50301;
+  optional string api_version = 50302;
+  optional string tag = 50303;
+  optional string name = 50304;
+  optional string api_level = 50305;
+  optional string serializer = 50306;
+  optional string param = 50307;
+  optional string baseurl = 50308;
+  optional string handler_path = 50309;
+}
+
+extend google.protobuf.EnumValueOptions {
+  optional int32 http_code = 50401;
+}
+
+extend google.protobuf.ServiceOptions {
+  optional string base_domain = 50402;
+}
\ No newline at end of file
diff --git a/cmd/hz/testdata/protobuf3/other/other.proto b/cmd/hz/testdata/protobuf3/other/other.proto
new file mode 100644
index 0000000..d5d883d
--- /dev/null
+++ b/cmd/hz/testdata/protobuf3/other/other.proto
@@ -0,0 +1,12 @@
+syntax = "proto2";
+
+package hertz.other;
+
+import "other/other_base.proto";
+
+option go_package = "github.com/cloudwego/hertz/cmd/hz/test/hertz_model/other";
+
+message OtherType {
+  optional string IsBaseString = 1;
+  optional OtherBaseType IsOtherBaseType = 2;
+}
\ No newline at end of file
diff --git a/cmd/hz/testdata/protobuf3/other/other_base.proto b/cmd/hz/testdata/protobuf3/other/other_base.proto
new file mode 100644
index 0000000..32c4b32
--- /dev/null
+++ b/cmd/hz/testdata/protobuf3/other/other_base.proto
@@ -0,0 +1,9 @@
+syntax = "proto2";
+
+package hertz.other;
+
+option go_package = "github.com/cloudwego/hertz/cmd/hz/test/hertz_model/other";
+
+message OtherBaseType {
+  optional string IsOtherBaseTypeString = 1;
+}
\ No newline at end of file
diff --git a/cmd/hz/testdata/protobuf3/psm/base.proto b/cmd/hz/testdata/protobuf3/psm/base.proto
new file mode 100644
index 0000000..ea8891a
--- /dev/null
+++ b/cmd/hz/testdata/protobuf3/psm/base.proto
@@ -0,0 +1,9 @@
+syntax = "proto2";
+
+package base;
+
+option go_package = "github.com/cloudwego/hertz/cmd/hz/test/hertz_model/psm";
+
+message Base {
+  optional string IsBaseString = 1;
+}
\ No newline at end of file
diff --git a/cmd/hz/testdata/protobuf3/psm/psm.proto b/cmd/hz/testdata/protobuf3/psm/psm.proto
new file mode 100644
index 0000000..972a85c
--- /dev/null
+++ b/cmd/hz/testdata/protobuf3/psm/psm.proto
@@ -0,0 +1,126 @@
+syntax = "proto3";
+
+package psm;
+
+option go_package = "github.com/cloudwego/hertz/cmd/hz/test/hertz_model/psm";
+
+import "api.proto";
+import "base.proto";
+import "other/other.proto";
+
+enum EnumType {
+    TWEET = 0;
+    RETWEET = 1;
+}
+message UnusedMessageType {
+  optional string IsUnusedMessageType = 1;
+}
+
+message BaseType {
+  optional base.Base IsBaseType = 1;
+}
+
+message MultiTypeReq {
+  // basic type (leading comments)
+  optional bool IsBoolOpt = 1;
+  optional int32 IsInt32Opt = 3;
+  int64 IsInt64Default = 5;
+  optional uint32 IsUInt32Opt = 6;
+  uint64 IsUInt64Default = 7;
+  optional sint32 IsSInt32Opt = 8;
+  sint64 IsSInt64Default = 9;
+  optional fixed32 IsFix32Opt = 10;
+  optional fixed64 IsFix64Opt = 11;
+  optional sfixed32 IsSFix32Opt = 12;
+  optional sfixed64 IsSFix64Opt = 13;
+  optional double IsDoubleOpt = 14;
+  optional float IsFloatOpt = 16;
+  optional string IsStringOpt = 17;
+  optional bytes IsBytesOpt = 19;
+  bytes IsBytesDefault = 20;
+
+  // slice
+  repeated string IsRepeatedString = 21;
+  repeated BaseType IsRepeatedBaseType = 22;
+
+  // map
+  map<string, string> IsStringMap = 23;
+  map<string, BaseType> IsBaseTypeMap = 24;
+
+  // oneof
+  oneof TestOneof {
+    string IsOneofString = 25;
+    BaseType IsOneofBaseTypeString = 26;
+  }
+
+  oneof TestOneof2 {
+    string IsOneofString2 = 100;
+  }
+
+  // nested message
+  message NestedMessageType {
+    oneof NestedOneof {
+      string YYY = 4;
+      string GGG = 5;
+    }
+    optional string IsNestedString = 1;
+    optional BaseType IsNestedBaseType = 2;
+    repeated BaseType IsNestedRepeatedBaseType = 3;
+  }
+  optional NestedMessageType IsNestedType = 27;
+
+  // other dependency
+  optional base.Base IsCurrentPackageBase = 28;
+  optional hertz.other.OtherType IsOtherType = 29;
+
+  // enum
+  optional EnumType IsEnumTypeOpt = 30;
+  EnumType IsEnumTypeDefault = 31;
+}
+
+message MultiTagReq {
+  optional string QueryTag = 1 [(api.query)="query"];
+  optional string RawBodyTag = 2 [(api.raw_body)="raw_body"];
+  optional string CookieTag = 3 [(api.cookie)="cookie"];
+  optional string BodyTag = 4 [(api.body)="body"];
+  optional string PathTag = 5 [(api.path)="path"];
+  optional string VdTag = 6 [(api.vd)="$!='?'"];
+  optional string DefaultTag = 7;
+  oneof TestOneof {
+    string IsOneofString = 25;
+    BaseType IsOneofBaseTypeString = 26;
+  }
+}
+
+message Resp {
+  optional string Resp = 1;
+}
+
+service Hertz {
+  rpc Method1(MultiTypeReq) returns(Resp) {
+    option (api.get)="/company/department/group/user:id/name";
+  }
+  rpc Method2(MultiTypeReq) returns(Resp) {
+    option (api.post)="/company/department/group/user:id/sex";
+  }
+  rpc Method3(MultiTypeReq) returns(Resp) {
+    option (api.put)="/company/department/group/user:id/number";
+  }
+  rpc Method4(MultiTypeReq) returns(Resp) {
+    option (api.delete)="/company/department/group/user:id/age";
+  }
+
+
+  rpc Method5(MultiTagReq) returns(Resp) {
+    option (api.options)="/school/class/student/name";
+  }
+  rpc Method6(MultiTagReq) returns(Resp) {
+    option (api.head)="/school/class/student/number";
+  }
+  rpc Method7(MultiTagReq) returns(Resp) {
+    option (api.patch)="/school/class/student/sex";
+  }
+  rpc Method8(MultiTagReq) returns(Resp) {
+    option (api.any)="/school/class/student/grade/*subjects";
+  }
+}
\ No newline at end of file
diff --git a/cmd/hz/testdata/thrift/common.thrift b/cmd/hz/testdata/thrift/common.thrift
new file mode 100644
index 0000000..4efbb82
--- /dev/null
+++ b/cmd/hz/testdata/thrift/common.thrift
@@ -0,0 +1,13 @@
+namespace go toutiao.middleware.hertz
+
+struct CommonType {
+    1: required string IsCommonString;
+    2: optional string TTT;
+    3: required bool HHH;
+    4: required Base GGG;
+}
+
+struct Base {
+    1: optional string AAA;
+    2: optional i32 BBB;
+}
\ No newline at end of file
diff --git a/cmd/hz/testdata/thrift/data/basic_data.thrift b/cmd/hz/testdata/thrift/data/basic_data.thrift
new file mode 100644
index 0000000..70f6b5a
--- /dev/null
+++ b/cmd/hz/testdata/thrift/data/basic_data.thrift
@@ -0,0 +1,5 @@
+namespace go toutiao.middleware.hertz_data
+
+struct BasicDataType {
+    1: optional string IsBasicDataString;
+}
\ No newline at end of file
diff --git a/cmd/hz/testdata/thrift/data/data.thrift b/cmd/hz/testdata/thrift/data/data.thrift
new file mode 100644
index 0000000..a0b7615
--- /dev/null
+++ b/cmd/hz/testdata/thrift/data/data.thrift
@@ -0,0 +1,7 @@
+include "basic_data.thrift"
+
+namespace go toutiao.middleware.hertz_data
+
+struct DataType {
+    1: optional basic_data.BasicDataType IsDataString;
+}
\ No newline at end of file
diff --git a/cmd/hz/testdata/thrift/psm.thrift b/cmd/hz/testdata/thrift/psm.thrift
new file mode 100644
index 0000000..5baedc8
--- /dev/null
+++ b/cmd/hz/testdata/thrift/psm.thrift
@@ -0,0 +1,117 @@
+include "common.thrift"
+include "data/data.thrift"
+
+namespace go toutiao.middleware.hertz
+
+const string STRING_CONST = "hertz";
+
+enum EnumType {
+    TWEET,
+    RETWEET = 2,
+}
+
+typedef i32 MyInteger
+
+struct BaseType {
+    1: string GoTag = "test" (go.tag="json:\"go\" goTag:\"tag\"");
+    2: optional string IsBaseString = "test";
+    3: optional common.CommonType IsDepCommonType = {"IsCommonString":"test", "TTT":"test", "HHH":true, "GGG": {"AAA":"test","BBB":32}};
+    4: optional EnumType IsBaseTypeEnum = 1;
+}
+
+typedef common.CommonType FFF
+
+typedef BaseType MyBaseType
+
+struct MultiTypeReq {
+    // basic type (leading comments)
+    1: optional bool IsBoolOpt = true; // trailing comments
+    2: required bool IsBoolReq;
+    3: optional byte IsByteOpt = 8;
+    4: required byte IsByteReq;
+    //5: optional i8 IsI8Opt; // unsupported i8, suggest byte
+    //6: required i8 IsI8Req = 5; // default
+    7: optional i16 IsI16Opt = 16;
+    8: optional i32 IsI32Opt;
+    9: optional i64 IsI64Opt;
+    10: optional double IsDoubleOpt;
+    11: required double IsDoubleReq;
+    12: optional string IsStringOpt = "test";
+    13: required string IsStringReq;
+
+    14: optional list<string> IsList;
+    22: required list<string> IsListReq;
+    15: optional set<string> IsSet;
+    16: optional map<string, string> IsMap;
+    21: optional map<string, BaseType> IsStructMap;
+
+    // struct type
+    17: optional BaseType IsBaseType; // use struct name
+    18: optional MyBaseType IsMyBaseType; // use typedef for struct
+    19: optional common.CommonType IsCommonType = {"IsCommonString": "fffff"};
+    20: optional data.DataType IsDataType; // multi-dependent struct
+}
+
+typedef data.DataType IsMyDataType
+
+struct MultiTagReq {
+    1: string QueryTag (api.query="query");
+    2: string RawBodyTag (api.raw_body="raw_body");
+    3: string PathTag (api.path="path");
+    4: string FormTag (api.form="form");
+    5: string CookieTag (api.cookie="cookie");
+    6: string HeaderTag (api.header="header");
+    7: string ProtobufTag (api.protobuf="protobuf");
+    8: string BodyTag (api.body="body");
+    9: string GoTag (go.tag="json:\"go\" goTag:\"tag\"");
+    10: string VdTag (api.vd="$!='?'");
+    11: string DefaultTag;
+}
+
+struct Resp {
+    1: string Resp = "this is Resp";
+}
+
+struct MultiNameStyleReq {
+  1: optional string hertz;
+  2: optional string Hertz;
+  3: optional string hertz_demo;
+  4: optional string hertz_demo_idl;
+  5: optional string hertz_Idl;
+  6: optional string hertzDemo;
+  7: optional string h;
+  8: optional string H;
+  9: optional string hertz_;
+}
+
+struct MultiDefaultReq {
+  1: optional bool IsBoolOpt = true;
+  2: required bool IsBoolReq = false;
+  3: optional i32 IsI32Opt = 32;
+  4: required i32 IsI32Req = 32;
+  5: optional string IsStringOpt = "test";
+  6: required string IsStringReq = "test";
+
+  14: optional list<string> IsListOpt = ["test", "ttt", "sdsds"];
+  22: required list<string> IsListReq = ["test", "ttt", "sdsds"];
+  15: optional set<string> IsSet = ["test", "ttt", "sdsds"];
+  16: optional map<string, string> IsMapOpt = {"test": "ttt", "ttt": "lll"};
+  17: required map<string, string> IsMapReq = {"test": "ttt", "ttt": "lll"};
+  21: optional map<string, BaseType> IsStructMapOpt = {"test": {"GoTag":"fff", "IsBaseTypeEnum":1, "IsBaseString":"ddd", "IsDepCommonType": {"IsCommonString":"fffffff", "TTT":"ttt", "HHH":true, "GGG": {"AAA":"test","BBB":32}}}};
+  25: required map<string, BaseType> IsStructMapReq = {"test": {"GoTag":"fff", "IsBaseTypeEnum":1, "IsBaseString":"ddd", "IsDepCommonType": {"IsCommonString":"fffffff", "TTT":"ttt", "HHH":true, "GGG": {"AAA":"test","BBB":32}}}};
+
+  23: optional common.CommonType IsDepCommonTypeOpt = {"IsCommonString":"fffffff", "TTT":"ttt", "HHH":true, "GGG": {"AAA":"test","BBB":32}};
+  24: required common.CommonType IsDepCommonTypeReq = {"IsCommonString":"fffffff", "TTT":"ttt", "HHH":true, "GGG": {"AAA":"test","BBB":32}};
+}
+
+service Hertz {
+    Resp Method1(1: MultiTypeReq request) (api.get="/company/department/group/user:id/name", api.handler_path="v1");
+    Resp Method2(1: MultiTagReq request) (api.post="/company/department/group/user:id/sex", api.handler_path="v1");
+    Resp Method3(1: BaseType request) (api.put="/company/department/group/user:id/number", api.handler_path="v1");
+    Resp Method4(1: data.DataType request) (api.delete="/company/department/group/user:id/age", api.handler_path="v1");
+
+    Resp Method5(1: MultiTypeReq request) (api.options="/school/class/student/name", api.handler_path="v2");
+    Resp Method6(1: MultiTagReq request) (api.head="/school/class/student/number", api.handler_path="v2");
+    Resp Method7(1: MultiTagReq request) (api.patch="/school/class/student/sex", api.handler_path="v2");
+    Resp Method8(1: BaseType request) (api.any="/school/class/student/grade/*subjects", api.handler_path="v2");
+}
\ No newline at end of file
diff --git a/pkg/app/client/client_test.go b/pkg/app/client/client_test.go
index b6eb263..8862d84 100644
--- a/pkg/app/client/client_test.go
+++ b/pkg/app/client/client_test.go
@@ -1951,6 +1951,58 @@ func TestClientReadResponseBodyStreamWithDoubleRequest(t *testing.T) {
 	}
 }
 
+func TestClientReadResponseBodyStreamWithConnectionClose(t *testing.T) {
+	part1 := ""
+	for i := 0; i < 8192; i++ {
+		part1 += "a"
+	}
+
+	opt := config.NewOptions([]config.Option{})
+	opt.Addr = "127.0.0.1:10036"
+	engine := route.NewEngine(opt)
+	engine.POST("/", func(ctx context.Context, c *app.RequestContext) {
+		c.String(consts.StatusOK, part1)
+	})
+	go engine.Run()
+	time.Sleep(100 * time.Millisecond)
+
+	client, _ := NewClient(WithResponseBodyStream(true))
+
+	// first req
+	req, resp := protocol.AcquireRequest(), protocol.AcquireResponse()
+	defer func() {
+		protocol.ReleaseRequest(req)
+		protocol.ReleaseResponse(resp)
+	}()
+	req.SetConnectionClose()
+	req.SetMethod(consts.MethodPost)
+	req.SetRequestURI("http://127.0.0.1:10036")
+
+	err := client.Do(context.Background(), req, resp)
+	if err != nil {
+		t.Fatalf("client Do error=%v", err.Error())
+	}
+
+	assert.DeepEqual(t, part1, string(resp.Body()))
+
+	// second req
+	req1, resp1 := protocol.AcquireRequest(), protocol.AcquireResponse()
+	defer func() {
+		protocol.ReleaseRequest(req1)
+		protocol.ReleaseResponse(resp1)
+	}()
+	req1.SetConnectionClose()
+	req1.SetMethod(consts.MethodPost)
+	req1.SetRequestURI("http://127.0.0.1:10036")
+
+	err = client.Do(context.Background(), req1, resp1)
+	if err != nil {
+		t.Fatalf("client Do error=%v", err.Error())
+	}
+
+	assert.DeepEqual(t, part1, string(resp1.Body()))
+}
+
 type mockDialer struct {
 	network.Dialer
 	customDialerFunc func(network, address string, timeout time.Duration, tlsConfig *tls.Config)
diff --git a/pkg/common/bytebufferpool/pool_test.go b/pkg/common/bytebufferpool/pool_test.go
index 1c2bc7e..0a26197 100644
--- a/pkg/common/bytebufferpool/pool_test.go
+++ b/pkg/common/bytebufferpool/pool_test.go
@@ -97,7 +97,7 @@ func TestPoolVariousSizesConcurrent(t *testing.T) {
 	for i := 0; i < concurrency; i++ {
 		select {
 		case <-ch:
-		case <-time.After(3 * time.Second):
+		case <-time.After(10 * time.Second):
 			t.Fatalf("timeout")
 		}
 	}
diff --git a/pkg/common/ut/context_test.go b/pkg/common/ut/context_test.go
new file mode 100644
index 0000000..023945c
--- /dev/null
+++ b/pkg/common/ut/context_test.go
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2023 CloudWeGo Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ut
+
+import (
+	"bytes"
+	"testing"
+
+	"github.com/cloudwego/hertz/pkg/common/test/assert"
+)
+
+func TestCreateUtRequestContext(t *testing.T) {
+	body := "1"
+	method := "PUT"
+	path := "/hey/dy"
+	headerKey := "Connection"
+	headerValue := "close"
+	ctx := CreateUtRequestContext(method, path, &Body{bytes.NewBufferString(body), len(body)},
+		Header{headerKey, headerValue})
+
+	assert.DeepEqual(t, method, string(ctx.Method()))
+	assert.DeepEqual(t, path, string(ctx.Path()))
+	body1, err := ctx.Body()
+	assert.DeepEqual(t, nil, err)
+	assert.DeepEqual(t, body, string(body1))
+	assert.DeepEqual(t, headerValue, string(ctx.GetHeader(headerKey)))
+}
diff --git a/pkg/network/standard/connection_test.go b/pkg/network/standard/connection_test.go
index 9b0fafd..6fcc456 100644
--- a/pkg/network/standard/connection_test.go
+++ b/pkg/network/standard/connection_test.go
@@ -22,11 +22,14 @@ import (
 	"errors"
 	"io"
 	"net"
+	"runtime"
 	"strings"
+	"sync/atomic"
 	"syscall"
 	"testing"
 	"time"
 
+	. "github.com/bytedance/mockey"
 	"github.com/cloudwego/hertz/pkg/common/test/assert"
 )
 
@@ -145,11 +148,11 @@ func TestPeekRelease(t *testing.T) {
 	}
 
 	// test cross node
-	b, _ = conn.Peek(100000000)
-	if len(b) != 100000000 {
+	b, _ = conn.Peek(1000000)
+	if len(b) != 1000000 {
 		t.Errorf("unexpected len(b): %v, expected 1", len(b))
 	}
-	conn.Skip(100000000)
+	conn.Skip(1000000)
 	conn.Release()
 
 	// test maxSize
@@ -184,7 +187,7 @@ func TestReadBytes(t *testing.T) {
 	}
 	bbb, _ := conn.ReadByte()
 	if bbb != 0 {
-		t.Errorf("unexpected bbb: %v, expected nil", bbb)
+		t.Errorf("unexpected bbb: %v, expected nil", string(bbb))
 	}
 	if conn.Len() != 4094 {
 		t.Errorf("unexpected conn.Len: %v, expected 4094", conn.Len())
@@ -286,6 +289,9 @@ func (m *mockConn) ConnectionState() tls.ConnectionState {
 
 func (m mockConn) Read(b []byte) (n int, err error) {
 	length := len(b)
+	for i := 0; i < length; i++ {
+		b[i] = 0
+	}
 	if length > 8192 {
 		return 8192, nil
 	}
@@ -342,3 +348,33 @@ func (m *mockAddr) Network() string {
 func (m *mockAddr) String() string {
 	return m.address
 }
+
+var release_count uint32 = 0
+
+func mockLinkBufferNodeRelease(b *linkBufferNode) {
+	atomic.AddUint32(&release_count, 1)
+
+	if !b.readOnly {
+		free(b.buf)
+	}
+	b.readOnly = false
+	b.buf = nil
+	b.next = nil
+	b.malloc, b.off = 0, 0
+	bufferPool.Put(b)
+}
+
+func TestConnSetFinalizer(t *testing.T) {
+	runtime.GC()
+	time.Sleep(time.Millisecond * 100)
+
+	Mock((*linkBufferNode).Release).To(mockLinkBufferNodeRelease).Build()
+
+	atomic.StoreUint32(&release_count, 0)
+	_ = newConn(&mockConn{}, 4096)
+
+	runtime.GC()
+	time.Sleep(time.Millisecond * 100)
+
+	assert.DeepEqual(t, uint32(2), atomic.LoadUint32(&release_count))
+}