Repository: cloudwego/hertz
Pull request #784 (2023-05-16 13:21:30)

optimize(hz): use sprig to add template function
#### What type of PR is this?
<!--
Add one of the following kinds:

build: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
ci: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
docs: Documentation only changes
feat: A new feature
optimize: A new optimization
fix: A bug fix
perf: A code change that improves performance
refactor: A code change that neither fixes a bug nor adds a feature
style: Changes that do not affect the meaning of the code (white space, formatting, missing semi-colons, etc)
test: Adding missing tests or correcting existing tests
chore: Changes to the build process or auxiliary tools and libraries such as documentation generation
-->

optimize

#### Check the PR title.
<!--
The description of the title will be attached in Release Notes, 
so please describe it from user-oriented, what this PR does / why we need it.
Please check your PR title with the below requirements:
-->
- [x] This PR title match the format: `<type>(optional scope): <description>`.
- [x] The description of this PR title is user-oriented and clear enough for others to understand.
- [x] Attach the PR updating the user documentation if the current PR requires user awareness at the usage level. [User docs repo](https://github.com/cloudwego/cloudwego.github.io).

#### (Optional) Translate the PR title into Chinese.

- 使用 sprig 添加更多的模版函数


#### (Optional) Which issue(s) this PR fixes:
<!--
Automatically closes linked issue when PR is merged.
Eg: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
-->

#### (Optional) The PR that updates user documentation:
<!--
If the current PR requires user awareness at the usage level, please submit a PR to update user docs. [User docs repo](https://github.com/cloudwego/cloudwego.github.io)
-->


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/cloudwego/hertz/pull/784?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego) Report
Patch coverage has no change and project coverage change: **`-0.12`** :warning:
> Comparison is base [(`62aa1a0`)](https://app.codecov.io/gh/cloudwego/hertz/commit/62aa1a08287e41d21495a92c194c1bd41c262bd0?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego) 75.66% compared to head [(`8927150`)](https://app.codecov.io/gh/cloudwego/hertz/pull/784?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego) 75.55%.

> :exclamation: Current head 8927150 differs from pull request most recent head c18e6d9. Consider uploading reports for the commit c18e6d9 to get more accurate results

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


```diff
@@             Coverage Diff             @@
##           develop     #784      +/-   ##
===========================================
- Coverage    75.66%   75.55%   -0.12%     
===========================================
  Files           97       97              
  Lines         9477     9477              
===========================================
- Hits          7171     7160      -11     
- Misses        1856     1866      +10     
- Partials       450      451       +1     
```


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


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/cloudwego/hertz/pull/784?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego).

welkeyever: Need to add some information to explain the necessity of introducing this dependency.

Patch:
diff --git a/cmd/hz/generator/handler.go b/cmd/hz/generator/handler.go
index 4d797d2..2a949c5 100644
--- a/cmd/hz/generator/handler.go
+++ b/cmd/hz/generator/handler.go
@@ -44,6 +44,7 @@ type HttpMethod struct {
 	RefPackage         string // handler import dir
 	RefPackageAlias    string // handler import alias
 	ModelPackage       map[string]string
+	GenHandler         bool // Whether to generate one handler, when an idl interface corresponds to multiple http method
 	// Annotations     map[string]string
 	Models map[string]*model.Model
 }
@@ -78,8 +79,10 @@ func (pkgGen *HttpPackageGenerator) genHandler(pkg *HttpPackage, handlerDir, han
 					return fmt.Errorf("generate handler %s failed, err: %v", handler.FilePath, err.Error())
 				}
 
-				if err := pkgGen.updateHandler(handler, handlerTplName, handler.FilePath, false); err != nil {
-					return fmt.Errorf("generate handler %s failed, err: %v", handler.FilePath, err.Error())
+				if m.GenHandler {
+					if err := pkgGen.updateHandler(handler, handlerTplName, handler.FilePath, false); err != nil {
+						return fmt.Errorf("generate handler %s failed, err: %v", handler.FilePath, err.Error())
+					}
 				}
 			}
 		} else { // generate handler service
@@ -105,6 +108,15 @@ func (pkgGen *HttpPackageGenerator) genHandler(pkg *HttpPackage, handlerDir, han
 				return fmt.Errorf("generate handler %s failed, err: %v", handler.FilePath, err.Error())
 			}
 
+			// Avoid generating duplicate handlers when IDL interface corresponds to multiple http methods
+			methods := handler.Methods
+			handler.Methods = []*HttpMethod{}
+			for _, m := range methods {
+				if m.GenHandler {
+					handler.Methods = append(handler.Methods, m)
+				}
+			}
+
 			if err := pkgGen.updateHandler(handler, handlerTplName, handler.FilePath, false); err != nil {
 				return fmt.Errorf("generate handler %s failed, err: %v", handler.FilePath, err.Error())
 			}
diff --git a/cmd/hz/generator/template_funcs.go b/cmd/hz/generator/template_funcs.go
index 556ffb1..2d33d72 100644
--- a/cmd/hz/generator/template_funcs.go
+++ b/cmd/hz/generator/template_funcs.go
@@ -20,15 +20,22 @@ import (
 	"strings"
 	"text/template"
 
+	"github.com/Masterminds/sprig/v3"
 	"github.com/cloudwego/hertz/cmd/hz/util"
 )
 
-var funcMap = template.FuncMap{
-	"GetUniqueHandlerOutDir": getUniqueHandlerOutDir,
-	"ToSnakeCase":            util.ToSnakeCase,
-	"Split":                  strings.Split,
-	"Trim":                   strings.Trim,
-}
+var funcMap = func() template.FuncMap {
+	m := template.FuncMap{
+		"GetUniqueHandlerOutDir": getUniqueHandlerOutDir,
+		"ToSnakeCase":            util.ToSnakeCase,
+		"Split":                  strings.Split,
+		"Trim":                   strings.Trim,
+	}
+	for key, f := range sprig.TxtFuncMap() {
+		m[key] = f
+	}
+	return m
+}()
 
 // getUniqueHandlerOutDir uses to get unique "api.handler_path"
 func getUniqueHandlerOutDir(methods []*HttpMethod) (ret []string) {
diff --git a/cmd/hz/go.mod b/cmd/hz/go.mod
index b65eeeb..712c5a2 100644
--- a/cmd/hz/go.mod
+++ b/cmd/hz/go.mod
@@ -3,6 +3,7 @@ module github.com/cloudwego/hertz/cmd/hz
 go 1.16
 
 require (
+	github.com/Masterminds/sprig/v3 v3.2.3
 	github.com/cloudwego/thriftgo v0.1.7
 	github.com/hashicorp/go-version v1.5.0
 	github.com/jhump/protoreflect v1.12.0
diff --git a/cmd/hz/go.sum b/cmd/hz/go.sum
index 23aa603..de5c00d 100644
--- a/cmd/hz/go.sum
+++ b/cmd/hz/go.sum
@@ -1,6 +1,12 @@
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
+github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
+github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
+github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
+github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
+github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
+github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
 github.com/apache/thrift v0.13.0 h1:5hryIiq9gtn+MiLVn0wP37kb/uTeRZgN08WoCsAhIhI=
 github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -11,6 +17,8 @@ github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnht
 github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
 github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
@@ -35,9 +43,15 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 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/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/hashicorp/go-version v1.5.0 h1:O293SZ2Eg+AAYijkVK3jR786Am1bhDEh2GHT0tIVE5E=
 github.com/hashicorp/go-version v1.5.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
+github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4=
+github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
+github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
+github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
 github.com/jhump/gopoet v0.0.0-20190322174617-17282ff210b3/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI=
 github.com/jhump/gopoet v0.1.0/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI=
 github.com/jhump/goprotoc v0.5.0/go.mod h1:VrbvcYrQOrTi3i0Vf+m+oqQWk9l72mjkJCYo7UvLHRQ=
@@ -49,11 +63,22 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
+github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
+github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
+github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
+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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
+github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
+github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
+github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/urfave/cli/v2 v2.23.0 h1:pkly7gKIeYv3olPAeNajNpLjeJrmTPYCoZWaV+2VfvE=
 github.com/urfave/cli/v2 v2.23.0/go.mod h1:1CNUng3PtjQMtRzJO4FMXBQvkGtuYRxxiR9xMa7jMwI=
@@ -63,6 +88,8 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A=
+golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@@ -78,6 +105,7 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
 golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
 golang.org/x/net v0.3.0 h1:VWL6FNY2bEEmsGVKabSlHu5Irp34xmMRoqb/9lF9lxk=
 golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -95,15 +123,18 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
 golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
 golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
 golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -148,6 +179,7 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogR
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/cmd/hz/meta/const.go b/cmd/hz/meta/const.go
index 8e7490d..abda7c7 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.6"
+const Version = "v0.6.7"
 
 const DefaultServiceName = "hertz_service"
 
diff --git a/cmd/hz/protobuf/api/api.pb.go b/cmd/hz/protobuf/api/api.pb.go
index 2c93b92..4c1d8c6 100644
--- a/cmd/hz/protobuf/api/api.pb.go
+++ b/cmd/hz/protobuf/api/api.pb.go
@@ -1,6 +1,6 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.28.1
+// 	protoc-gen-go v1.30.0
 // 	protoc        v3.21.12
 // source: api.proto
 
@@ -318,6 +318,22 @@ var file_api_proto_extTypes = []protoimpl.ExtensionInfo{
 		Tag:           "bytes,50731,opt,name=base_domain_compatible",
 		Filename:      "api.proto",
 	},
+	{
+		ExtendedType:  (*descriptorpb.ServiceOptions)(nil),
+		ExtensionType: (*string)(nil),
+		Field:         50732,
+		Name:          "api.service_path",
+		Tag:           "bytes,50732,opt,name=service_path",
+		Filename:      "api.proto",
+	},
+	{
+		ExtendedType:  (*descriptorpb.MessageOptions)(nil),
+		ExtensionType: (*string)(nil),
+		Field:         50830,
+		Name:          "api.reserve",
+		Tag:           "bytes,50830,opt,name=reserve",
+		Filename:      "api.proto",
+	},
 }
 
 // Extension fields to descriptorpb.FieldOptions.
@@ -414,6 +430,14 @@ var (
 	//
 	// optional string base_domain_compatible = 50731;
 	E_BaseDomainCompatible = &file_api_proto_extTypes[36]
+	// optional string service_path = 50732;
+	E_ServicePath = &file_api_proto_extTypes[37]
+)
+
+// Extension fields to descriptorpb.MessageOptions.
+var (
+	// optional string reserve = 50830;
+	E_Reserve = &file_api_proto_extTypes[38]
 )
 
 var File_api_proto protoreflect.FileDescriptor
@@ -564,7 +588,15 @@ var file_api_proto_rawDesc = []byte{
 	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
 	0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xab, 0x8c, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52,
 	0x14, 0x62, 0x61, 0x73, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x43, 0x6f, 0x6d, 0x70, 0x61,
-	0x74, 0x69, 0x62, 0x6c, 0x65, 0x42, 0x06, 0x5a, 0x04, 0x2f, 0x61, 0x70, 0x69,
+	0x74, 0x69, 0x62, 0x6c, 0x65, 0x3a, 0x44, 0x0a, 0x0c, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
+	0x5f, 0x70, 0x61, 0x74, 0x68, 0x12, 0x1f, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
+	0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4f,
+	0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xac, 0x8c, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b,
+	0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x50, 0x61, 0x74, 0x68, 0x3a, 0x3b, 0x0a, 0x07, 0x72,
+	0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x12, 0x1f, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
+	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
+	0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x8e, 0x8d, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52,
+	0x07, 0x72, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x42, 0x06, 0x5a, 0x04, 0x2f, 0x61, 0x70, 0x69,
 }
 
 var file_api_proto_goTypes = []interface{}{
@@ -572,6 +604,7 @@ var file_api_proto_goTypes = []interface{}{
 	(*descriptorpb.MethodOptions)(nil),    // 1: google.protobuf.MethodOptions
 	(*descriptorpb.EnumValueOptions)(nil), // 2: google.protobuf.EnumValueOptions
 	(*descriptorpb.ServiceOptions)(nil),   // 3: google.protobuf.ServiceOptions
+	(*descriptorpb.MessageOptions)(nil),   // 4: google.protobuf.MessageOptions
 }
 var file_api_proto_depIdxs = []int32{
 	0,  // 0: api.raw_body:extendee -> google.protobuf.FieldOptions
@@ -611,10 +644,12 @@ var file_api_proto_depIdxs = []int32{
 	2,  // 34: api.http_code:extendee -> google.protobuf.EnumValueOptions
 	3,  // 35: api.base_domain:extendee -> google.protobuf.ServiceOptions
 	3,  // 36: api.base_domain_compatible:extendee -> google.protobuf.ServiceOptions
-	37, // [37:37] is the sub-list for method output_type
-	37, // [37:37] is the sub-list for method input_type
-	37, // [37:37] is the sub-list for extension type_name
-	0,  // [0:37] is the sub-list for extension extendee
+	3,  // 37: api.service_path:extendee -> google.protobuf.ServiceOptions
+	4,  // 38: api.reserve:extendee -> google.protobuf.MessageOptions
+	39, // [39:39] is the sub-list for method output_type
+	39, // [39:39] is the sub-list for method input_type
+	39, // [39:39] is the sub-list for extension type_name
+	0,  // [0:39] is the sub-list for extension extendee
 	0,  // [0:0] is the sub-list for field type_name
 }
 
@@ -630,7 +665,7 @@ func file_api_proto_init() {
 			RawDescriptor: file_api_proto_rawDesc,
 			NumEnums:      0,
 			NumMessages:   0,
-			NumExtensions: 37,
+			NumExtensions: 39,
 			NumServices:   0,
 		},
 		GoTypes:           file_api_proto_goTypes,
diff --git a/cmd/hz/protobuf/api/api.proto b/cmd/hz/protobuf/api/api.proto
index 72edf41..71a3cd3 100644
--- a/cmd/hz/protobuf/api/api.proto
+++ b/cmd/hz/protobuf/api/api.proto
@@ -64,6 +64,7 @@ extend google.protobuf.ServiceOptions {
 
   // 50731~50760 used to extend service option by hz
   optional string base_domain_compatible = 50731;
+  optional string service_path = 50732;
 }
 
 extend google.protobuf.MessageOptions {
diff --git a/cmd/hz/protobuf/ast.go b/cmd/hz/protobuf/ast.go
index fcb8197..264dc4e 100644
--- a/cmd/hz/protobuf/ast.go
+++ b/cmd/hz/protobuf/ast.go
@@ -19,6 +19,7 @@ package protobuf
 import (
 	"fmt"
 	"path/filepath"
+	"sort"
 	"strings"
 
 	"github.com/cloudwego/hertz/cmd/hz/generator"
@@ -129,12 +130,25 @@ func astToService(ast *descriptorpb.FileDescriptorProto, resolver *Resolver, cmd
 		ms := s.GetMethod()
 		methods := make([]*generator.HttpMethod, 0, len(ms))
 		clientMethods := make([]*generator.ClientMethod, 0, len(ms))
+		servicePathAnno := checkFirstOption(api.E_ServicePath, s.GetOptions())
+		servicePath := ""
+		if val, ok := servicePathAnno.(string); ok {
+			servicePath = val
+		}
 		for _, m := range ms {
-			hmethod, vpath := checkFirstOptions(HttpMethodOptions, m.GetOptions())
-			if hmethod == "" {
+			rs := getAllOptions(HttpMethodOptions, m.GetOptions())
+			if len(rs) == 0 {
 				continue
 			}
-			path := vpath.(string)
+			httpOpts := httpOptions{}
+			for k, v := range rs {
+				httpOpts = append(httpOpts, httpOption{
+					method: k,
+					path:   v.(string),
+				})
+			}
+			// turn the map into a slice and sort it to make sure getting the results in the same order every time
+			sort.Sort(httpOpts)
 
 			var handlerOutDir string
 			genPath := getCompatibleAnnotation(m.GetOptions(), api.E_HandlerPath, api.E_HandlerPathCompatible)
@@ -142,6 +156,9 @@ func astToService(ast *descriptorpb.FileDescriptorProto, resolver *Resolver, cmd
 			if !ok || len(handlerOutDir) == 0 {
 				handlerOutDir = ""
 			}
+			if len(handlerOutDir) == 0 {
+				handlerOutDir = servicePath
+			}
 
 			// protoGoInfo can get generated "Go Info" for proto file.
 			// the type name may be different between "***.proto" and "***.pb.go"
@@ -181,10 +198,11 @@ func astToService(ast *descriptorpb.FileDescriptorProto, resolver *Resolver, cmd
 
 			method := &generator.HttpMethod{
 				Name:       util.CamelString(m.GetName()),
-				HTTPMethod: hmethod,
-				Path:       path,
+				HTTPMethod: httpOpts[0].method,
+				Path:       httpOpts[0].path,
 				Serializer: serializer,
 				OutputDir:  handlerOutDir,
+				GenHandler: true,
 			}
 
 			goOptMapAlias := make(map[string]string, 1)
@@ -223,6 +241,16 @@ func astToService(ast *descriptorpb.FileDescriptorProto, resolver *Resolver, cmd
 			method.ReturnTypePackage = respPackage
 
 			methods = append(methods, method)
+			for idx, anno := range httpOpts {
+				if idx == 0 {
+					continue
+				}
+				tmp := *method
+				tmp.HTTPMethod = anno.method
+				tmp.Path = anno.path
+				tmp.GenHandler = false
+				methods = append(methods, &tmp)
+			}
 
 			if cmdType == meta.CmdClient {
 				clientMethod := &generator.ClientMethod{}
diff --git a/cmd/hz/protobuf/tags.go b/cmd/hz/protobuf/tags.go
index 23c0464..1abf231 100644
--- a/cmd/hz/protobuf/tags.go
+++ b/cmd/hz/protobuf/tags.go
@@ -94,6 +94,38 @@ var (
 	SerializerOptions = map[*protoimpl.ExtensionInfo]string{api.E_Serializer: "serializer"}
 )
 
+type httpOption struct {
+	method string
+	path   string
+}
+
+type httpOptions []httpOption
+
+func (s httpOptions) Len() int {
+	return len(s)
+}
+
+func (s httpOptions) Swap(i, j int) {
+	s[i], s[j] = s[j], s[i]
+}
+
+func (s httpOptions) Less(i, j int) bool {
+	return s[i].method < s[j].method
+}
+
+func getAllOptions(extensions map[*protoimpl.ExtensionInfo]string, opts ...protoreflect.ProtoMessage) map[string]interface{} {
+	out := map[string]interface{}{}
+	for _, opt := range opts {
+		for e, t := range extensions {
+			if proto.HasExtension(opt, e) {
+				v := proto.GetExtension(opt, e)
+				out[t] = v
+			}
+		}
+	}
+	return out
+}
+
 func checkFirstOptions(extensions map[*protoimpl.ExtensionInfo]string, opts ...protoreflect.ProtoMessage) (string, interface{}) {
 	for _, opt := range opts {
 		for e, t := range extensions {
diff --git a/cmd/hz/thrift/ast.go b/cmd/hz/thrift/ast.go
index 557b50a..f634d96 100644
--- a/cmd/hz/thrift/ast.go
+++ b/cmd/hz/thrift/ast.go
@@ -18,6 +18,7 @@ package thrift
 
 import (
 	"fmt"
+	"sort"
 	"strings"
 
 	"github.com/cloudwego/hertz/cmd/hz/config"
@@ -98,26 +99,34 @@ func astToService(ast *parser.Thrift, resolver *Resolver, args *config.Argument)
 		}
 		methods := make([]*generator.HttpMethod, 0, len(ms))
 		clientMethods := make([]*generator.ClientMethod, 0, len(ms))
+		servicePathAnno := getAnnotation(s.Annotations, ApiServicePath)
+		servicePath := ""
+		if len(servicePathAnno) > 0 {
+			servicePath = servicePathAnno[0]
+		}
 		for _, m := range ms {
 			rs := getAnnotations(m.Annotations, HttpMethodAnnotations)
-			if len(rs) > 1 {
-				return nil, fmt.Errorf("too many 'api.XXX' annotations: %s", rs)
-			}
 			if len(rs) == 0 {
 				continue
 			}
-
-			var handlerOutDir string
+			httpAnnos := httpAnnotations{}
+			for k, v := range rs {
+				httpAnnos = append(httpAnnos, httpAnnotation{
+					method: k,
+					path:   v,
+				})
+			}
+			// turn the map into a slice and sort it to make sure getting the results in the same order every time
+			sort.Sort(httpAnnos)
+			handlerOutDir := servicePath
 			genPaths := getAnnotation(m.Annotations, ApiGenPath)
-			if len(genPaths) == 0 {
-				handlerOutDir = ""
-			} else if len(genPaths) > 1 {
-				return nil, fmt.Errorf("too many 'api.handler_path' for %s", m.Name)
-			} else {
+			if len(genPaths) == 1 {
 				handlerOutDir = genPaths[0]
+			} else if len(genPaths) > 0 {
+				return nil, fmt.Errorf("too many 'api.handler_path' for %s", m.Name)
 			}
 
-			hmethod, path := util.GetFirstKV(rs)
+			hmethod, path := httpAnnos[0].method, httpAnnos[0].path
 			if len(path) != 1 || path[0] == "" {
 				return nil, fmt.Errorf("invalid api.%s  for %s.%s: %s", hmethod, s.Name, m.Name, path)
 			}
@@ -175,6 +184,7 @@ func astToService(ast *parser.Thrift, resolver *Resolver, args *config.Argument)
 				Path:               path[0],
 				Serializer:         sr,
 				OutputDir:          handlerOutDir,
+				GenHandler:         true,
 				// Annotations:     m.Annotations,
 			}
 			refs := resolver.ExportReferred(false, true)
@@ -187,6 +197,20 @@ func astToService(ast *parser.Thrift, resolver *Resolver, args *config.Argument)
 			}
 			models.MergeMap(method.Models)
 			methods = append(methods, method)
+			for idx, anno := range httpAnnos {
+				if idx == 0 {
+					continue
+				}
+				tmp := *method
+				hmethod, path := anno.method, anno.path
+				if len(path) != 1 || path[0] == "" {
+					return nil, fmt.Errorf("invalid api.%s  for %s.%s: %s", hmethod, s.Name, m.Name, path)
+				}
+				tmp.HTTPMethod = hmethod
+				tmp.Path = path[0]
+				tmp.GenHandler = false
+				methods = append(methods, &tmp)
+			}
 			if args.CmdType == meta.CmdClient {
 				clientMethod := &generator.ClientMethod{}
 				clientMethod.HttpMethod = method
@@ -353,18 +377,7 @@ func getAllExtendFunction(svc *parser.Service, ast *parser.Thrift, resolver *Res
 				}
 				funcs := extendSvc.GetFunctions()
 				for _, f := range funcs {
-					// the method of other file is extended, and the package of req/resp needs to be changed
-					// ex. base.thrift -> Resp Method(Req){}
-					//					  base.Resp Method(base.Req){}
-					// todo: support container for Struct
-					if len(f.Arguments) > 0 {
-						if !strings.Contains(f.Arguments[0].Type.Name, ".") && f.Arguments[0].Type.Category.IsStruct() {
-							f.Arguments[0].Type.Name = base + "." + f.Arguments[0].Type.Name
-						}
-					}
-					if !strings.Contains(f.FunctionType.Name, ".") && f.FunctionType.Category.IsStruct() {
-						f.FunctionType.Name = base + "." + f.FunctionType.Name
-					}
+					processExtendsType(f, base)
 				}
 				extendFuncs, err := getAllExtendFunction(extendSvc, ast, resolver, args)
 				if err != nil {
@@ -392,18 +405,7 @@ func getAllExtendFunction(svc *parser.Service, ast *parser.Thrift, resolver *Res
 			if found {
 				funcs := extendSvc.GetFunctions()
 				for _, f := range funcs {
-					// the method of other file is extended, and the package of req/resp needs to be changed
-					// ex. base.thrift -> Resp Method(Req){}
-					//					  base.Resp Method(base.Req){}
-					// todo: support container for Struct
-					if len(f.Arguments) > 0 {
-						if !strings.Contains(f.Arguments[0].Type.Name, ".") && f.Arguments[0].Type.Category.IsStruct() {
-							f.Arguments[0].Type.Name = base + "." + f.Arguments[0].Type.Name
-						}
-					}
-					if !strings.Contains(f.FunctionType.Name, ".") && f.FunctionType.Category.IsStruct() {
-						f.FunctionType.Name = base + "." + f.FunctionType.Name
-					}
+					processExtendsType(f, base)
 				}
 				extendFuncs, err := getAllExtendFunction(extendSvc, refAst, resolver, args)
 				if err != nil {
@@ -418,6 +420,53 @@ func getAllExtendFunction(svc *parser.Service, ast *parser.Thrift, resolver *Res
 	return res, nil
 }
 
+func processExtendsType(f *parser.Function, base string) {
+	// the method of other file is extended, and the package of req/resp needs to be changed
+	// ex. base.thrift -> Resp Method(Req){}
+	//					  base.Resp Method(base.Req){}
+	if len(f.Arguments) > 0 {
+		if f.Arguments[0].Type.Category.IsContainerType() {
+			switch f.Arguments[0].Type.Category {
+			case parser.Category_Set, parser.Category_List:
+				if !strings.Contains(f.Arguments[0].Type.ValueType.Name, ".") && f.Arguments[0].Type.ValueType.Category.IsStruct() {
+					f.Arguments[0].Type.ValueType.Name = base + "." + f.Arguments[0].Type.ValueType.Name
+				}
+			case parser.Category_Map:
+				if !strings.Contains(f.Arguments[0].Type.ValueType.Name, ".") && f.Arguments[0].Type.ValueType.Category.IsStruct() {
+					f.Arguments[0].Type.ValueType.Name = base + "." + f.Arguments[0].Type.ValueType.Name
+				}
+				if !strings.Contains(f.Arguments[0].Type.KeyType.Name, ".") && f.Arguments[0].Type.KeyType.Category.IsStruct() {
+					f.Arguments[0].Type.KeyType.Name = base + "." + f.Arguments[0].Type.KeyType.Name
+				}
+			}
+		} else {
+			if !strings.Contains(f.Arguments[0].Type.Name, ".") && f.Arguments[0].Type.Category.IsStruct() {
+				f.Arguments[0].Type.Name = base + "." + f.Arguments[0].Type.Name
+			}
+		}
+	}
+
+	if f.FunctionType.Category.IsContainerType() {
+		switch f.FunctionType.Category {
+		case parser.Category_Set, parser.Category_List:
+			if !strings.Contains(f.FunctionType.ValueType.Name, ".") && f.FunctionType.ValueType.Category.IsStruct() {
+				f.FunctionType.ValueType.Name = base + "." + f.FunctionType.ValueType.Name
+			}
+		case parser.Category_Map:
+			if !strings.Contains(f.FunctionType.ValueType.Name, ".") && f.FunctionType.ValueType.Category.IsStruct() {
+				f.FunctionType.ValueType.Name = base + "." + f.FunctionType.ValueType.Name
+			}
+			if !strings.Contains(f.FunctionType.KeyType.Name, ".") && f.FunctionType.KeyType.Category.IsStruct() {
+				f.FunctionType.KeyType.Name = base + "." + f.FunctionType.KeyType.Name
+			}
+		}
+	} else {
+		if !strings.Contains(f.FunctionType.Name, ".") && f.FunctionType.Category.IsStruct() {
+			f.FunctionType.Name = base + "." + f.FunctionType.Name
+		}
+	}
+}
+
 func getUniqueResolveDependentName(name string, resolver *Resolver) string {
 	rawName := name
 	for i := 0; i < 10000; i++ {
diff --git a/cmd/hz/thrift/tags.go b/cmd/hz/thrift/tags.go
index 5b2e8b6..96c414d 100644
--- a/cmd/hz/thrift/tags.go
+++ b/cmd/hz/thrift/tags.go
@@ -63,7 +63,8 @@ const (
 const (
 	ApiBaseDomain    = "api.base_domain"
 	ApiServiceGroup  = "api.service_group"
-	ApiServiceGenDir = "api.service_gen_dir"
+	ApiServiceGenDir = "api.service_gen_dir" // handler_dir for handler_by_service
+	ApiServicePath   = "api.service_path"    // declare the path to the service's handler according to this annotation for handler_by_method
 )
 
 var (
@@ -141,6 +142,25 @@ func getAnnotation(input parser.Annotations, target string) []string {
 	return []string{}
 }
 
+type httpAnnotation struct {
+	method string
+	path   []string
+}
+
+type httpAnnotations []httpAnnotation
+
+func (s httpAnnotations) Len() int {
+	return len(s)
+}
+
+func (s httpAnnotations) Swap(i, j int) {
+	s[i], s[j] = s[j], s[i]
+}
+
+func (s httpAnnotations) Less(i, j int) bool {
+	return s[i].method < s[j].method
+}
+
 func getAnnotations(input parser.Annotations, targets map[string]string) map[string][]string {
 	if len(input) == 0 || len(targets) == 0 {
 		return nil
diff --git a/licenses/LICENSE-sprig.txt b/licenses/LICENSE-sprig.txt
new file mode 100644
index 0000000..9e6cf57
--- /dev/null
+++ b/licenses/LICENSE-sprig.txt
@@ -0,0 +1,19 @@
+Copyright (C) 2013-2020 Masterminds
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
\ No newline at end of file
diff --git a/pkg/protocol/http1/resp/writer.go b/pkg/protocol/http1/resp/writer.go
index 745ab4c..7b50cd4 100644
--- a/pkg/protocol/http1/resp/writer.go
+++ b/pkg/protocol/http1/resp/writer.go
@@ -17,6 +17,7 @@
 package resp
 
 import (
+	"runtime"
 	"sync"
 
 	"github.com/cloudwego/hertz/pkg/network"
@@ -24,6 +25,16 @@ import (
 	"github.com/cloudwego/hertz/pkg/protocol/http1/ext"
 )
 
+var chunkReaderPool sync.Pool
+
+func init() {
+	chunkReaderPool = sync.Pool{
+		New: func() interface{} {
+			return &chunkedBodyWriter{}
+		},
+	}
+}
+
 type chunkedBodyWriter struct {
 	sync.Once
 	finalizeErr error
@@ -59,6 +70,14 @@ func (c *chunkedBodyWriter) Flush() error {
 // Warning: do not call this method by yourself, unless you know what you are doing.
 func (c *chunkedBodyWriter) Finalize() error {
 	c.Do(func() {
+		// in case no actual data from user
+		if !c.wroteHeader {
+			c.r.Header.SetContentLength(-1)
+			if c.finalizeErr = WriteHeader(&c.r.Header, c.w); c.finalizeErr != nil {
+				return
+			}
+			c.wroteHeader = true
+		}
 		c.finalizeErr = ext.WriteChunk(c.w, nil, true)
 		if c.finalizeErr != nil {
 			return
@@ -68,9 +87,19 @@ func (c *chunkedBodyWriter) Finalize() error {
 	return c.finalizeErr
 }
 
+func (c *chunkedBodyWriter) release() {
+	c.r = nil
+	c.w = nil
+	c.finalizeErr = nil
+	c.wroteHeader = false
+	chunkReaderPool.Put(c)
+}
+
 func NewChunkedBodyWriter(r *protocol.Response, w network.Writer) network.ExtWriter {
-	return &chunkedBodyWriter{
-		r: r,
-		w: w,
-	}
+	extWriter := chunkReaderPool.Get().(*chunkedBodyWriter)
+	extWriter.r = r
+	extWriter.w = w
+	extWriter.Once = sync.Once{}
+	runtime.SetFinalizer(extWriter, (*chunkedBodyWriter).release)
+	return extWriter
 }
diff --git a/version.go b/version.go
index 2f7a82f..2793606 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.7"
+	Version = "v0.6.8"
 )


Test patch:
diff --git a/pkg/app/client/client_test.go b/pkg/app/client/client_test.go
index 8449cd4..e7dcbf5 100644
--- a/pkg/app/client/client_test.go
+++ b/pkg/app/client/client_test.go
@@ -2269,7 +2269,7 @@ func TestClientDoWithDialFunc(t *testing.T) {
 
 func TestClientState(t *testing.T) {
 	opt := config.NewOptions([]config.Option{})
-	opt.Addr = "127.0.0.1:11000"
+	opt.Addr = ":10037"
 	engine := route.NewEngine(opt)
 	go engine.Run()
 
@@ -2282,12 +2282,12 @@ 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, "127.0.0.1:11000", hcs.ConnPoolState().Addr)
+				assert.DeepEqual(t, "127.0.0.1:10037", hcs.ConnPoolState().Addr)
 				atomic.StoreInt32(&state, int32(1))
 			case int32(1):
 				assert.DeepEqual(t, 0, hcs.ConnPoolState().TotalConnNum)
 				assert.DeepEqual(t, 0, hcs.ConnPoolState().PoolConnNum)
-				assert.DeepEqual(t, "127.0.0.1:11000", hcs.ConnPoolState().Addr)
+				assert.DeepEqual(t, "127.0.0.1:10037", hcs.ConnPoolState().Addr)
 				atomic.StoreInt32(&state, int32(2))
 				return
 			case int32(2):
@@ -2295,7 +2295,7 @@ func TestClientState(t *testing.T) {
 			}
 		}, time.Second*9))
 
-	client.Get(context.Background(), nil, "http://127.0.0.1:11000")
+	client.Get(context.Background(), nil, "http://127.0.0.1:10037")
 	time.Sleep(time.Second * 22)
 }
 
diff --git a/pkg/app/context_test.go b/pkg/app/context_test.go
index 8bf52f4..7211e39 100644
--- a/pkg/app/context_test.go
+++ b/pkg/app/context_test.go
@@ -17,10 +17,14 @@
 package app
 
 import (
+	"bytes"
 	"context"
+	"encoding/xml"
 	"errors"
 	"fmt"
+	"html/template"
 	"io/ioutil"
+	"os"
 	"reflect"
 	"strings"
 	"testing"
@@ -82,6 +86,17 @@ func TestContext(t *testing.T) {
 	}
 }
 
+func TestValue(t *testing.T) {
+	ctx := NewContext(0)
+
+	v := ctx.Value("testContextKey")
+	assert.Nil(t, v)
+
+	ctx.Set("testContextKey", "testValue")
+	v = ctx.Value("testContextKey")
+	assert.DeepEqual(t, "testValue", v)
+}
+
 func TestContextNotModified(t *testing.T) {
 	reqContext := NewContext(0)
 	reqContext.Response.SetStatusCode(consts.StatusOK)
@@ -273,6 +288,10 @@ func TestQuery(t *testing.T) {
 		t.Fatalf("unexpected query: %#v, expected menu", ctx.Query("name"))
 	}
 
+	if ctx.DefaultQuery("name", "default value") != "menu" {
+		t.Fatalf("unexpected query: %#v, expected menu", ctx.Query("name"))
+	}
+
 	if ctx.DefaultQuery("defaultQuery", "default value") != "default value" {
 		t.Fatalf("unexpected query: %#v, expected `default value`", ctx.Query("defaultQuery"))
 	}
@@ -436,6 +455,15 @@ tailfoobar`
 		}
 	}
 
+	err = ctx.SaveUploadedFile(f.File["fileaaa"][0], "TODO")
+	assert.Nil(t, err)
+	fileInfo, err := os.Stat("TODO")
+	assert.Nil(t, err)
+	assert.DeepEqual(t, "TODO", fileInfo.Name())
+	assert.DeepEqual(t, f.File["fileaaa"][0].Size, fileInfo.Size())
+	err = os.Remove("TODO")
+	assert.Nil(t, err)
+
 	ff, err := ctx.FormFile("fileaaa")
 	if err != nil || ff == nil {
 		t.Fatalf("unexpected error happened when ctx.FormFile()")
@@ -525,6 +553,13 @@ func TestRequestContext_Header(t *testing.T) {
 	if val != "" {
 		t.Fatalf("unexpected %q. Expecting %q", val, "")
 	}
+
+	c.Header("header_key1", "header_val1")
+	c.Header("header_key1", "")
+	val = string(c.Response.Header.Peek("header_key1"))
+	if val != "" {
+		t.Fatalf("unexpected %q. Expecting %q", val, "")
+	}
 }
 
 func TestRequestContext_Keys(t *testing.T) {
@@ -554,6 +589,10 @@ func TestRequestContext_Handler(t *testing.T) {
 	if val != "123" {
 		t.Fatalf("unexpected %v. Expecting %v", val, "123")
 	}
+
+	c.handlers = nil
+	handler := c.Handler()
+	assert.Nil(t, handler)
 }
 
 func TestRequestContext_Handlers(t *testing.T) {
@@ -576,6 +615,24 @@ func TestRequestContext_HandlerName(t *testing.T) {
 	}
 }
 
+func TestNext(t *testing.T) {
+	c := NewContext(0)
+	a := 0
+
+	testFunc1 := func(c context.Context, ctx *RequestContext) {
+		a = 1
+	}
+	testFunc3 := func(c context.Context, ctx *RequestContext) {
+		a = 3
+	}
+	c.handlers = HandlersChain{testFunc1, testFunc3}
+
+	c.Next(context.Background())
+
+	assert.True(t, c.index == 2)
+	assert.DeepEqual(t, 3, a)
+}
+
 func TestContextError(t *testing.T) {
 	c := NewContext(0)
 	assert.Nil(t, c.Errors)
@@ -631,6 +688,77 @@ func TestRender(t *testing.T) {
 
 	assert.DeepEqual(t, consts.StatusOK, c.Response.StatusCode())
 	assert.True(t, strings.Contains(string(c.Response.Body()), "test"))
+
+	c.Reset()
+	c.Render(110, &render.Data{
+		ContentType: "application/json; charset=utf-8",
+		Data:        []byte("{\"test\":1}"),
+	})
+	assert.DeepEqual(t, "application/json; charset=utf-8", string(c.Response.Header.ContentType()))
+	assert.DeepEqual(t, "", string(c.Response.Body()))
+
+	c.Reset()
+	c.Render(consts.StatusNoContent, &render.Data{
+		ContentType: "application/json; charset=utf-8",
+		Data:        []byte("{\"test\":1}"),
+	})
+	assert.DeepEqual(t, "application/json; charset=utf-8", string(c.Response.Header.ContentType()))
+	assert.DeepEqual(t, "", string(c.Response.Body()))
+
+	c.Reset()
+	c.Render(consts.StatusNotModified, &render.Data{
+		ContentType: "application/json; charset=utf-8",
+		Data:        []byte("{\"test\":1}"),
+	})
+	assert.DeepEqual(t, "application/json; charset=utf-8", string(c.Response.Header.ContentType()))
+	assert.DeepEqual(t, "", string(c.Response.Body()))
+}
+
+func TestHTML(t *testing.T) {
+	c := NewContext(0)
+
+	tmpl := template.Must(template.New("").
+		Delims("{[{", "}]}").
+		Funcs(template.FuncMap{}).
+		ParseFiles("../common/testdata/template/index.tmpl"))
+
+	r := &render.HTMLProduction{Template: tmpl}
+	c.HTMLRender = r
+	c.HTML(consts.StatusOK, "index.tmpl", utils.H{"title": "Main website"})
+
+	assert.DeepEqual(t, []byte("text/html; charset=utf-8"), c.Response.Header.Peek("Content-Type"))
+	assert.DeepEqual(t, []byte("<html><h1>Main website</h1></html>"), c.Response.Body())
+}
+
+type xmlmap map[string]interface{}
+
+// Allows type H to be used with xml.Marshal
+func (h xmlmap) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
+	start.Name = xml.Name{
+		Space: "",
+		Local: "map",
+	}
+	if err := e.EncodeToken(start); err != nil {
+		return err
+	}
+	for key, value := range h {
+		elem := xml.StartElement{
+			Name: xml.Name{Space: "", Local: key},
+			Attr: []xml.Attr{},
+		}
+		if err := e.EncodeElement(value, elem); err != nil {
+			return err
+		}
+	}
+
+	return e.EncodeToken(xml.EndElement{Name: start.Name})
+}
+
+func TestXML(t *testing.T) {
+	c := NewContext(0)
+	c.XML(consts.StatusOK, xmlmap{"foo": "bar"})
+	assert.DeepEqual(t, []byte("<map><foo>bar</foo></map>"), c.Response.Body())
+	assert.DeepEqual(t, []byte("application/xml; charset=utf-8"), c.Response.Header.Peek("Content-Type"))
 }
 
 func TestJSON(t *testing.T) {
@@ -654,6 +782,7 @@ func TestContextReset(t *testing.T) {
 	c.Params = param.Params{param.Param{}}
 	c.Error(errors.New("test")) // nolint: errcheck
 	c.Set("foo", "bar")
+	c.Finished()
 	c.Request.SetIsTLS(true)
 	c.ResetWithoutConn()
 	c.Request.URI()
@@ -664,6 +793,7 @@ func TestContextReset(t *testing.T) {
 	assert.Nil(t, c.Errors.ByType(errs.ErrorTypeAny))
 	assert.DeepEqual(t, 0, len(c.Params))
 	assert.DeepEqual(t, int8(-1), c.index)
+	assert.Nil(t, c.finished)
 }
 
 func TestContextContentType(t *testing.T) {
@@ -751,6 +881,16 @@ func TestRemoteAddr(t *testing.T) {
 	assert.DeepEqual(t, "0.0.0.0:0", addr)
 }
 
+func TestRequestBodyStream(t *testing.T) {
+	c := NewContext(0)
+	s := "testRequestBodyStream"
+	mr := bytes.NewBufferString(s)
+	c.Request.SetBodyStream(mr, -1)
+	data, err := ioutil.ReadAll(c.RequestBodyStream())
+	assert.Nil(t, err)
+	assert.DeepEqual(t, "testRequestBodyStream", string(data))
+}
+
 func TestContextIsAborted(t *testing.T) {
 	ctx := NewContext(0)
 	assert.False(t, ctx.IsAborted())
@@ -828,6 +968,23 @@ func TestRequestCtxFormValue(t *testing.T) {
 	if string(v) != "1" {
 		t.Fatalf("unexpected value %q. Expecting %q", v, "1")
 	}
+
+	ctx.Request.Reset()
+	s := `------WebKitFormBoundaryJwfATyF8tmxSJnLg
+Content-Disposition: form-data; name="f"
+
+fff
+------WebKitFormBoundaryJwfATyF8tmxSJnLg
+`
+	mr := bytes.NewBufferString(s)
+	ctx.Request.SetBodyStream(mr, -1)
+	ctx.Request.Header.SetContentLength(len(s))
+	ctx.Request.Header.SetContentTypeBytes([]byte("multipart/form-data; boundary=----WebKitFormBoundaryJwfATyF8tmxSJnLg"))
+
+	v = ctx.FormValue("f")
+	if string(v) != "fff" {
+		t.Fatalf("unexpected value %q. Expecting %q", v, "fff")
+	}
 }
 
 func TestSetCustomFormValueFunc(t *testing.T) {
@@ -1076,6 +1233,25 @@ func TestForEachKey(t *testing.T) {
 	assert.True(t, ok)
 }
 
+func TestFlush(t *testing.T) {
+	ctx := NewContext(0)
+	err := ctx.Flush()
+	assert.Nil(t, err)
+}
+
+func TestConn(t *testing.T) {
+	ctx := NewContext(0)
+
+	conn := mock.NewConn("")
+
+	ctx.SetConn(conn)
+	connRes := ctx.GetConn()
+
+	val1 := reflect.ValueOf(conn).Pointer()
+	val2 := reflect.ValueOf(connRes).Pointer()
+	assert.DeepEqual(t, val1, val2)
+}
+
 func TestHijackHandler(t *testing.T) {
 	ctx := NewContext(0)
 	handle := func(c network.Conn) {
@@ -1089,6 +1265,32 @@ func TestHijackHandler(t *testing.T) {
 	assert.DeepEqual(t, val1, val2)
 }
 
+func TestGetReader(t *testing.T) {
+	ctx := NewContext(0)
+
+	conn := mock.NewConn("")
+
+	ctx.SetConn(conn)
+	connRes := ctx.GetReader()
+
+	val1 := reflect.ValueOf(conn).Pointer()
+	val2 := reflect.ValueOf(connRes).Pointer()
+	assert.DeepEqual(t, val1, val2)
+}
+
+func TestGetWriter(t *testing.T) {
+	ctx := NewContext(0)
+
+	conn := mock.NewConn("")
+
+	ctx.SetConn(conn)
+	connRes := ctx.GetWriter()
+
+	val1 := reflect.ValueOf(conn).Pointer()
+	val2 := reflect.ValueOf(connRes).Pointer()
+	assert.DeepEqual(t, val1, val2)
+}
+
 func TestIndex(t *testing.T) {
 	ctx := NewContext(0)
 	ctx.ResetWithoutConn()
diff --git a/pkg/app/fs_test.go b/pkg/app/fs_test.go
index 1cd545e..c4ad0e4 100644
--- a/pkg/app/fs_test.go
+++ b/pkg/app/fs_test.go
@@ -53,6 +53,7 @@ import (
 	"testing"
 	"time"
 
+	"github.com/cloudwego/hertz/pkg/common/test/assert"
 	"github.com/cloudwego/hertz/pkg/common/test/mock"
 	"github.com/cloudwego/hertz/pkg/protocol"
 	"github.com/cloudwego/hertz/pkg/protocol/consts"
@@ -223,6 +224,32 @@ func TestServeFileSmallNoReadFrom(t *testing.T) {
 	if body != teststr {
 		t.Fatalf("expected '%s'", teststr)
 	}
+
+	data := make([]byte, len([]byte(teststr)))
+	nn, err := reader.Read(data)
+	assert.DeepEqual(t, len([]byte(teststr)), nn)
+	assert.Nil(t, err)
+	assert.DeepEqual(t, teststr, string(data))
+	assert.DeepEqual(t, reader.startPos, len([]byte(teststr)))
+
+	nn, err = reader.Read(data)
+	assert.DeepEqual(t, 0, nn)
+	assert.DeepEqual(t, io.EOF, err)
+
+	data1 := make([]byte, 2)
+	reader.startPos = len([]byte(teststr)) - 1
+	nn, err = reader.Read(data1)
+	assert.DeepEqual(t, []byte("!"), []byte{data1[0]})
+	assert.DeepEqual(t, 1, nn)
+	assert.DeepEqual(t, nil, err)
+
+	reader.startPos = 0
+	reader.ff.f = nil
+	buf = bytes.NewBuffer(nil)
+	reader.ff.dirIndex = make([]byte, len([]byte(teststr)))
+	n, err = reader.WriteTo(pureWriter{buf})
+	assert.DeepEqual(t, int64(len(teststr)), n)
+	assert.Nil(t, err)
 }
 
 type pureWriter struct {
@@ -660,3 +687,11 @@ func TestServeFileContentType(t *testing.T) {
 		t.Fatalf("Unexpected Content-Type, expected: %q got %q", expected, r.Header.ContentType())
 	}
 }
+
+func TestFileSmallUpdateByteRange(t *testing.T) {
+	r := &fsSmallFileReader{}
+	err := r.UpdateByteRange(1, 1)
+	assert.Nil(t, err)
+	assert.DeepEqual(t, 1, r.startPos)
+	assert.DeepEqual(t, 2, r.endPos)
+}
diff --git a/pkg/app/middlewares/client/sd/options_test.go b/pkg/app/middlewares/client/sd/options_test.go
index 44a7356..d0fad12 100644
--- a/pkg/app/middlewares/client/sd/options_test.go
+++ b/pkg/app/middlewares/client/sd/options_test.go
@@ -20,16 +20,27 @@ import (
 	"context"
 	"testing"
 
+	"github.com/cloudwego/hertz/pkg/app/client/loadbalance"
 	"github.com/cloudwego/hertz/pkg/common/test/assert"
 )
 
 func TestWithCustomizedAddrs(t *testing.T) {
 	var options []ServiceDiscoveryOption
-	options = append(options, WithCustomizedAddrs("127.0.0.1:8080"))
+	options = append(options, WithCustomizedAddrs("127.0.0.1:8080", "/tmp/unix_ss"))
 	opts := &ServiceDiscoveryOptions{}
 	opts.Apply(options)
-	assert.Assert(t, opts.Resolver.Name() == "127.0.0.1:8080")
+	assert.Assert(t, opts.Resolver.Name() == "127.0.0.1:8080,/tmp/unix_ss")
 	res, err := opts.Resolver.Resolve(context.Background(), "")
 	assert.Assert(t, err == nil)
 	assert.Assert(t, res.Instances[0].Address().String() == "127.0.0.1:8080")
+	assert.Assert(t, res.Instances[1].Address().String() == "/tmp/unix_ss")
+}
+
+func TestWithLoadBalanceOptions(t *testing.T) {
+	balance := loadbalance.NewWeightedBalancer()
+	var options []ServiceDiscoveryOption
+	options = append(options, WithLoadBalanceOptions(balance, loadbalance.DefaultLbOpts))
+	opts := &ServiceDiscoveryOptions{}
+	opts.Apply(options)
+	assert.Assert(t, opts.Balancer.Name() == "weight_random")
 }
diff --git a/pkg/app/server/binding/binding_test.go b/pkg/app/server/binding/binding_test.go
index 84c5980..a9050f9 100644
--- a/pkg/app/server/binding/binding_test.go
+++ b/pkg/app/server/binding/binding_test.go
@@ -164,6 +164,19 @@ func TestJsonBind(t *testing.T) {
 	// NOTE: The default does not support string to go int conversion in json.
 	// You can add "string" tags or use other json unmarshal libraries that support this feature
 	assert.DeepEqual(t, 100, req.D)
+
+	req = Test{}
+	UseStdJSONUnmarshaler()
+	err = BindAndValidate(r, &req, nil)
+	if err != nil {
+		t.Fatalf("unexpected error: %v", err)
+	}
+	assert.DeepEqual(t, "aaa", req.A)
+	assert.DeepEqual(t, 2, len(req.B))
+	assert.DeepEqual(t, "ccc", req.C)
+	// NOTE: The default does not support string to go int conversion in json.
+	// You can add "string" tags or use other json unmarshal libraries that support this feature
+	assert.DeepEqual(t, 100, req.D)
 }
 
 // TestQueryParamInconsistency tests the Inconsistency for GetQuery(), the other unit test for GetFunc() in request.go  are similar to it
diff --git a/pkg/app/server/binding/request_test.go b/pkg/app/server/binding/request_test.go
new file mode 100644
index 0000000..b3bb705
--- /dev/null
+++ b/pkg/app/server/binding/request_test.go
@@ -0,0 +1,235 @@
+/*
+ * Copyright 2022 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 binding
+
+import (
+	"bytes"
+	"fmt"
+	"testing"
+
+	"github.com/cloudwego/hertz/pkg/common/test/assert"
+	"github.com/cloudwego/hertz/pkg/protocol"
+	"github.com/cloudwego/hertz/pkg/protocol/consts"
+)
+
+func TestGetQuery(t *testing.T) {
+	r := protocol.NewRequest("GET", "/foo", nil)
+	r.SetRequestURI("/foo/bar?para1=hertz&para2=query1&para2=query2&para3=1&para3=2")
+
+	bindReq := bindRequest{
+		req: r,
+	}
+
+	values := bindReq.GetQuery()
+
+	assert.DeepEqual(t, []string{"hertz"}, values["para1"])
+	assert.DeepEqual(t, []string{"query1", "query2"}, values["para2"])
+	assert.DeepEqual(t, []string{"1", "2"}, values["para3"])
+}
+
+func TestGetPostForm(t *testing.T) {
+	data := "a=aaa&b=b1&b=b2&c=ccc&d=100"
+	mr := bytes.NewBufferString(data)
+
+	r := protocol.NewRequest("POST", "/foo", mr)
+	r.Header.SetContentTypeBytes([]byte(consts.MIMEApplicationHTMLForm))
+	r.Header.SetContentLength(len(data))
+
+	bindReq := bindRequest{
+		req: r,
+	}
+
+	values, err := bindReq.GetPostForm()
+	if err != nil {
+		t.Fatalf("unexpected error: %v", err)
+	}
+
+	assert.DeepEqual(t, []string{"aaa"}, values["a"])
+	assert.DeepEqual(t, []string{"b1", "b2"}, values["b"])
+	assert.DeepEqual(t, []string{"ccc"}, values["c"])
+	assert.DeepEqual(t, []string{"100"}, values["d"])
+}
+
+func TestGetForm(t *testing.T) {
+	data := "a=aaa&b=b1&b=b2&c=ccc&d=100"
+	mr := bytes.NewBufferString(data)
+
+	r := protocol.NewRequest("POST", "/foo", mr)
+	r.SetRequestURI("/foo/bar?para1=hertz&para2=query1&para2=query2&para3=1&para3=2")
+	r.Header.SetContentTypeBytes([]byte(consts.MIMEApplicationHTMLForm))
+	r.Header.SetContentLength(len(data))
+
+	bindReq := bindRequest{
+		req: r,
+	}
+
+	values, err := bindReq.GetForm()
+	if err != nil {
+		t.Fatalf("unexpected error: %v", err)
+	}
+
+	assert.DeepEqual(t, []string{"aaa"}, values["a"])
+	assert.DeepEqual(t, []string{"b1", "b2"}, values["b"])
+	assert.DeepEqual(t, []string{"ccc"}, values["c"])
+	assert.DeepEqual(t, []string{"100"}, values["d"])
+	assert.DeepEqual(t, []string{"hertz"}, values["para1"])
+	assert.DeepEqual(t, []string{"query1", "query2"}, values["para2"])
+	assert.DeepEqual(t, []string{"1", "2"}, values["para3"])
+}
+
+func TestGetCookies(t *testing.T) {
+	r := protocol.NewRequest("POST", "/foo", nil)
+	r.SetCookie("cookie1", "cookies1")
+	r.SetCookie("cookie2", "cookies2")
+
+	bindReq := bindRequest{
+		req: r,
+	}
+
+	values := bindReq.GetCookies()
+
+	assert.DeepEqual(t, "cookies1", values[0].Value)
+	assert.DeepEqual(t, "cookies2", values[1].Value)
+}
+
+func TestGetHeader(t *testing.T) {
+	headers := map[string]string{
+		"Header1": "headers1",
+		"Header2": "headers2",
+	}
+
+	r := protocol.NewRequest("GET", "/foo", nil)
+	r.SetHeaders(headers)
+	r.SetHeader("Header3", "headers3")
+
+	bindReq := bindRequest{
+		req: r,
+	}
+
+	values := bindReq.GetHeader()
+
+	assert.DeepEqual(t, []string{"headers1"}, values["Header1"])
+	assert.DeepEqual(t, []string{"headers2"}, values["Header2"])
+	assert.DeepEqual(t, []string{"headers3"}, values["Header3"])
+}
+
+func TestGetMethod(t *testing.T) {
+	r := protocol.NewRequest("GET", "/foo", nil)
+
+	bindReq := bindRequest{
+		req: r,
+	}
+
+	values := bindReq.GetMethod()
+
+	assert.DeepEqual(t, "GET", values)
+}
+
+func TestGetContentType(t *testing.T) {
+	data := "a=aaa&b=b1&b=b2&c=ccc&d=100"
+	mr := bytes.NewBufferString(data)
+
+	r := protocol.NewRequest("POST", "/foo", mr)
+	r.Header.SetContentTypeBytes([]byte(consts.MIMEApplicationHTMLForm))
+	r.Header.SetContentLength(len(data))
+
+	bindReq := bindRequest{
+		req: r,
+	}
+
+	values := bindReq.GetContentType()
+
+	assert.DeepEqual(t, consts.MIMEApplicationHTMLForm, values)
+}
+
+func TestGetBody(t *testing.T) {
+	data := "a=aaa&b=b1&b=b2&c=ccc&d=100"
+	mr := bytes.NewBufferString(data)
+
+	r := protocol.NewRequest("POST", "/foo", mr)
+	r.Header.SetContentTypeBytes([]byte(consts.MIMEApplicationHTMLForm))
+	r.Header.SetContentLength(len(data))
+
+	bindReq := bindRequest{
+		req: r,
+	}
+
+	values, err := bindReq.GetBody()
+	if err != nil {
+		t.Fatalf("unexpected error: %v", err)
+	}
+
+	assert.DeepEqual(t, []byte("a=aaa&b=b1&b=b2&c=ccc&d=100"), values)
+}
+
+func TestGetFileHeaders(t *testing.T) {
+	s := `------WebKitFormBoundaryJwfATyF8tmxSJnLg
+Content-Disposition: form-data; name="f"
+
+fff
+------WebKitFormBoundaryJwfATyF8tmxSJnLg
+Content-Disposition: form-data; name="F1"; filename="TODO1"
+Content-Type: application/octet-stream
+
+- SessionClient with referer and cookies support.
+- Client with requests' pipelining support.
+- ProxyHandler similar to FSHandler.
+- WebSockets. See https://tools.ietf.org/html/rfc6455 .
+- HTTP/2.0. See https://tools.ietf.org/html/rfc7540 .
+------WebKitFormBoundaryJwfATyF8tmxSJnLg
+Content-Disposition: form-data; name="F1"; filename="TODO2"
+Content-Type: application/octet-stream
+
+- SessionClient with referer and cookies support.
+- Client with requests' pipelining support.
+- ProxyHandler similar to FSHandler.
+- WebSockets. See https://tools.ietf.org/html/rfc6455 .
+- HTTP/2.0. See https://tools.ietf.org/html/rfc7540 .
+------WebKitFormBoundaryJwfATyF8tmxSJnLg
+Content-Disposition: form-data; name="F2"; filename="TODO3"
+Content-Type: application/octet-stream
+
+- SessionClient with referer and cookies support.
+- Client with requests' pipelining support.
+- ProxyHandler similar to FSHandler.
+- WebSockets. See https://tools.ietf.org/html/rfc6455 .
+- HTTP/2.0. See https://tools.ietf.org/html/rfc7540 .
+
+------WebKitFormBoundaryJwfATyF8tmxSJnLg--
+tailfoobar`
+
+	mr := bytes.NewBufferString(s)
+
+	r := protocol.NewRequest("POST", "/foo", mr)
+	r.Header.SetContentTypeBytes([]byte("multipart/form-data; boundary=----WebKitFormBoundaryJwfATyF8tmxSJnLg"))
+	r.Header.SetContentLength(len(s))
+
+	bindReq := bindRequest{
+		req: r,
+	}
+
+	values, err := bindReq.GetFileHeaders()
+	if err != nil {
+		t.Fatalf("unexpected error: %v", err)
+	}
+
+	fmt.Printf("%v\n", values)
+
+	assert.DeepEqual(t, "TODO1", values["F1"][0].Filename)
+	assert.DeepEqual(t, "TODO2", values["F1"][1].Filename)
+	assert.DeepEqual(t, "TODO3", values["F2"][0].Filename)
+}
diff --git a/pkg/app/server/hertz_test.go b/pkg/app/server/hertz_test.go
index bc3e1a0..3ce91a8 100644
--- a/pkg/app/server/hertz_test.go
+++ b/pkg/app/server/hertz_test.go
@@ -50,7 +50,7 @@ import (
 )
 
 func TestHertz_Run(t *testing.T) {
-	hertz := New(WithHostPorts("127.0.0.1:6666"))
+	hertz := Default(WithHostPorts("127.0.0.1:6666"))
 	hertz.GET("/test", func(c context.Context, ctx *app.RequestContext) {
 		time.Sleep(time.Second)
 		path := ctx.Request.URI().PathOriginal()
@@ -62,6 +62,8 @@ func TestHertz_Run(t *testing.T) {
 		atomic.StoreUint32(&testint, 1)
 	})
 
+	assert.Assert(t, len(hertz.Handlers) == 1)
+
 	go hertz.Spin()
 	time.Sleep(100 * time.Millisecond)
 
@@ -145,7 +147,7 @@ func TestHertz_GracefulShutdown(t *testing.T) {
 }
 
 func TestLoadHTMLGlob(t *testing.T) {
-	engine := New(WithMaxRequestBodySize(15), WithHostPorts("127.0.0.1:8890"))
+	engine := New(WithMaxRequestBodySize(15), WithHostPorts("127.0.0.1:8893"))
 	engine.Delims("{[{", "}]}")
 	engine.LoadHTMLGlob("../../common/testdata/template/index.tmpl")
 	engine.GET("/index", func(c context.Context, ctx *app.RequestContext) {
@@ -155,7 +157,7 @@ func TestLoadHTMLGlob(t *testing.T) {
 	})
 	go engine.Run()
 	time.Sleep(200 * time.Millisecond)
-	resp, _ := http.Get("http://127.0.0.1:8890/index")
+	resp, _ := http.Get("http://127.0.0.1:8893/index")
 	assert.DeepEqual(t, consts.StatusOK, resp.StatusCode)
 	b := make([]byte, 100)
 	n, _ := resp.Body.Read(b)
@@ -690,7 +692,7 @@ type CloseWithoutResetBuffer interface {
 }
 
 func TestOnprepare(t *testing.T) {
-	h := New(
+	h1 := New(
 		WithHostPorts("localhost:9229"),
 		WithOnConnect(func(ctx context.Context, conn network.Conn) context.Context {
 			b, err := conn.Peek(3)
@@ -703,42 +705,42 @@ func TestOnprepare(t *testing.T) {
 			}
 			return ctx
 		}))
-	h.GET("/ping", func(ctx context.Context, c *app.RequestContext) {
+	h1.GET("/ping", func(ctx context.Context, c *app.RequestContext) {
 		c.JSON(consts.StatusOK, utils.H{"ping": "pong"})
 	})
 
-	go h.Spin()
+	go h1.Spin()
 	time.Sleep(time.Second)
 	_, _, err := c.Get(context.Background(), nil, "http://127.0.0.1:9229/ping")
 	assert.DeepEqual(t, "the server closed connection before returning the first response byte. Make sure the server returns 'Connection: close' response header before closing the connection", err.Error())
 
-	h = New(
+	h2 := New(
 		WithOnAccept(func(conn net.Conn) context.Context {
 			conn.Close()
 			return context.Background()
 		}),
 		WithHostPorts("localhost:9230"))
-	h.GET("/ping", func(ctx context.Context, c *app.RequestContext) {
+	h2.GET("/ping", func(ctx context.Context, c *app.RequestContext) {
 		c.JSON(consts.StatusOK, utils.H{"ping": "pong"})
 	})
-	go h.Spin()
+	go h2.Spin()
 	time.Sleep(time.Second)
 	_, _, err = c.Get(context.Background(), nil, "http://127.0.0.1:9230/ping")
 	if err == nil {
 		t.Fatalf("err should not be nil")
 	}
 
-	h = New(
+	h3 := New(
 		WithOnAccept(func(conn net.Conn) context.Context {
 			assert.DeepEqual(t, conn.LocalAddr().String(), "127.0.0.1:9231")
 			return context.Background()
 		}),
 		WithHostPorts("localhost:9231"),
 		WithTransport(standard.NewTransporter))
-	h.GET("/ping", func(ctx context.Context, c *app.RequestContext) {
+	h3.GET("/ping", func(ctx context.Context, c *app.RequestContext) {
 		c.JSON(consts.StatusOK, utils.H{"ping": "pong"})
 	})
-	go h.Spin()
+	go h3.Spin()
 	time.Sleep(time.Second)
 	c.Get(context.Background(), nil, "http://127.0.0.1:9231/ping")
 }
diff --git a/pkg/app/server/option_test.go b/pkg/app/server/option_test.go
index d9abda2..aef554c 100644
--- a/pkg/app/server/option_test.go
+++ b/pkg/app/server/option_test.go
@@ -17,6 +17,10 @@
 package server
 
 import (
+	"context"
+	"net"
+	"reflect"
+	"syscall"
 	"testing"
 	"time"
 
@@ -25,6 +29,7 @@ import (
 	"github.com/cloudwego/hertz/pkg/common/test/assert"
 	"github.com/cloudwego/hertz/pkg/common/tracer/stats"
 	"github.com/cloudwego/hertz/pkg/common/utils"
+	"github.com/cloudwego/hertz/pkg/network"
 )
 
 func TestOptions(t *testing.T) {
@@ -33,6 +38,12 @@ func TestOptions(t *testing.T) {
 		Weight:      10,
 		Addr:        utils.NewNetAddr("tcp", ":8888"),
 	}
+	cfg := &net.ListenConfig{Control: func(network, address string, c syscall.RawConn) error {
+		return c.Control(func(fd uintptr) {})
+	}}
+	transporter := func(options *config.Options) network.Transporter {
+		return &mockTransporter{}
+	}
 	opt := config.NewOptions([]config.Option{
 		WithReadTimeout(time.Second),
 		WithWriteTimeout(time.Second),
@@ -62,6 +73,8 @@ func TestOptions(t *testing.T) {
 		WithTraceLevel(stats.LevelDisabled),
 		WithRegistry(nil, info),
 		WithAutoReloadRender(true, 5*time.Second),
+		WithListenConfig(cfg),
+		WithAltTransport(transporter),
 	})
 	assert.DeepEqual(t, opt.ReadTimeout, time.Second)
 	assert.DeepEqual(t, opt.WriteTimeout, time.Second)
@@ -92,6 +105,8 @@ func TestOptions(t *testing.T) {
 	assert.DeepEqual(t, opt.Registry, nil)
 	assert.DeepEqual(t, opt.AutoReloadRender, true)
 	assert.DeepEqual(t, opt.AutoReloadInterval, 5*time.Second)
+	assert.DeepEqual(t, opt.ListenConfig, cfg)
+	assert.Assert(t, reflect.TypeOf(opt.AltTransporterNewer) == reflect.TypeOf(transporter))
 }
 
 func TestDefaultOptions(t *testing.T) {
@@ -125,3 +140,17 @@ func TestDefaultOptions(t *testing.T) {
 	assert.DeepEqual(t, opt.AutoReloadRender, false)
 	assert.DeepEqual(t, opt.AutoReloadInterval, time.Duration(0))
 }
+
+type mockTransporter struct{}
+
+func (m *mockTransporter) ListenAndServe(onData network.OnData) (err error) {
+	panic("implement me")
+}
+
+func (m *mockTransporter) Close() error {
+	panic("implement me")
+}
+
+func (m *mockTransporter) Shutdown(ctx context.Context) error {
+	panic("implement me")
+}
diff --git a/pkg/app/server/render/html_test.go b/pkg/app/server/render/html_test.go
index eb8f36e..d474f1c 100644
--- a/pkg/app/server/render/html_test.go
+++ b/pkg/app/server/render/html_test.go
@@ -17,14 +17,24 @@
 package render
 
 import (
+	"html/template"
 	"io/ioutil"
 	"os"
 	"testing"
 	"time"
+
+	"github.com/cloudwego/hertz/pkg/common/test/assert"
+	"github.com/cloudwego/hertz/pkg/common/utils"
+	"github.com/cloudwego/hertz/pkg/protocol"
 )
 
 func TestHTMLDebug_StartChecker_timer(t *testing.T) {
-	render := &HTMLDebug{RefreshInterval: time.Second}
+	render := &HTMLDebug{
+		RefreshInterval: time.Second,
+		Delims:          Delims{Left: "{[{", Right: "}]}"},
+		FuncMap:         template.FuncMap{},
+		Files:           []string{"../../../common/testdata/template/index.tmpl"},
+	}
 	select {
 	case <-render.reloadCh:
 		t.Fatalf("should not be triggered")
@@ -35,6 +45,7 @@ func TestHTMLDebug_StartChecker_timer(t *testing.T) {
 	case <-time.After(render.RefreshInterval + 500*time.Millisecond):
 		t.Fatalf("should be triggered in 1.5 second")
 	case <-render.reloadCh:
+		render.reload()
 	}
 }
 
@@ -64,3 +75,55 @@ func TestHTMLDebug_StartChecker_fs_watcher(t *testing.T) {
 	default:
 	}
 }
+
+func TestRenderHTML(t *testing.T) {
+	resp := &protocol.Response{}
+
+	tmpl := template.Must(template.New("").
+		Delims("{[{", "}]}").
+		Funcs(template.FuncMap{}).
+		ParseFiles("../../../common/testdata/template/index.tmpl"))
+
+	r := &HTMLProduction{Template: tmpl}
+
+	html := r.Instance("index.tmpl", utils.H{
+		"title": "Main website",
+	})
+
+	err := r.Close()
+	assert.Nil(t, err)
+
+	html.WriteContentType(resp)
+	assert.DeepEqual(t, []byte("text/html; charset=utf-8"), resp.Header.Peek("Content-Type"))
+
+	err = html.Render(resp)
+
+	assert.Nil(t, err)
+	assert.DeepEqual(t, []byte("text/html; charset=utf-8"), resp.Header.Peek("Content-Type"))
+	assert.DeepEqual(t, []byte("<html><h1>Main website</h1></html>"), resp.Body())
+
+	respDebug := &protocol.Response{}
+
+	rDebug := &HTMLDebug{
+		Template: tmpl,
+		Delims:   Delims{Left: "{[{", Right: "}]}"},
+		FuncMap:  template.FuncMap{},
+		Files:    []string{"../../../common/testdata/template/index.tmpl"},
+	}
+
+	htmlDebug := rDebug.Instance("index.tmpl", utils.H{
+		"title": "Main website",
+	})
+
+	err = rDebug.Close()
+	assert.Nil(t, err)
+
+	htmlDebug.WriteContentType(respDebug)
+	assert.DeepEqual(t, []byte("text/html; charset=utf-8"), respDebug.Header.Peek("Content-Type"))
+
+	err = htmlDebug.Render(respDebug)
+
+	assert.Nil(t, err)
+	assert.DeepEqual(t, []byte("text/html; charset=utf-8"), respDebug.Header.Peek("Content-Type"))
+	assert.DeepEqual(t, []byte("<html><h1>Main website</h1></html>"), respDebug.Body())
+}
diff --git a/pkg/app/server/render/render_test.go b/pkg/app/server/render/render_test.go
index 338d151..0669cb4 100644
--- a/pkg/app/server/render/render_test.go
+++ b/pkg/app/server/render/render_test.go
@@ -47,6 +47,7 @@ import (
 
 	"github.com/bytedance/sonic"
 	"github.com/cloudwego/hertz/pkg/common/test/assert"
+	"github.com/cloudwego/hertz/pkg/common/testdata/proto"
 	"github.com/cloudwego/hertz/pkg/protocol"
 	"github.com/cloudwego/hertz/pkg/protocol/consts"
 )
@@ -96,8 +97,59 @@ func TestRenderJSONError(t *testing.T) {
 	resp := &protocol.Response{}
 	data := make(chan int)
 
+	err := (JSONRender{data}).Render(resp)
+	// json: unsupported type: chan int
+	assert.NotNil(t, err)
+}
+
+func TestRenderPureJSON(t *testing.T) {
+	resp := &protocol.Response{}
+	data := map[string]interface{}{
+		"foo":  "bar",
+		"html": "<b>",
+	}
+
+	(PureJSON{data}).WriteContentType(resp)
+	assert.DeepEqual(t, []byte(consts.MIMEApplicationJSONUTF8), resp.Header.Peek("Content-Type"))
+
+	err := (PureJSON{data}).Render(resp)
+
+	assert.Nil(t, err)
+
+	assert.DeepEqual(t, []byte("{\"foo\":\"bar\",\"html\":\"<b>\"}\n"), resp.Body())
+	assert.DeepEqual(t, []byte(consts.MIMEApplicationJSONUTF8), resp.Header.Peek("Content-Type"))
+}
+
+func TestRenderPureJSONError(t *testing.T) {
+	resp := &protocol.Response{}
+	data := make(chan int)
+
+	err := (PureJSON{data}).Render(resp)
 	// json: unsupported type: chan int
-	assert.NotNil(t, func() { (JSONRender{data}).Render(resp) })
+	assert.NotNil(t, err)
+}
+
+func TestRenderProtobuf(t *testing.T) {
+	resp := &protocol.Response{}
+	data := proto.TestStruct{Body: []byte("Hello World")}
+
+	(ProtoBuf{&data}).WriteContentType(resp)
+	assert.DeepEqual(t, []byte("application/x-protobuf"), resp.Header.Peek("Content-Type"))
+
+	err := (ProtoBuf{&data}).Render(resp)
+
+	assert.Nil(t, err)
+	assert.DeepEqual(t, []byte("\n\vHello World"), resp.Body())
+	assert.DeepEqual(t, []byte("application/x-protobuf"), resp.Header.Peek("Content-Type"))
+}
+
+func TestRenderProtobufError(t *testing.T) {
+	resp := &protocol.Response{}
+	data := proto.Test{}
+
+	err := (ProtoBuf{&data}).Render(resp)
+
+	assert.NotNil(t, err)
 }
 
 func TestRenderString(t *testing.T) {
@@ -162,6 +214,15 @@ func TestRenderXML(t *testing.T) {
 	assert.DeepEqual(t, []byte(consts.MIMEApplicationXMLUTF8), resp.Header.Peek("Content-Type"))
 }
 
+func TestRenderXMLError(t *testing.T) {
+	resp := &protocol.Response{}
+	data := make(chan int)
+
+	err := (XML{data}).Render(resp)
+
+	assert.NotNil(t, err)
+}
+
 func TestRenderIndentedJSON(t *testing.T) {
 	data := map[string]interface{}{
 		"foo":  "bar",
diff --git a/pkg/protocol/http1/resp/writer_test.go b/pkg/protocol/http1/resp/writer_test.go
index bca2d43..b57281a 100644
--- a/pkg/protocol/http1/resp/writer_test.go
+++ b/pkg/protocol/http1/resp/writer_test.go
@@ -51,3 +51,16 @@ func TestNewChunkedBodyWriter1(t *testing.T) {
 	assert.True(t, strings.Contains(string(out), "5"+string(bytestr.StrCRLF)+"hello"))
 	assert.True(t, strings.Contains(string(out), "0"+string(bytestr.StrCRLF)+string(bytestr.StrCRLF)))
 }
+
+func TestNewChunkedBodyWriterNoData(t *testing.T) {
+	response := protocol.AcquireResponse()
+	response.Header.Set("Foo", "Bar")
+	mockConn := mock.NewConn("")
+	w := NewChunkedBodyWriter(response, mockConn)
+	w.Finalize()
+	w.Flush()
+	out, _ := mockConn.WriterRecorder().ReadBinary(mockConn.WriterRecorder().WroteLen())
+	assert.True(t, strings.Contains(string(out), "Transfer-Encoding: chunked"))
+	assert.True(t, strings.Contains(string(out), "Foo: Bar"))
+	assert.True(t, strings.Contains(string(out), "0"+string(bytestr.StrCRLF)+string(bytestr.StrCRLF)))
+}