Repository: cloudwego/hertz
Pull request #640 (2023-02-27 06:54:01)

chore: release v0.6.0


Patch:
diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml
index 4e6b54f..683bd08 100644
--- a/.github/workflows/pr-check.yml
+++ b/.github/workflows/pr-check.yml
@@ -24,13 +24,6 @@ jobs:
         with:
           go-version: 1.19
 
-      - uses: actions/cache@v3
-        with:
-          path: ~/go/pkg/mod
-          key: reviewdog-${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
-          restore-keys: |
-            reviewdog-${{ runner.os }}-go-
-
       - uses: reviewdog/action-staticcheck@v1
         with:
           github_token: ${{ secrets.github_token }}
diff --git a/README.md b/README.md
index 387b2f4..ea347fd 100644
--- a/README.md
+++ b/README.md
@@ -63,6 +63,7 @@ Hertz [həːts] is a high-usability, high-performance and high-extensibility Gol
 | [Autotls](https://github.com/hertz-contrib/autotls)                                                | Make Hertz support Let's Encrypt.                                                                                                                                              |
 | [Http2](https://github.com/hertz-contrib/http2)                                                    | HTTP2 support for Hertz.                                                                                                                                                       |
 | [Websocket](https://github.com/hertz-contrib/websocket)                                            | Enable Hertz to support the Websocket protocol.                                                                                                                                |
+| [Etag](https://github.com/hertz-contrib/etag)                                                      | Support ETag (or entity tag) HTTP response header for Hertz.                                                                                                                   |
 | [Limiter](https://github.com/hertz-contrib/limiter)                                                | Provides a current limiter based on the bbr algorithm.                                                                                                                         |
 | [Monitor-prometheus](https://github.com/hertz-contrib/monitor-prometheus)                          | Provides service monitoring based on Prometheus.                                                                                                                               |
 | [Obs-opentelemetry](https://github.com/hertz-contrib/obs-opentelemetry)                            | Hertz's Opentelemetry extension that supports Metric, Logger, Tracing and works out of the box.                                                                                |
@@ -76,6 +77,7 @@ Hertz [həːts] is a high-usability, high-performance and high-extensibility Gol
 | [Keyauth](https://github.com/hertz-contrib/keyauth)                                                | Provides token-based authentication.                                                                                                                                           |
 | [Requestid](https://github.com/hertz-contrib/requestid)                                            | Add request id in response.                                                                                                                                                    |
 | [Sessions](https://github.com/hertz-contrib/sessions)                                              | Session middleware with multi-state store support.                                                                                                                             |
+| [Casbin](https://github.com/hertz-contrib/casbin)                                                  | Supports various access control models by Casbin.                                                                                                                              |
 | [Cors](https://github.com/hertz-contrib/cors)                                                      | Provides cross-domain resource sharing support.                                                                                                                                |
 | [Csrf](https://github.com/hertz-contrib/csrf)                                                      | Csrf middleware is used to prevent cross-site request forgery attacks.                                                                                                         |
 | [Secure](https://github.com/hertz-contrib/secure)                                                  | Secure middleware with multiple configuration items.                                                                                                                           |
@@ -87,6 +89,7 @@ Hertz [həːts] is a high-usability, high-performance and high-extensibility Gol
 | [Recovery](https://github.com/cloudwego/hertz/tree/develop/pkg/app/middlewares/server/recovery)    | Recovery middleware for Hertz.                                                                                                                                                 |
 | [Reverseproxy](https://github.com/hertz-contrib/reverseproxy)                                      | Implement a reverse proxy.                                                                                                                                                     |
 | [Swagger](https://github.com/hertz-contrib/swagger)                                                | Automatically generate RESTful API documentation with Swagger 2.0.                                                                                                             |
+| [Cache](https://github.com/hertz-contrib/cache)                                                    | Hertz middleware for cache HTTP response with multi-backend support                                                                                                            |
 
 ## Blogs
 - [ByteDance Practice on Go Network Library](https://www.cloudwego.io/blog/2021/10/09/bytedance-practices-on-go-network-library/)
diff --git a/README_cn.md b/README_cn.md
index 2ca5823..a1c13fb 100644
--- a/README_cn.md
+++ b/README_cn.md
@@ -63,9 +63,10 @@ Hertz[həːts] 是一个 Golang 微服务 HTTP 框架，在设计之初参考了
 | [Autotls](https://github.com/hertz-contrib/autotls)                                                | 为 Hertz 支持 Let's Encrypt 。                                                                        |
 | [Http2](https://github.com/hertz-contrib/http2)                                                    | 提供对 HTTP2 的支持。                                                                                    |
 | [Websocket](https://github.com/hertz-contrib/websocket)                                            | 使 Hertz 支持 Websocket 协议。                                                                          |
+| [Etag](https://github.com/hertz-contrib/etag)                                                      | 提供 ETag HTTP 响应标头。                                                                                |
 | [Limiter](https://github.com/hertz-contrib/limiter)                                                | 提供了基于 bbr 算法的限流器。                                                                                 |
 | [Monitor-prometheus](https://github.com/hertz-contrib/monitor-prometheus)                          | 提供基于 Prometheus 服务监控功能。                                                                           |
-| [Obs-opentelemetry](https://github.com/hertz-contrib/obs-opentelemetry)                            | Hertz 的 Opentelemetry 扩展，支持 Metric、Logger、Tracing并且达到开箱即用。                                        |
+| [Obs-opentelemetry](https://github.com/hertz-contrib/obs-opentelemetry)                            | Hertz 的 Opentelemetry 扩展，支持 Metric、Logger、Tracing 并且达到开箱即用。                                       |
 | [Opensergo](https://github.com/hertz-contrib/opensergo)                                            | Opensergo 扩展。                                                                                     |
 | [Pprof](https://github.com/hertz-contrib/pprof)                                                    | Hertz 集成 Pprof 的扩展。                                                                               |
 | [Registry](https://github.com/hertz-contrib/registry)                                              | 提供服务注册与发现功能。到现在为止，支持的服务发现拓展有 nacos， consul， etcd， eureka， polaris， servicecomb， zookeeper， redis。 |
@@ -76,6 +77,7 @@ Hertz[həːts] 是一个 Golang 微服务 HTTP 框架，在设计之初参考了
 | [Keyauth](https://github.com/hertz-contrib/keyauth)                                                | 提供基于 token 的身份验证。                                                                                 |
 | [Requestid](https://github.com/hertz-contrib/requestid)                                            | 在 response 中添加 request id。                                                                        |
 | [Sessions](https://github.com/hertz-contrib/sessions)                                              | 具有多状态存储支持的 Session 中间件。                                                                           |
+| [Casbin](https://github.com/hertz-contrib/casbin)                                                  | 通过 Casbin 支持各种访问控制模型。                                                                             |
 | [Cors](https://github.com/hertz-contrib/cors)                                                      | 提供跨域资源共享支持。                                                                                       |
 | [Csrf](https://github.com/hertz-contrib/csrf)                                                      | Csrf 中间件用于防止跨站点请求伪造攻击。                                                                            |
 | [Secure](https://github.com/hertz-contrib/secure)                                                  | 具有多配置项的 Secure 中间件。                                                                               |
@@ -87,6 +89,7 @@ Hertz[həːts] 是一个 Golang 微服务 HTTP 框架，在设计之初参考了
 | [Recovery](https://github.com/cloudwego/hertz/tree/develop/pkg/app/middlewares/server/recovery)    | Hertz 的异常恢复中间件。                                                                                   |
 | [Reverseproxy](https://github.com/hertz-contrib/reverseproxy)                                      | 实现反向代理。                                                                                           |
 | [Swagger](https://github.com/hertz-contrib/swagger)                                                | 使用 Swagger 2.0 自动生成 RESTful API 文档。                                                               |
+| [Cache](https://github.com/hertz-contrib/cache)                                                    | 用于缓存 HTTP 接口内容的 Hertz 中间件，支持多种客户端。                                                                |
 
 ## 相关文章
 - [字节跳动在 Go 网络库上的实践](https://www.cloudwego.io/blog/2021/10/09/bytedance-practices-on-go-network-library/)
diff --git a/cmd/hz/app/app.go b/cmd/hz/app/app.go
index 1f639a9..835ee03 100644
--- a/cmd/hz/app/app.go
+++ b/cmd/hz/app/app.go
@@ -44,6 +44,15 @@ func New(c *cli.Context) error {
 	setLogVerbose(args.Verbose)
 	logs.Debugf("args: %#v\n", args)
 
+	exist, err := util.PathExist(filepath.Join(args.OutDir, meta.ManifestFile))
+	if err != nil {
+		return cli.Exit(err, meta.LoadError)
+	}
+
+	if exist && !args.ForceNew {
+		return cli.Exit(fmt.Errorf("the current is already a hertz project, if you want to regenerate it you can specify \"-force\""), meta.LoadError)
+	}
+
 	err = GenerateLayout(args)
 	if err != nil {
 		return cli.Exit(err, meta.GenerateLayoutError)
@@ -53,6 +62,17 @@ func New(c *cli.Context) error {
 	if err != nil {
 		return cli.Exit(err, meta.PluginError)
 	}
+	// ".hz" file converges to the hz tool
+	manifest := new(meta.Manifest)
+	args.InitManifest(manifest)
+	err = manifest.Persist(args.OutDir)
+	if err != nil {
+		return cli.Exit(fmt.Errorf("persist manifest failed: %v", err), meta.PersistError)
+	}
+	if !args.NeedGoMod && args.IsNew() {
+		fmt.Println(meta.AddThriftReplace)
+	}
+
 	return nil
 }
 
@@ -62,25 +82,28 @@ func Update(c *cli.Context) error {
 	if err != nil {
 		return cli.Exit(err, meta.LoadError)
 	}
+	setLogVerbose(args.Verbose)
+	logs.Debugf("Args: %#v\n", args)
+
 	manifest := new(meta.Manifest)
-	err = manifest.Validate(args.OutDir)
+	err = manifest.InitAndValidate(args.OutDir)
 	if err != nil {
 		return cli.Exit(err, meta.LoadError)
 	}
-
-	setLogVerbose(args.Verbose)
-	logs.Debugf("Args: %#v\n", args)
+	// update argument by ".hz", can automatically get "handler_dir"/"model_dir"/"router_dir"
+	args.UpdateByManifest(manifest)
 
 	err = TriggerPlugin(args)
 	if err != nil {
 		return cli.Exit(err, meta.PluginError)
 	}
-
-	manifest.Version = meta.GoVersion
-	err = manifest.Persist(".")
+	// If the "handler_dir"/"model_dir" is updated, write it back to ".hz"
+	args.UpdateManifest(manifest)
+	err = manifest.Persist(args.OutDir)
 	if err != nil {
 		return cli.Exit(fmt.Errorf("persist manifest failed: %v", err), meta.PersistError)
 	}
+
 	return nil
 }
 
@@ -139,10 +162,13 @@ func Init() *cli.App {
 	moduleFlag := cli.StringFlag{Name: "module", Aliases: []string{"mod"}, Usage: "Specify the Go module name.", Destination: &globalArgs.Gomod}
 	serviceNameFlag := cli.StringFlag{Name: "service", Usage: "Specify the service name.", Destination: &globalArgs.ServiceName}
 	outDirFlag := cli.StringFlag{Name: "out_dir", Usage: "Specify the project path.", Destination: &globalArgs.OutDir}
-	handlerDirFlag := cli.StringFlag{Name: "handler_dir", Usage: "Specify the handler path.", Destination: &globalArgs.HandlerDir}
-	modelDirFlag := cli.StringFlag{Name: "model_dir", Usage: "Specify the model path.", Destination: &globalArgs.ModelDir}
+	handlerDirFlag := cli.StringFlag{Name: "handler_dir", Usage: "Specify the handler relative path (based on \"out_dir\").", Destination: &globalArgs.HandlerDir}
+	modelDirFlag := cli.StringFlag{Name: "model_dir", Usage: "Specify the model relative path (based on \"out_dir\").", Destination: &globalArgs.ModelDir}
+	routerDirFlag := cli.StringFlag{Name: "router_dir", Usage: "Specify the router relative path (based on \"out_dir\").", Destination: &globalArgs.RouterDir}
+	useFlag := cli.StringFlag{Name: "use", Usage: "Specify the model package to import for handler.", Destination: &globalArgs.Use}
 	baseDomainFlag := cli.StringFlag{Name: "base_domain", Usage: "Specify the request domain.", Destination: &globalArgs.BaseDomain}
 	clientDirFlag := cli.StringFlag{Name: "client_dir", Usage: "Specify the client path. If not specified, IDL generated path is used for 'client' command; no client code is generated for 'new' command", Destination: &globalArgs.ClientDir}
+	forceClientDirFlag := cli.StringFlag{Name: "force_client_dir", Usage: "Specify the client path, and won't use namespaces as subpaths", Destination: &globalArgs.ForceClientDir}
 
 	optPkgFlag := cli.StringSliceFlag{Name: "option_package", Aliases: []string{"P"}, Usage: "Specify the package path. ({include_path}={import_path})"}
 	includesFlag := cli.StringSliceFlag{Name: "proto_path", Aliases: []string{"I"}, Usage: "Add an IDL search path for includes. (Valid only if idl is protobuf)"}
@@ -152,13 +178,15 @@ func Init() *cli.App {
 	thriftPluginsFlag := cli.StringSliceFlag{Name: "thrift-plugins", Usage: "Specify plugins for the thriftgo. ({plugin_name}:{options})"}
 	protoPluginsFlag := cli.StringSliceFlag{Name: "protoc-plugins", Usage: "Specify plugins for the protoc. ({plugin_name}:{options}:{out_dir})"}
 	noRecurseFlag := cli.BoolFlag{Name: "no_recurse", Usage: "Generate master model only.", Destination: &globalArgs.NoRecurse}
+	forceNewFlag := cli.BoolFlag{Name: "force", Aliases: []string{"f"}, Usage: "Force new a project, which will overwrite the generated files", Destination: &globalArgs.ForceNew}
 
 	jsonEnumStrFlag := cli.BoolFlag{Name: "json_enumstr", Usage: "Use string instead of num for json enums when idl is thrift.", Destination: &globalArgs.JSONEnumStr}
 	unsetOmitemptyFlag := cli.BoolFlag{Name: "unset_omitempty", Usage: "Remove 'omitempty' tag for generated struct.", Destination: &globalArgs.UnsetOmitempty}
 	protoCamelJSONTag := cli.BoolFlag{Name: "pb_camel_json_tag", Usage: "Convert Name style for json tag to camel(Only works protobuf).", Destination: &globalArgs.ProtobufCamelJSONTag}
 	snakeNameFlag := cli.BoolFlag{Name: "snake_tag", Usage: "Use snake_case style naming for tags. (Only works for 'form', 'query', 'json')", Destination: &globalArgs.SnakeName}
-	customLayout := cli.StringFlag{Name: "customize_layout", Usage: "Specify the layout template. ({{Template Profile}}:{{Rendering Data}})", Destination: &globalArgs.CustomizeLayout}
-	customPackage := cli.StringFlag{Name: "customize_package", Usage: "Specify the package template. ({{Template Profile}}:)", Destination: &globalArgs.CustomizePackage}
+	customLayout := cli.StringFlag{Name: "customize_layout", Usage: "Specify the path for layout template.", Destination: &globalArgs.CustomizeLayout}
+	customLayoutData := cli.StringFlag{Name: "customize_layout_data_path", Usage: "Specify the path for layout template render data.", Destination: &globalArgs.CustomizeLayoutData}
+	customPackage := cli.StringFlag{Name: "customize_package", Usage: "Specify the path for package template.", Destination: &globalArgs.CustomizePackage}
 	handlerByMethod := cli.BoolFlag{Name: "handler_by_method", Usage: "Generate a separate handler file for each method.", Destination: &globalArgs.HandlerByMethod}
 
 	// app
@@ -186,13 +214,16 @@ func Init() *cli.App {
 				&outDirFlag,
 				&handlerDirFlag,
 				&modelDirFlag,
+				&routerDirFlag,
 				&clientDirFlag,
+				&useFlag,
 
 				&includesFlag,
 				&thriftOptionsFlag,
 				&protoOptionsFlag,
 				&optPkgFlag,
 				&noRecurseFlag,
+				&forceNewFlag,
 
 				&jsonEnumStrFlag,
 				&unsetOmitemptyFlag,
@@ -200,6 +231,7 @@ func Init() *cli.App {
 				&snakeNameFlag,
 				&excludeFilesFlag,
 				&customLayout,
+				&customLayoutData,
 				&customPackage,
 				&handlerByMethod,
 				&protoPluginsFlag,
@@ -217,6 +249,7 @@ func Init() *cli.App {
 				&handlerDirFlag,
 				&modelDirFlag,
 				&clientDirFlag,
+				&useFlag,
 
 				&includesFlag,
 				&thriftOptionsFlag,
@@ -267,6 +300,8 @@ func Init() *cli.App {
 				&baseDomainFlag,
 				&modelDirFlag,
 				&clientDirFlag,
+				&useFlag,
+				&forceClientDirFlag,
 
 				&includesFlag,
 				&thriftOptionsFlag,
@@ -308,6 +343,10 @@ func GenerateLayout(args *config.Argument) error {
 		ServiceName:     args.ServiceName,
 		UseApacheThrift: args.IdlType == meta.IdlThrift,
 		HasIdl:          0 != len(args.IdlPaths),
+		ModelDir:        args.ModelDir,
+		HandlerDir:      args.HandlerDir,
+		RouterDir:       args.RouterDir,
+		NeedGoMod:       args.NeedGoMod,
 	}
 
 	if args.CustomizeLayout == "" {
@@ -318,11 +357,7 @@ func GenerateLayout(args *config.Argument) error {
 		}
 	} else {
 		// generate by customized layout
-		sp := strings.Split(args.CustomizeLayout, ":")
-		if len(sp) != 2 || sp[0] == "" {
-			return errors.New("custom_layout must be like: {{layout_config_path}}:{{template_data_config_path}}")
-		}
-		configPath, dataPath := sp[0], sp[1]
+		configPath, dataPath := args.CustomizeLayout, args.CustomizeLayoutData
 		logs.Infof("get customized layout info, layout_config_path: %s, template_data_path: %s", configPath, dataPath)
 		exist, err := util.PathExist(configPath)
 		if err != nil {
@@ -371,7 +406,10 @@ func TriggerPlugin(args *config.Argument) error {
 	logs.Debugf("begin to trigger plugin, compiler: %s, idl_paths: %v", compiler, args.IdlPaths)
 	buf, err := cmd.CombinedOutput()
 	if err != nil {
-		return fmt.Errorf("plugin %s_gen_hertz returns error: %v, cause:\n%v", compiler, err, string(buf))
+		out := strings.TrimSpace(string(buf))
+		if !strings.HasSuffix(out, meta.TheUseOptionMessage) {
+			return fmt.Errorf("plugin %s_gen_hertz returns error: %v, cause:\n%v", compiler, err, string(buf))
+		}
 	}
 
 	// If len(buf) != 0, the plugin returned the log.
diff --git a/cmd/hz/config/argument.go b/cmd/hz/config/argument.go
index bf15fdf..3e154c8 100644
--- a/cmd/hz/config/argument.go
+++ b/cmd/hz/config/argument.go
@@ -24,20 +24,22 @@ import (
 
 	"github.com/cloudwego/hertz/cmd/hz/meta"
 	"github.com/cloudwego/hertz/cmd/hz/util"
+	"github.com/cloudwego/hertz/cmd/hz/util/logs"
 	"github.com/urfave/cli/v2"
 )
 
 type Argument struct {
 	// Mode              meta.Mode // operating mode（0-compiler, 1-plugin)
-	CmdType    string // command type
-	Verbose    bool   // print verbose log
-	Cwd        string // execution path
-	OutDir     string // output path
-	HandlerDir string // handler path
-	ModelDir   string // model path
-	RouterDir  string // router path
-	ClientDir  string // client path
-	BaseDomain string // request domain
+	CmdType        string // command type
+	Verbose        bool   // print verbose log
+	Cwd            string // execution path
+	OutDir         string // output path
+	HandlerDir     string // handler path
+	ModelDir       string // model path
+	RouterDir      string // router path
+	ClientDir      string // client path
+	BaseDomain     string // request domain
+	ForceClientDir string // client dir (not use namespace as a subpath)
 
 	IdlType   string   // idl type
 	IdlPaths  []string // master idl path
@@ -51,6 +53,8 @@ type Argument struct {
 	Gomod       string
 	Gopkg       string // $GOPATH/src/{{gopkg}}
 	ServiceName string // service name
+	Use         string
+	NeedGoMod   bool
 
 	JSONEnumStr          bool
 	UnsetOmitempty       bool
@@ -63,10 +67,12 @@ type Argument struct {
 	Excludes             []string
 	NoRecurse            bool
 	HandlerByMethod      bool
+	ForceNew             bool
 
-	CustomizeLayout  string
-	CustomizePackage string
-	ModelBackend     string
+	CustomizeLayout     string
+	CustomizeLayoutData string
+	CustomizePackage    string
+	ModelBackend        string
 }
 
 func NewArgument() *Argument {
@@ -115,6 +121,21 @@ func (arg *Argument) parseStringSlice(c *cli.Context) {
 	arg.ProtobufPlugins = c.StringSlice("protoc-plugins")
 }
 
+func (arg *Argument) UpdateByManifest(m *meta.Manifest) {
+	if arg.HandlerDir == "" && m.HandlerDir != "" {
+		logs.Infof("use \"handler_dir\" in \".hz\" as the handler generated dir\n")
+		arg.HandlerDir = m.HandlerDir
+	}
+	if arg.ModelDir == "" && m.ModelDir != "" {
+		logs.Infof("use \"model_dir\" in \".hz\" as the model generated dir\n")
+		arg.ModelDir = m.ModelDir
+	}
+	if len(m.RouterDir) != 0 {
+		logs.Infof("use \"router_dir\" in \".hz\" as the router generated dir\n")
+		arg.RouterDir = m.RouterDir
+	}
+}
+
 // checkPath sets the project path and verifies that the model、handler、router and client path is compliant
 func (arg *Argument) checkPath() error {
 	dir, err := os.Getwd()
@@ -169,10 +190,14 @@ func (arg *Argument) checkIDL() error {
 	return nil
 }
 
-func (arg Argument) IsUpdate() bool {
+func (arg *Argument) IsUpdate() bool {
 	return arg.CmdType == meta.CmdUpdate
 }
 
+func (arg *Argument) IsNew() bool {
+	return arg.CmdType == meta.CmdNew
+}
+
 // checkPackage check and set the gopath、 module and package name
 func (arg *Argument) checkPackage() error {
 	gopath, err := util.GetGOPATH()
@@ -193,21 +218,37 @@ func (arg *Argument) checkPackage() error {
 		} else {
 			arg.Gopkg = gopkg
 		}
-		if arg.Gomod == "" {
+	}
+	if len(arg.Gomod) == 0 { // not specified "go module"
+		// search go.mod recursively
+		module, path, ok := util.SearchGoMod(arg.Cwd, true)
+		if ok {
+			logs.Debugf("find module '%s' form '%s/go.mod'", module, path)
+			rel, err := filepath.Rel(path, arg.Cwd)
+			if err != nil {
+				return fmt.Errorf("can not get relative path, err :%v", err)
+			}
+			module = filepath.Join(module, rel)
+			arg.Gomod = module
+		}
+		if len(arg.Gomod) == 0 {
 			arg.Gomod = arg.Gopkg
 		}
-	}
-	if !arg.IsUpdate() && arg.Gomod == "" {
-		return fmt.Errorf("output directory %s is not under GOPATH/src. Please specify a module name with the '-module' flag", arg.Cwd)
+	} else { // specified "go module"
+		// search go.mod in current path
+		module, path, ok := util.SearchGoMod(arg.Cwd, false)
+		if ok {
+			// go.mod exists in current path
+			if module != arg.Gomod {
+				return fmt.Errorf("module name given by the '-module' option ('%s') is not consist with the name defined in go.mod ('%s' from %s)\n", arg.Gomod, module, path)
+			}
+		} else {
+			arg.NeedGoMod = true
+		}
 	}
 
-	module, path, ok := util.SearchGoMod(".", false)
-	if ok {
-		// go.mod exists
-		if module != arg.Gomod && !arg.IsUpdate() {
-			return fmt.Errorf("module name given by the '-module' option ('%s') is not consist with the name defined in go.mod ('%s' from %s)\n", arg.Gomod, module, path)
-		}
-		arg.Gomod = module
+	if len(arg.Gomod) == 0 {
+		return fmt.Errorf("can not get go module, please specify a module name with the '-module' flag")
 	}
 
 	if len(arg.RawOptPkg) > 0 {
@@ -330,3 +371,21 @@ func (arg *Argument) GetClientDir() (string, error) {
 	}
 	return util.RelativePath(arg.ClientDir)
 }
+
+func (arg *Argument) InitManifest(m *meta.Manifest) {
+	m.Version = meta.Version
+	m.HandlerDir = arg.HandlerDir
+	m.ModelDir = arg.ModelDir
+	m.RouterDir = arg.RouterDir
+}
+
+func (arg *Argument) UpdateManifest(m *meta.Manifest) {
+	m.Version = meta.Version
+	if arg.HandlerDir != m.HandlerDir {
+		m.HandlerDir = arg.HandlerDir
+	}
+	if arg.HandlerDir != m.ModelDir {
+		m.ModelDir = arg.ModelDir
+	}
+	// "router_dir" must not be defined by "update" command
+}
diff --git a/cmd/hz/generator/client.go b/cmd/hz/generator/client.go
index bbc3b5c..ff32c4b 100644
--- a/cmd/hz/generator/client.go
+++ b/cmd/hz/generator/client.go
@@ -18,6 +18,7 @@ package generator
 
 import (
 	"path/filepath"
+	"strings"
 
 	"github.com/cloudwego/hertz/cmd/hz/generator/model"
 	"github.com/cloudwego/hertz/cmd/hz/util"
@@ -45,6 +46,9 @@ type ClientFile struct {
 func (pkgGen *HttpPackageGenerator) genClient(pkg *HttpPackage, clientDir string) error {
 	for _, s := range pkg.Services {
 		cliDir := util.SubDir(clientDir, util.ToSnakeCase(s.Name))
+		if len(pkgGen.ForceClientDir) != 0 {
+			cliDir = pkgGen.ForceClientDir
+		}
 		hertzClientPath := filepath.Join(cliDir, hertzClientTplName)
 		isExist, err := util.PathExist(hertzClientPath)
 		if err != nil {
@@ -56,7 +60,7 @@ func (pkgGen *HttpPackageGenerator) genClient(pkg *HttpPackage, clientDir string
 		}
 		client := ClientFile{
 			FilePath:      filepath.Join(cliDir, util.ToSnakeCase(s.Name)+".go"),
-			PackageName:   util.ToSnakeCase(s.Name),
+			PackageName:   util.ToSnakeCase(filepath.Base(cliDir)),
 			ServiceName:   util.ToCamelCase(s.Name),
 			ClientMethods: s.ClientMethods,
 			BaseDomain:    baseDomain,
@@ -78,6 +82,15 @@ func (pkgGen *HttpPackageGenerator) genClient(pkg *HttpPackage, clientDir string
 				client.Imports[mm.PackageName] = mm
 			}
 		}
+		if len(pkgGen.UseDir) != 0 {
+			oldModelDir := filepath.Clean(filepath.Join(pkgGen.ProjPackage, pkgGen.ModelDir))
+			newModelDir := filepath.Clean(pkgGen.UseDir)
+			for _, m := range client.ClientMethods {
+				for _, mm := range m.Models {
+					mm.Package = strings.Replace(mm.Package, oldModelDir, newModelDir, 1)
+				}
+			}
+		}
 		err = pkgGen.TemplateGenerator.Generate(client, idlClientName, client.FilePath, false)
 		if err != nil {
 			return err
diff --git a/cmd/hz/generator/handler.go b/cmd/hz/generator/handler.go
index c05793e..a39e0fe 100644
--- a/cmd/hz/generator/handler.go
+++ b/cmd/hz/generator/handler.go
@@ -19,7 +19,7 @@ package generator
 import (
 	"bytes"
 	"fmt"
-	"os"
+	"io/ioutil"
 	"path/filepath"
 	"strings"
 
@@ -130,6 +130,16 @@ func (pkgGen *HttpPackageGenerator) processHandler(handler *Handler, root *Route
 		}
 	}
 
+	if len(pkgGen.UseDir) != 0 {
+		oldModelDir := filepath.Clean(filepath.Join(pkgGen.ProjPackage, pkgGen.ModelDir))
+		newModelDir := filepath.Clean(pkgGen.UseDir)
+		for _, m := range handler.Methods {
+			for _, mm := range m.Models {
+				mm.Package = strings.Replace(mm.Package, oldModelDir, newModelDir, 1)
+			}
+		}
+	}
+
 	handler.Format()
 	return nil
 }
@@ -143,7 +153,7 @@ func (pkgGen *HttpPackageGenerator) updateHandler(handler interface{}, handlerTp
 		return pkgGen.TemplateGenerator.Generate(handler, handlerTpl, filePath, noRepeat)
 	}
 
-	file, err := os.ReadFile(filePath)
+	file, err := ioutil.ReadFile(filePath)
 	if err != nil {
 		return err
 	}
diff --git a/cmd/hz/generator/layout.go b/cmd/hz/generator/layout.go
index 472b6a2..c756202 100644
--- a/cmd/hz/generator/layout.go
+++ b/cmd/hz/generator/layout.go
@@ -21,8 +21,9 @@ import (
 	"errors"
 	"fmt"
 	"io/ioutil"
-	"os"
+	"path/filepath"
 	"reflect"
+	"strings"
 
 	"github.com/cloudwego/hertz/cmd/hz/util"
 	"gopkg.in/yaml.v2"
@@ -30,10 +31,15 @@ import (
 
 // Layout contains the basic information of idl
 type Layout struct {
+	OutDir          string
 	GoModule        string
 	ServiceName     string
 	UseApacheThrift bool
 	HasIdl          bool
+	NeedGoMod       bool
+	ModelDir        string
+	HandlerDir      string
+	RouterDir       string
 }
 
 // LayoutGenerator contains the information generated by generating the layout template
@@ -56,7 +62,7 @@ func (lg *LayoutGenerator) Init() error {
 	config := layoutConfig
 	// unmarshal from user-defined config file if it exists
 	if lg.ConfigPath != "" {
-		cdata, err := os.ReadFile(lg.ConfigPath)
+		cdata, err := ioutil.ReadFile(lg.ConfigPath)
 		if err != nil {
 			return fmt.Errorf("read layout config from  %s failed, err: %v", lg.ConfigPath, err.Error())
 		}
@@ -96,6 +102,32 @@ func (lg *LayoutGenerator) GenerateByService(service Layout) error {
 		return err
 	}
 
+	if len(service.HandlerDir) != 0 {
+		// override the default "biz/handler/ping.go" to "HANDLER_DIR/ping.go"
+		defaultPingDir := defaultHandlerDir + sp + "ping.go"
+		if tpl, exist := lg.tpls[defaultPingDir]; exist {
+			delete(lg.tpls, defaultPingDir)
+			newPingDir := filepath.Clean(service.HandlerDir + sp + "ping.go")
+			lg.tpls[newPingDir] = tpl
+		}
+	}
+
+	if len(service.RouterDir) != 0 {
+		defaultRegisterDir := defaultRouterDir + sp + registerTplName
+		if tpl, exist := lg.tpls[defaultRegisterDir]; exist {
+			delete(lg.tpls, defaultRegisterDir)
+			newRegisterDir := filepath.Clean(service.RouterDir + sp + registerTplName)
+			lg.tpls[newRegisterDir] = tpl
+		}
+	}
+
+	if !service.NeedGoMod {
+		gomodFile := "go.mod"
+		if _, exist := lg.tpls["go.mod"]; exist {
+			delete(lg.tpls, gomodFile)
+		}
+	}
+
 	sd, err := serviceToLayoutData(service)
 	if err != nil {
 		return err
@@ -106,7 +138,12 @@ func (lg *LayoutGenerator) GenerateByService(service Layout) error {
 		return err
 	}
 	if service.HasIdl {
-		delete(lg.tpls, defaultRouterDir+sp+registerTplName)
+		for k, _ := range lg.tpls {
+			if strings.Contains(k, registerTplName) {
+				delete(lg.tpls, k)
+				break
+			}
+		}
 	}
 
 	data := map[string]interface{}{
@@ -124,20 +161,38 @@ func serviceToLayoutData(service Layout) (map[string]interface{}, error) {
 	if goMod == "" {
 		return nil, errors.New("please specify go-module")
 	}
+	handlerPkg := filepath.Base(defaultHandlerDir)
+	if len(service.HandlerDir) != 0 {
+		handlerPkg = filepath.Base(service.HandlerDir)
+	}
+	routerPkg := filepath.Base(defaultRouterDir)
+	if len(service.RouterDir) != 0 {
+		routerPkg = filepath.Base(service.RouterDir)
+	}
 
 	return map[string]interface{}{
 		"GoModule":        goMod,
 		"ServiceName":     service.ServiceName,
 		"UseApacheThrift": service.UseApacheThrift,
+		"HandlerPkg":      handlerPkg,
+		"RouterPkg":       routerPkg,
 	}, nil
 }
 
 // serviceToRouterData stores the registers function, router import path, handler import path
 func serviceToRouterData(service Layout) (map[string]interface{}, error) {
+	routerDir := sp + defaultRouterDir
+	handlerDir := sp + defaultHandlerDir
+	if len(service.RouterDir) != 0 {
+		routerDir = sp + service.RouterDir
+	}
+	if len(service.HandlerDir) != 0 {
+		handlerDir = sp + service.HandlerDir
+	}
 	return map[string]interface{}{
 		"Registers":      []string{},
-		"RouterPkgPath":  service.GoModule + util.PathToImport(sp+defaultRouterDir, ""),
-		"HandlerPkgPath": service.GoModule + util.PathToImport(sp+defaultHandlerDir, ""),
+		"RouterPkgPath":  filepath.Clean(service.GoModule + util.PathToImport(routerDir, "")),
+		"HandlerPkgPath": filepath.Clean(service.GoModule + util.PathToImport(handlerDir, "")),
 	}, nil
 }
 
diff --git a/cmd/hz/generator/layout_tpl.go b/cmd/hz/generator/layout_tpl.go
index 3be1543..6f2b4c3 100644
--- a/cmd/hz/generator/layout_tpl.go
+++ b/cmd/hz/generator/layout_tpl.go
@@ -35,8 +35,8 @@ const (
 )
 
 const (
-	routerGenIndex = 9
-	routerIndex    = 10
+	routerGenIndex = 8
+	routerIndex    = 9
 
 	RegisterFile = "router_gen.go"
 )
@@ -122,18 +122,11 @@ _testmain.go
 dumped_hertz_remote_config.json
 		  `,
 		},
-		{
-			Path:   ".hz",
-			Delims: [2]string{"{{", "}}"},
-			Body: `// Code generated by hz. DO NOT EDIT.
-
-hz version: {{.hzVersion}}`,
-		},
 		{
 			Path: defaultHandlerDir + sp + "ping.go",
 			Body: `// Code generated by hertz generator.
 
-package handler
+package {{.HandlerPkg}}
 
 import (
 	"context"
@@ -194,7 +187,7 @@ func customizedRegister(r *server.Hertz){
 			Path: defaultRouterDir + sp + registerTplName,
 			Body: `// Code generated by hertz generator. DO NOT EDIT.
 
-package router
+package {{.RouterPkg}}
 
 import (
 	"github.com/cloudwego/hertz/pkg/app/server"
@@ -204,6 +197,27 @@ import (
 func GeneratedRegister(r *server.Hertz){
 	` + insertPointNew + `
 }
+`,
+		},
+		{
+			Path: "build.sh",
+			Body: `
+#!/bin/bash
+RUN_NAME={{.ServiceName}}
+mkdir -p output/bin
+cp script/* output 2>/dev/null
+chmod +x output/bootstrap.sh
+go build -o output/bin/${RUN_NAME}
+`,
+		},
+		{
+			Path: defaultScriptDir + sp + "bootstrap.sh",
+			Body: `
+#!/bin/bash
+CURDIR=$(cd $(dirname $0); pwd)
+BinaryName={{.ServiceName}}
+echo "$CURDIR/bin/${BinaryName}"
+exec $CURDIR/bin/${BinaryName}
 `,
 		},
 	},
diff --git a/cmd/hz/generator/package.go b/cmd/hz/generator/package.go
index 54ecd41..8613a5b 100644
--- a/cmd/hz/generator/package.go
+++ b/cmd/hz/generator/package.go
@@ -19,7 +19,7 @@ package generator
 import (
 	"errors"
 	"fmt"
-	"os"
+	"io/ioutil"
 	"path/filepath"
 	"reflect"
 	"text/template"
@@ -55,8 +55,10 @@ type HttpPackageGenerator struct {
 	HandlerDir      string
 	RouterDir       string
 	ModelDir        string
+	UseDir          string
 	ClientDir       string
 	IdlClientDir    string
+	ForceClientDir  string
 	NeedModel       bool
 	HandlerByMethod bool
 	BaseDomain      string
@@ -73,7 +75,7 @@ func (pkgGen *HttpPackageGenerator) Init() error {
 	customConfig := TemplateConfig{}
 	// unmarshal from user-defined config file if it exists
 	if pkgGen.ConfigPath != "" {
-		cdata, err := os.ReadFile(pkgGen.ConfigPath)
+		cdata, err := ioutil.ReadFile(pkgGen.ConfigPath)
 		if err != nil {
 			return fmt.Errorf("read layout config from  %s failed, err: %v", pkgGen.ConfigPath, err.Error())
 		}
diff --git a/cmd/hz/generator/package_tpl.go b/cmd/hz/generator/package_tpl.go
index e313d91..f2a0f2e 100644
--- a/cmd/hz/generator/package_tpl.go
+++ b/cmd/hz/generator/package_tpl.go
@@ -146,17 +146,17 @@ func Register(r *server.Hertz) {
 			Path: defaultRouterDir + sp + registerTplName,
 			Body: `// Code generated by hertz generator. DO NOT EDIT.
 
-package router
+package {{.PackageName}}
 
 import (
 	"github.com/cloudwego/hertz/pkg/app/server"
-	{{$.PkgAlias}} "{{$.Pkg}}"
+	{{$.DepPkgAlias}} "{{$.DepPkg}}"
 )
 
 // GeneratedRegister registers routers generated by IDL.
 func GeneratedRegister(r *server.Hertz){
 	` + insertPointNew + `
-	{{$.PkgAlias}}.Register(r)
+	{{$.DepPkgAlias}}.Register(r)
 }
 `,
 		},
@@ -710,7 +710,7 @@ func parseRequestHeader(c *cli, r *request) error {
 		hdr[k] = append(hdr[k], r.header[k]...)
 	}
 
-	if len(r.formParam) != 0 && len(r.fileParam) != 0 {
+	if len(r.formParam) != 0 || len(r.fileParam) != 0 {
 		hdr.Add(hdrContentTypeKey, formContentType)
 	}
 
@@ -857,6 +857,7 @@ package {{.PackageName}}
 
 import (
 	"context"
+	"fmt"
 
 	"github.com/cloudwego/hertz/pkg/common/config"
 	"github.com/cloudwego/hertz/pkg/protocol"
@@ -865,6 +866,11 @@ import (
 {{- end}}
 )
 
+// unused protection
+var (
+	_ = fmt.Formatter(nil)
+)
+
 type Client interface {
 	{{range $_, $MethodInfo := .ClientMethods}}
 		{{$MethodInfo.Name}}(context context.Context, req *{{$MethodInfo.RequestTypeName}}, reqOpt ...config.RequestOption) (resp *{{$MethodInfo.ReturnTypeName}}, rawResponse *protocol.Response, err error)
@@ -922,6 +928,11 @@ func (s *{{$.ServiceName}}Client) {{$MethodInfo.Name}}(context context.Context,
 
 var defaultClient, _ = New{{.ServiceName}}Client("{{.BaseDomain}}")
 
+func ConfigDefaultClient(ops ...Option) (err error) {
+	defaultClient, err = New{{.ServiceName}}Client("{{.BaseDomain}}", ops...)
+	return
+}
+
 {{range $_, $MethodInfo := .ClientMethods}}
 func {{$MethodInfo.Name}}(context context.Context, req *{{$MethodInfo.RequestTypeName}}, reqOpt ...config.RequestOption) (resp *{{$MethodInfo.ReturnTypeName}}, rawResponse *protocol.Response, err error) {
 	return defaultClient.{{$MethodInfo.Name}}(context, req, reqOpt...)
diff --git a/cmd/hz/generator/router.go b/cmd/hz/generator/router.go
index 3ea65fc..e335a26 100644
--- a/cmd/hz/generator/router.go
+++ b/cmd/hz/generator/router.go
@@ -48,9 +48,10 @@ type RouterNode struct {
 	HttpMethod          string
 }
 
-type RegisterDependency struct {
-	PkgAlias string
-	Pkg      string
+type RegisterInfo struct {
+	PackageName string
+	DepPkgAlias string
+	DepPkg      string
 }
 
 // NewRouterTree contains "/" as root node
@@ -237,9 +238,10 @@ var (
 )
 
 func (pkgGen *HttpPackageGenerator) updateRegister(pkg, rDir, pkgName string) error {
-	register := RegisterDependency{
-		PkgAlias: strings.ReplaceAll(pkgName, "/", "_"),
-		Pkg:      pkg,
+	register := RegisterInfo{
+		PackageName: filepath.Base(rDir),
+		DepPkgAlias: strings.ReplaceAll(pkgName, "/", "_"),
+		DepPkg:      pkg,
 	}
 	registerPath := filepath.Join(rDir, registerTplName)
 	isExist, err := util.PathExist(registerPath)
@@ -255,13 +257,13 @@ func (pkgGen *HttpPackageGenerator) updateRegister(pkg, rDir, pkgName string) er
 		return fmt.Errorf("read register '%s' failed, err: %v", registerPath, err.Error())
 	}
 
-	if !bytes.Contains(file, []byte(register.Pkg)) {
-		file, err = util.AddImport(registerPath, register.PkgAlias, register.Pkg)
+	if !bytes.Contains(file, []byte(register.DepPkg)) {
+		file, err = util.AddImport(registerPath, register.DepPkgAlias, register.DepPkg)
 		if err != nil {
 			return err
 		}
 
-		insertReg := register.PkgAlias + ".Register(r)\n"
+		insertReg := register.DepPkgAlias + ".Register(r)\n"
 		if bytes.Contains(file, []byte(insertReg)) {
 			return fmt.Errorf("the router(%s) has been registered", insertReg)
 		}
@@ -289,7 +291,7 @@ func (pkgGen *HttpPackageGenerator) genRouter(pkg *HttpPackage, root *RouterNode
 	}
 	router := Router{
 		FilePath:    filepath.Join(routerDir, util.BaseNameAndTrim(pkg.IdlName)+".go"),
-		PackageName: util.ToCamelCase(util.BaseName(routerPackage, "")),
+		PackageName: filepath.Base(routerDir),
 		HandlerPackages: map[string]string{
 			util.BaseName(handlerPackage, ""): handlerPackage,
 		},
diff --git a/cmd/hz/meta/const.go b/cmd/hz/meta/const.go
index 2e4d82f..a28b5bf 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.5.2"
+const Version = "v0.6.0"
 
 // Mode hz run modes
 type Mode int
@@ -81,3 +81,8 @@ const (
 const (
 	SetBodyParam = "setBodyParam(req).\n"
 )
+
+// TheUseOptionMessage indicates that the generating of 'model code' is aborted due to the -use option for thrift IDL.
+const TheUseOptionMessage = "'model code' is not generated due to the '-use' option"
+
+const AddThriftReplace = "do not generate 'go.mod', please add 'replace github.com/apache/thrift => github.com/apache/thrift v0.13.0' to your 'go.mod'"
diff --git a/cmd/hz/meta/manifest.go b/cmd/hz/meta/manifest.go
index 773d283..a4a7434 100644
--- a/cmd/hz/meta/manifest.go
+++ b/cmd/hz/meta/manifest.go
@@ -17,19 +17,22 @@
 package meta
 
 import (
+	"bytes"
 	"fmt"
 	"os"
 	"path/filepath"
-	"regexp"
 
-	"github.com/cloudwego/hertz/cmd/hz/util/logs"
 	gv "github.com/hashicorp/go-version"
+	"gopkg.in/yaml.v2"
 )
 
 const ManifestFile = ".hz"
 
 type Manifest struct {
-	Version *gv.Version
+	Version    string `yaml:"hz version"`
+	HandlerDir string `yaml:"handlerDir"`
+	ModelDir   string `yaml:"modelDir"`
+	RouterDir  string `yaml:"routerDir"`
 }
 
 var GoVersion *gv.Version
@@ -39,36 +42,35 @@ func init() {
 	GoVersion, _ = gv.NewVersion(Version)
 }
 
-var regVersion = regexp.MustCompile(`hz version: v([0-9a-z.]*)`)
-
-func (manifest *Manifest) Validate(dir string) error {
-	rd, err := os.ReadFile(filepath.Join(dir, ManifestFile))
+func (manifest *Manifest) InitAndValidate(dir string) error {
+	m, err := loadConfigFile(filepath.Join(dir, ManifestFile))
 	if err != nil {
-		return err
+		return fmt.Errorf("can not load \".hz\", err: %v", err)
 	}
 
-	ms := regVersion.FindSubmatchIndex(rd)
-	if len(ms) != 4 || ms[2] < 19 {
-		logs.Errorf("Invalid manifest file: %s, pattern '%s' not match\n", filepath.Join(dir, ManifestFile), regVersion.String())
-		return fmt.Errorf("current project doesn't belong to hertz framework")
+	if len(m.Version) == 0 {
+		return fmt.Errorf("can not get hz version form \".hz\", current project doesn't belong to hertz framework")
 	}
-	v, err := gv.NewVersion(string(rd[ms[2]:ms[3]]))
+
+	*manifest = *m
+	_, err = gv.NewVersion(manifest.Version)
 	if err != nil {
-		return err
-	}
-	*manifest = Manifest{
-		Version: v,
+		return fmt.Errorf("invalid hz version in \".hz\", err: %v", err)
 	}
+
 	return nil
 }
 
-func (manifest Manifest) String() string {
-	return `// Code generated by hz. DO NOT EDIT.
+const hzTitle = "// Code generated by hz. DO NOT EDIT.\n"
 
-hz version: v` + manifest.Version.String() + "\n"
+func (manifest *Manifest) String() string {
+	conf, _ := yaml.Marshal(*manifest)
+
+	return hzTitle + "\n" +
+		string(conf)
 }
 
-func (manifest Manifest) Persist(dir string) error {
+func (manifest *Manifest) Persist(dir string) error {
 	file := filepath.Join(dir, ManifestFile)
 	fd, err := os.OpenFile(file, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, os.FileMode(0o644))
 	if err != nil {
@@ -78,3 +80,17 @@ func (manifest Manifest) Persist(dir string) error {
 	_, err = fd.WriteString(manifest.String())
 	return err
 }
+
+// loadConfigFile load config file from path
+func loadConfigFile(path string) (*Manifest, error) {
+	file, err := os.ReadFile(path)
+	if err != nil {
+		return nil, err
+	}
+	var manifest Manifest
+	file = bytes.TrimPrefix(file, []byte(hzTitle))
+	if err = yaml.Unmarshal(file, &manifest); err != nil {
+		return nil, fmt.Errorf("decode \".hz\" failed, err: %v", err)
+	}
+	return &manifest, nil
+}
diff --git a/cmd/hz/protobuf/ast.go b/cmd/hz/protobuf/ast.go
index 89a51ed..0e2803e 100644
--- a/cmd/hz/protobuf/ast.go
+++ b/cmd/hz/protobuf/ast.go
@@ -30,6 +30,7 @@ import (
 	"github.com/jhump/protoreflect/desc"
 	"google.golang.org/protobuf/compiler/protogen"
 	"google.golang.org/protobuf/proto"
+	"google.golang.org/protobuf/reflect/protoreflect"
 	"google.golang.org/protobuf/types/descriptorpb"
 )
 
@@ -141,15 +142,28 @@ func astToService(ast *descriptorpb.FileDescriptorProto, resolver *Resolver, cmd
 				handlerOutDir = ""
 			}
 
+			// protoGoInfo can get generated "Go Info" for proto file.
+			// the type name may be different between "***.proto" and "***.pb.go"
+			protoGoInfo, exist := gen.FilesByPath[ast.GetName()]
+			if !exist {
+				return nil, fmt.Errorf("file(%s) can not exist", ast.GetName())
+			}
+			methodGoInfo, err := getMethod(protoGoInfo, m)
+			if err != nil {
+				return nil, err
+			}
+			inputGoType := methodGoInfo.Input
+			outputGoType := methodGoInfo.Output
+
 			reqName := m.GetInputType()
 			sb, err := resolver.ResolveIdentifier(reqName)
-			reqName = util.BaseName(sb.Scope.GetOptions().GetGoPackage(), "") + "." + sb.Name
+			reqName = util.BaseName(sb.Scope.GetOptions().GetGoPackage(), "") + "." + inputGoType.GoIdent.GoName
 			if err != nil {
 				return nil, err
 			}
 			respName := m.GetOutputType()
 			st, err := resolver.ResolveIdentifier(respName)
-			respName = util.BaseName(st.Scope.GetOptions().GetGoPackage(), "") + "." + st.Name
+			respName = util.BaseName(st.Scope.GetOptions().GetGoPackage(), "") + "." + outputGoType.GoIdent.GoName
 			if err != nil {
 				return nil, err
 			}
@@ -191,10 +205,10 @@ func astToService(ast *descriptorpb.FileDescriptorProto, resolver *Resolver, cmd
 			merges = service.Models
 			merges.MergeMap(method.Models)
 			if goOptMapAlias[sb.Scope.GetOptions().GetGoPackage()] != "" {
-				reqName = goOptMapAlias[sb.Scope.GetOptions().GetGoPackage()] + "." + sb.Name
+				reqName = goOptMapAlias[sb.Scope.GetOptions().GetGoPackage()] + "." + inputGoType.GoIdent.GoName
 			}
 			if goOptMapAlias[sb.Scope.GetOptions().GetGoPackage()] != "" {
-				respName = goOptMapAlias[st.Scope.GetOptions().GetGoPackage()] + "." + st.Name
+				respName = goOptMapAlias[st.Scope.GetOptions().GetGoPackage()] + "." + outputGoType.GoIdent.GoName
 			}
 			method.RequestTypeName = reqName
 			method.ReturnTypeName = respName
@@ -236,41 +250,67 @@ func parseAnnotationToClient(clientMethod *generator.ClientMethod, gen *protogen
 		hasFormAnnotation bool
 	)
 	for _, f := range inputType.Fields {
+		hasAnnotation := false
+		isStringFieldType := false
+		if f.Desc.Kind() == protoreflect.StringKind {
+			isStringFieldType = true
+		}
 		if proto.HasExtension(f.Desc.Options(), api.E_Query) {
+			hasAnnotation = true
 			queryAnnos := proto.GetExtension(f.Desc.Options(), api.E_Query)
 			val := queryAnnos.(string)
 			clientMethod.QueryParamsCode += fmt.Sprintf("%q: req.Get%s(),\n", val, f.GoName)
 		}
 
 		if proto.HasExtension(f.Desc.Options(), api.E_Path) {
+			hasAnnotation = true
 			pathAnnos := proto.GetExtension(f.Desc.Options(), api.E_Path)
 			val := pathAnnos.(string)
-			clientMethod.PathParamsCode += fmt.Sprintf("%q: req.Get%s(),\n", val, f.GoName)
+			if isStringFieldType {
+				clientMethod.PathParamsCode += fmt.Sprintf("%q: req.Get%s(),\n", val, f.GoName)
+			} else {
+				clientMethod.PathParamsCode += fmt.Sprintf("%q: fmt.Sprint(req.Get%s()),\n", val, f.GoName)
+			}
 		}
 
 		if proto.HasExtension(f.Desc.Options(), api.E_Header) {
+			hasAnnotation = true
 			headerAnnos := proto.GetExtension(f.Desc.Options(), api.E_Header)
 			val := headerAnnos.(string)
-			clientMethod.HeaderParamsCode += fmt.Sprintf("%q: req.Get%s(),\n", val, f.GoName)
+			if isStringFieldType {
+				clientMethod.HeaderParamsCode += fmt.Sprintf("%q: req.Get%s(),\n", val, f.GoName)
+			} else {
+				clientMethod.HeaderParamsCode += fmt.Sprintf("%q: fmt.Sprint(req.Get%s()),\n", val, f.GoName)
+			}
 		}
 
 		if proto.HasExtension(f.Desc.Options(), api.E_Form) {
+			hasAnnotation = true
 			formAnnos := proto.GetExtension(f.Desc.Options(), api.E_Form)
 			hasFormAnnotation = true
 			val := formAnnos.(string)
-			clientMethod.FormValueCode += fmt.Sprintf("%q: req.Get%s(),\n", val, f.GoName)
+			if isStringFieldType {
+				clientMethod.FormValueCode += fmt.Sprintf("%q: req.Get%s(),\n", val, f.GoName)
+			} else {
+				clientMethod.FormValueCode += fmt.Sprintf("%q: fmt.Sprint(req.Get%s()),\n", val, f.GoName)
+			}
 		}
 
 		if proto.HasExtension(f.Desc.Options(), api.E_Body) {
+			hasAnnotation = true
 			hasBodyAnnotation = true
 		}
 
 		if proto.HasExtension(f.Desc.Options(), api.E_FileName) {
+			hasAnnotation = true
 			fileAnnos := proto.GetExtension(f.Desc.Options(), api.E_FileName)
 			hasFormAnnotation = true
 			val := fileAnnos.(string)
 			clientMethod.FormFileCode += fmt.Sprintf("%q: req.Get%s(),\n", val, f.GoName)
 		}
+		if !hasAnnotation && strings.EqualFold(clientMethod.HTTPMethod, "get") {
+			clientMethod.QueryParamsCode += fmt.Sprintf("%q: req.Get%s(),\n", f.GoName, f.GoName)
+		}
 	}
 	clientMethod.BodyParamsCode = meta.SetBodyParam
 	if hasBodyAnnotation && hasFormAnnotation {
diff --git a/cmd/hz/protobuf/plugin.go b/cmd/hz/protobuf/plugin.go
index fdddc93..6d9afc7 100644
--- a/cmd/hz/protobuf/plugin.go
+++ b/cmd/hz/protobuf/plugin.go
@@ -74,6 +74,7 @@ type Plugin struct {
 	Recursive    bool
 	OutDir       string
 	ModelDir     string
+	UseDir       string
 	IdlClientDir string
 	PkgMap       map[string]string
 	logger       *logs.StdLogger
@@ -83,6 +84,9 @@ func (plugin *Plugin) Run() int {
 	plugin.setLogger()
 	args := &config.Argument{}
 	defer func() {
+		if args == nil {
+			return
+		}
 		if args.Verbose {
 			verboseLog := plugin.recvVerboseLogger()
 			if len(verboseLog) != 0 {
@@ -165,6 +169,7 @@ func (plugin *Plugin) parseArgs(param string) (*config.Argument, error) {
 	}
 	plugin.OutDir = args.OutDir
 	plugin.PkgMap = args.OptPkgMap
+	plugin.UseDir = args.Use
 	return args, nil
 }
 
@@ -349,7 +354,10 @@ func (plugin *Plugin) GenerateFile(gen *protogen.Plugin, f *protogen.File) error
 	}
 	f.GeneratedFilenamePrefix = filepath.Join(util.ImportToPath(impt, ""), util.BaseName(f.Proto.GetName(), ".proto"))
 	f.Generate = true
-
+	// if use third-party model, no model code is generated within the project
+	if len(plugin.UseDir) != 0 {
+		return nil
+	}
 	file, err := generateFile(gen, f)
 	if err != nil || file == nil {
 		return fmt.Errorf("generate file %s failed: %s", f.Proto.GetName(), err.Error())
@@ -560,7 +568,7 @@ func (plugin *Plugin) genHttpPackage(ast *descriptorpb.FileDescriptorProto, deps
 		return nil, err
 	}
 
-	cf, _ := util.GetColonPair(args.CustomizePackage)
+	customPackageTemplate := args.CustomizePackage
 	pkg, err := args.GetGoPackage()
 	if err != nil {
 		return nil, err
@@ -582,19 +590,22 @@ func (plugin *Plugin) genHttpPackage(ast *descriptorpb.FileDescriptorProto, deps
 		return nil, err
 	}
 	sg := generator.HttpPackageGenerator{
-		ConfigPath: cf,
+		ConfigPath: customPackageTemplate,
 		HandlerDir: handlerDir,
 		RouterDir:  routerDir,
 		ModelDir:   modelDir,
+		UseDir:     args.Use,
 		ClientDir:  clientDir,
 		TemplateGenerator: generator.TemplateGenerator{
 			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,
 	}
 
diff --git a/cmd/hz/thrift/ast.go b/cmd/hz/thrift/ast.go
index 1a8f607..d1580e9 100644
--- a/cmd/hz/thrift/ast.go
+++ b/cmd/hz/thrift/ast.go
@@ -181,59 +181,61 @@ func parseAnnotationToClient(clientMethod *generator.ClientMethod, p *parser.Typ
 		hasFormAnnotation bool
 	)
 	for _, field := range st.Fields() {
-		rwctx, err := thriftgoUtil.MkRWCtx(thriftgoUtil.RootScope(), field)
-		if err != nil {
-			fmt.Errorf("can not get field info for %s", field.Name)
+		hasAnnotation := false
+		isStringFieldType := false
+		if field.GetType().String() == "string" {
+			isStringFieldType = true
 		}
 		if anno := getAnnotation(field.Annotations, AnnotationQuery); len(anno) > 0 {
+			hasAnnotation = true
 			query := anno[0]
-			if rwctx.IsPointer {
-				clientMethod.QueryParamsCode += fmt.Sprintf("%q: *req.%v,\n", query, field.GoName().String())
-			} else {
-				clientMethod.QueryParamsCode += fmt.Sprintf("%q: req.%v,\n", query, field.GoName().String())
-			}
+			clientMethod.QueryParamsCode += fmt.Sprintf("%q: req.Get%s(),\n", query, field.GoName().String())
 		}
 
 		if anno := getAnnotation(field.Annotations, AnnotationPath); len(anno) > 0 {
+			hasAnnotation = true
 			path := anno[0]
-			if rwctx.IsPointer {
-				clientMethod.PathParamsCode += fmt.Sprintf("%q: *req.%v,\n", path, field.GoName().String())
+			if isStringFieldType {
+				clientMethod.PathParamsCode += fmt.Sprintf("%q: req.Get%s(),\n", path, field.GoName().String())
 			} else {
-				clientMethod.PathParamsCode += fmt.Sprintf("%q: req.%v,\n", path, field.GoName().String())
+				clientMethod.PathParamsCode += fmt.Sprintf("%q: fmt.Sprint(req.Get%s()),\n", path, field.GoName().String())
 			}
 		}
 
 		if anno := getAnnotation(field.Annotations, AnnotationHeader); len(anno) > 0 {
+			hasAnnotation = true
 			header := anno[0]
-			if rwctx.IsPointer {
-				clientMethod.HeaderParamsCode += fmt.Sprintf("%q: *req.%v,\n", header, field.GoName().String())
+			if isStringFieldType {
+				clientMethod.HeaderParamsCode += fmt.Sprintf("%q: req.Get%s(),\n", header, field.GoName().String())
 			} else {
-				clientMethod.HeaderParamsCode += fmt.Sprintf("%q: req.%v,\n", header, field.GoName().String())
+				clientMethod.HeaderParamsCode += fmt.Sprintf("%q: fmt.Sprint(req.Get%s()),\n", header, field.GoName().String())
 			}
 		}
 
 		if anno := getAnnotation(field.Annotations, AnnotationForm); len(anno) > 0 {
+			hasAnnotation = true
 			form := anno[0]
 			hasFormAnnotation = true
-			if rwctx.IsPointer {
-				clientMethod.FormValueCode += fmt.Sprintf("%q: *req.%v,\n", form, field.GoName().String())
+			if isStringFieldType {
+				clientMethod.FormValueCode += fmt.Sprintf("%q: req.Get%s(),\n", form, field.GoName().String())
 			} else {
-				clientMethod.FormValueCode += fmt.Sprintf("%q: req.%v,\n", form, field.GoName().String())
+				clientMethod.FormValueCode += fmt.Sprintf("%q: fmt.Sprint(req.Get%s()),\n", form, field.GoName().String())
 			}
 		}
 
 		if anno := getAnnotation(field.Annotations, AnnotationBody); len(anno) > 0 {
+			hasAnnotation = true
 			hasBodyAnnotation = true
 		}
 
 		if anno := getAnnotation(field.Annotations, AnnotationFileName); len(anno) > 0 {
+			hasAnnotation = true
 			fileName := anno[0]
 			hasFormAnnotation = true
-			if rwctx.IsPointer {
-				clientMethod.FormFileCode += fmt.Sprintf("%q: *req.%v,\n", fileName, field.GoName().String())
-			} else {
-				clientMethod.FormFileCode += fmt.Sprintf("%q: req.%v,\n", fileName, field.GoName().String())
-			}
+			clientMethod.FormFileCode += fmt.Sprintf("%q: req.Get%s(),\n", fileName, field.GoName().String())
+		}
+		if !hasAnnotation && strings.EqualFold(clientMethod.HTTPMethod, "get") {
+			clientMethod.QueryParamsCode += fmt.Sprintf("%q: req.Get%s(),\n", field.GoName().String(), field.GoName().String())
 		}
 	}
 	clientMethod.BodyParamsCode = meta.SetBodyParam
diff --git a/cmd/hz/thrift/plugin.go b/cmd/hz/thrift/plugin.go
index c0f52cf..9392553 100644
--- a/cmd/hz/thrift/plugin.go
+++ b/cmd/hz/thrift/plugin.go
@@ -17,6 +17,7 @@
 package thrift
 
 import (
+	"errors"
 	"fmt"
 	"io/ioutil"
 	"os"
@@ -45,6 +46,9 @@ func (plugin *Plugin) Run() int {
 	plugin.setLogger()
 	args := &config.Argument{}
 	defer func() {
+		if args == nil {
+			return
+		}
 		if args.Verbose {
 			verboseLog := plugin.recvVerboseLogger()
 			if len(verboseLog) != 0 {
@@ -97,7 +101,7 @@ func (plugin *Plugin) Run() int {
 		return meta.PluginError
 	}
 
-	cf, _ := util.GetColonPair(args.CustomizePackage)
+	customPackageTemplate := args.CustomizePackage
 	pkg, err := args.GetGoPackage()
 	if err != nil {
 		logs.Errorf("get go package failed: %s", err.Error())
@@ -124,19 +128,22 @@ func (plugin *Plugin) Run() int {
 		return meta.PluginError
 	}
 	sg := generator.HttpPackageGenerator{
-		ConfigPath: cf,
+		ConfigPath: customPackageTemplate,
 		HandlerDir: handlerDir,
 		RouterDir:  routerDir,
 		ModelDir:   modelDir,
+		UseDir:     args.Use,
 		ClientDir:  clientDir,
 		TemplateGenerator: generator.TemplateGenerator{
 			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,
 	}
 	if args.ModelBackend != "" {
@@ -149,6 +156,20 @@ func (plugin *Plugin) Run() int {
 		logs.Errorf("generate package failed: %s", err.Error())
 		return meta.PluginError
 	}
+	if len(args.Use) != 0 {
+		err = sg.Persist()
+		if err != nil {
+			logs.Errorf("persist file failed within '-use' option: %s", err.Error())
+			return meta.PluginError
+		}
+		res := thriftgo_plugin.BuildErrorResponse(errors.New(meta.TheUseOptionMessage).Error())
+		err = plugin.response(res)
+		if err != nil {
+			logs.Errorf("response failed: %s", err.Error())
+			return meta.PluginError
+		}
+		return 0
+	}
 	files, err := sg.GetFormatAndExcludedFiles()
 	if err != nil {
 		logs.Errorf("format file failed: %s", err.Error())
@@ -159,7 +180,7 @@ func (plugin *Plugin) Run() int {
 		logs.Errorf("get response failed: %s", err.Error())
 		return meta.PluginError
 	}
-	plugin.response(res)
+	err = plugin.response(res)
 	if err != nil {
 		logs.Errorf("response failed: %s", err.Error())
 		return meta.PluginError
diff --git a/cmd/hz/util/data.go b/cmd/hz/util/data.go
index 97c0603..ad19158 100644
--- a/cmd/hz/util/data.go
+++ b/cmd/hz/util/data.go
@@ -213,14 +213,6 @@ func GetFirstKV(m map[string][]string) (string, []string) {
 	return "", nil
 }
 
-func GetColonPair(str string) (string, string) {
-	ps := strings.Split(str, ":")
-	if len(ps) != 2 {
-		return "", ""
-	}
-	return ps[0], ps[1]
-}
-
 func ToCamelCase(name string) string {
 	return CamelString(name)
 }
diff --git a/cmd/hz/util/env.go b/cmd/hz/util/env.go
index 8385877..51168e2 100644
--- a/cmd/hz/util/env.go
+++ b/cmd/hz/util/env.go
@@ -20,6 +20,7 @@ import (
 	"bytes"
 	"fmt"
 	"go/build"
+	"io/ioutil"
 	"os"
 	"os/exec"
 	"path/filepath"
@@ -84,7 +85,7 @@ var goModReg = regexp.MustCompile(`^\s*module\s+(\S+)\s*`)
 func SearchGoMod(cwd string, recurse bool) (moduleName, path string, found bool) {
 	for {
 		path = filepath.Join(cwd, "go.mod")
-		data, err := os.ReadFile(path)
+		data, err := ioutil.ReadFile(path)
 		if err == nil {
 			for _, line := range strings.Split(string(data), "\n") {
 				m := goModReg.FindStringSubmatch(line)
diff --git a/go.mod b/go.mod
index 450f1f6..2c7a975 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +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/sonic v1.5.0
+	github.com/bytedance/sonic v1.8.1
 	github.com/cloudwego/netpoll v0.3.1
 	github.com/fsnotify/fsnotify v1.5.4
 	github.com/tidwall/gjson v1.13.0 // indirect
diff --git a/go.sum b/go.sum
index f9885f8..fe7ecbf 100644
--- a/go.sum
+++ b/go.sum
@@ -2,10 +2,12 @@ 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/sonic v1.5.0 h1:XWdTi8bwPgxIML+eNV1IwNuTROK6EUrQ65ey8yd6fRQ=
 github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
-github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06 h1:1sDoSuDPWzhkdzNVxCxtIaKiAe96ESVPv8coGwc1gZ4=
+github.com/bytedance/sonic v1.8.1 h1:NqAHCaGaTzro0xMmnTCLUyRlbEP6r8MCA1cJUrH3Pu4=
+github.com/bytedance/sonic v1.8.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
 github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
+github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
+github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
 github.com/cloudwego/netpoll v0.3.1 h1:xByoORmCLIyKZ8gS+da06WDo3j+jvmhaqS2KeKejtBk=
 github.com/cloudwego/netpoll v0.3.1/go.mod h1:1T2WVuQ+MQw6h6DpE45MohSvDTKdy2DlzCx2KsnPI4E=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -30,10 +32,15 @@ github.com/nyaruka/phonenumbers v1.0.55/go.mod h1:sDaTZ/KPX5f8qyV9qN+hIm+4ZBARJr
 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/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=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
-github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 github.com/tidwall/gjson v1.9.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
 github.com/tidwall/gjson v1.13.0 h1:3TFY9yxOQShrvmjdM76K+jc66zJeT6D3/VFFYCGQf7M=
 github.com/tidwall/gjson v1.13.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
@@ -57,6 +64,7 @@ google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+Rur
 google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
diff --git a/pkg/app/context.go b/pkg/app/context.go
index 157674c..3ed3f19 100644
--- a/pkg/app/context.go
+++ b/pkg/app/context.go
@@ -78,22 +78,72 @@ type Handler interface {
 
 type ClientIP func(ctx *RequestContext) string
 
-var defaultClientIP = func(ctx *RequestContext) string {
-	RemoteIPHeaders := []string{"X-Real-IP", "X-Forwarded-For"}
-	for _, headerName := range RemoteIPHeaders {
-		ip := ctx.Request.Header.Get(headerName)
-		if ip != "" {
-			return ip
+type ClientIPOptions struct {
+	RemoteIPHeaders []string
+	TrustedProxies  map[string]bool
+}
+
+var defaultClientIPOptions = ClientIPOptions{
+	RemoteIPHeaders: []string{"X-Real-IP", "X-Forwarded-For"},
+	TrustedProxies: map[string]bool{
+		"0.0.0.0": true,
+	},
+}
+
+// ClientIPWithOption used to generate custom ClientIP function and set by engine.SetClientIPFunc
+func ClientIPWithOption(opts ClientIPOptions) ClientIP {
+	return func(ctx *RequestContext) string {
+		RemoteIPHeaders := opts.RemoteIPHeaders
+		TrustedProxies := opts.TrustedProxies
+
+		remoteIP, _, err := net.SplitHostPort(strings.TrimSpace(ctx.RemoteAddr().String()))
+		if err != nil {
+			return ""
 		}
+		trusted := isTrustedProxy(TrustedProxies, remoteIP)
+
+		if trusted {
+			for _, headerName := range RemoteIPHeaders {
+				ip, valid := validateHeader(TrustedProxies, ctx.Request.Header.Get(headerName))
+				if valid {
+					return ip
+				}
+			}
+		}
+
+		return remoteIP
 	}
+}
+
+// isTrustedProxy will check whether the IP address is included in the trusted list according to TrustedProxies
+func isTrustedProxy(trustedProxies map[string]bool, remoteIP string) bool {
+	return trustedProxies[remoteIP]
+}
 
-	if ip, _, err := net.SplitHostPort(strings.TrimSpace(ctx.RemoteAddr().String())); err == nil {
-		return ip
+// validateHeader will parse X-Real-IP and X-Forwarded-For header and return the Initial client IP address or an untrusted IP address
+func validateHeader(trustedProxies map[string]bool, header string) (clientIP string, valid bool) {
+	if header == "" {
+		return "", false
 	}
+	items := strings.Split(header, ",")
+	for i := len(items) - 1; i >= 0; i-- {
+		ipStr := strings.TrimSpace(items[i])
+		ip := net.ParseIP(ipStr)
+		if ip == nil {
+			break
+		}
 
-	return ""
+		// X-Forwarded-For is appended by proxy
+		// Check IPs in reverse order and stop when find untrusted proxy
+		if (i == 0) || (!isTrustedProxy(trustedProxies, ipStr)) {
+			return ipStr, true
+		}
+	}
+	return "", false
 }
 
+var defaultClientIP = ClientIPWithOption(defaultClientIPOptions)
+
 // SetClientIPFunc sets ClientIP function implementation to get ClientIP.
 // Deprecated: Use engine.SetClientIPFunc instead of SetClientIPFunc
 func SetClientIPFunc(fn ClientIP) {
@@ -161,6 +211,15 @@ type RequestContext struct {
 	formValueFunc FormValueFunc
 }
 
+// Flush is the shortcut for ctx.Response.GetHijackWriter().Flush().
+// Will return nil if the response writer is not hijacked.
+func (ctx *RequestContext) Flush() error {
+	if ctx.Response.GetHijackWriter() == nil {
+		return nil
+	}
+	return ctx.Response.GetHijackWriter().Flush()
+}
+
 func (ctx *RequestContext) SetClientIPFunc(f ClientIP) {
 	ctx.clientIPFunc = f
 }
diff --git a/pkg/app/fs.go b/pkg/app/fs.go
index 9cd6ba4..5bad7cc 100644
--- a/pkg/app/fs.go
+++ b/pkg/app/fs.go
@@ -48,6 +48,7 @@ import (
 	"fmt"
 	"html"
 	"io"
+	"io/ioutil"
 	"mime"
 	"net/http"
 	"os"
@@ -275,8 +276,6 @@ func (r *fsSmallFileReader) WriteTo(w io.Writer) (int64, error) {
 // Directory contents is returned if path points to directory.
 //
 // Use ServeFileUncompressed is you don't need serving compressed file contents.
-//
-// See also RequestCtx.SendFile.
 func ServeFile(ctx *RequestContext, path string) {
 	rootFSOnce.Do(func() {
 		rootFSHandler = rootFS.NewRequestHandler()
@@ -1076,7 +1075,7 @@ func readFileHeader(f *os.File, compressed bool) ([]byte, error) {
 		R: r,
 		N: 512,
 	}
-	data, err := io.ReadAll(lr)
+	data, err := ioutil.ReadAll(lr)
 	if _, err := f.Seek(0, 0); err != nil {
 		return nil, err
 	}
@@ -1203,8 +1202,6 @@ func stripLeadingSlashes(path []byte, stripSlashes int) []byte {
 //
 // ServeFile may be used for saving network traffic when serving files
 // with good compression ratio.
-//
-// See also RequestCtx.SendFile.
 func ServeFileUncompressed(ctx *RequestContext, path string) {
 	ctx.Request.Header.DelBytes(bytestr.StrAcceptEncoding)
 	ServeFile(ctx, path)
diff --git a/pkg/app/middlewares/server/recovery/recovery.go b/pkg/app/middlewares/server/recovery/recovery.go
index 02eccba..f05aad2 100644
--- a/pkg/app/middlewares/server/recovery/recovery.go
+++ b/pkg/app/middlewares/server/recovery/recovery.go
@@ -20,7 +20,7 @@ import (
 	"bytes"
 	"context"
 	"fmt"
-	"os"
+	"io/ioutil"
 	"runtime"
 
 	"github.com/cloudwego/hertz/pkg/app"
@@ -66,7 +66,7 @@ func stack(skip int) []byte {
 		// Print this much at least.  If we can't find the source, it won't show.
 		fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc)
 		if file != lastFile {
-			data, err := os.ReadFile(file)
+			data, err := ioutil.ReadFile(file)
 			if err != nil {
 				continue
 			}
diff --git a/pkg/common/ut/request.go b/pkg/common/ut/request.go
index 70ca7c3..f6dcfc4 100644
--- a/pkg/common/ut/request.go
+++ b/pkg/common/ut/request.go
@@ -20,6 +20,7 @@ package ut
 import (
 	"context"
 	"io"
+	"io/ioutil"
 
 	"github.com/cloudwego/hertz/pkg/protocol"
 	"github.com/cloudwego/hertz/pkg/route"
@@ -57,7 +58,7 @@ func PerformRequest(engine *route.Engine, method, url string, body *Body, header
 		if engine.IsStreamRequestBody() || body.Len == -1 {
 			ctx.Request.SetBodyStream(body.Body, body.Len)
 		} else {
-			buf, err := io.ReadAll(&io.LimitedReader{R: body.Body, N: int64(body.Len)})
+			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)
diff --git a/pkg/network/writer.go b/pkg/network/writer.go
index 0b3a405..ed6c6ac 100644
--- a/pkg/network/writer.go
+++ b/pkg/network/writer.go
@@ -103,3 +103,12 @@ func NewWriter(w io.Writer) Writer {
 		w: w,
 	}
 }
+
+type ExtWriter interface {
+	io.Writer
+	Flush() error
+
+	// Finalize will be called by framework before the writer is released.
+	// Implementations must guarantee that Finalize is safe for multiple calls.
+	Finalize() error
+}
diff --git a/pkg/protocol/args.go b/pkg/protocol/args.go
index ebf9ae5..f29aefe 100644
--- a/pkg/protocol/args.go
+++ b/pkg/protocol/args.go
@@ -177,6 +177,19 @@ func releaseArg(h []argsKV) []argsKV {
 	return h[:len(h)-1]
 }
 
+func updateArgBytes(h []argsKV, key, value []byte) []argsKV {
+	n := len(h)
+	for i := 0; i < n; i++ {
+		kv := &h[i]
+		if kv.noValue && bytes.Equal(key, kv.key) {
+			kv.value = append(kv.value[:0], value...)
+			kv.noValue = ArgsHasValue
+			return h
+		}
+	}
+	return h
+}
+
 func setArgBytes(h []argsKV, key, value []byte, noValue bool) []argsKV {
 	n := len(h)
 	for i := 0; i < n; i++ {
diff --git a/pkg/protocol/consts/headers.go b/pkg/protocol/consts/headers.go
index 02ae7f3..d01af5a 100644
--- a/pkg/protocol/consts/headers.go
+++ b/pkg/protocol/consts/headers.go
@@ -17,37 +17,69 @@
 package consts
 
 const (
-	HeaderAuthorization = "Authorization"
-	HeaderHost          = "Host"
-	HeaderReferer       = "Referer"
-	HeaderContentType   = "Content-Type"
-	HeaderUserAgent     = "User-Agent"
-	HeaderExpect        = "Expect"
-	HeaderConnection    = "Connection"
-	HeaderContentLength = "Content-Length"
-	HeaderCookie        = "Cookie"
-	HeaderAltSvc        = "Alt-Svc"
-
-	HeaderServer           = "Server"
-	HeaderServerLower      = "server"
-	HeaderSetCookie        = "Set-Cookie"
-	HeaderSetCookieLower   = "set-cookie"
+	HeaderDate = "Date"
+
+	HeaderIfModifiedSince = "If-Modified-Since"
+	HeaderLastModified    = "Last-Modified"
+
+	// Redirects
+	HeaderLocation = "Location"
+
+	// Transfer coding
+	HeaderTE               = "TE"
+	HeaderTrailer          = "Trailer"
+	HeaderTrailerLower     = "trailer"
 	HeaderTransferEncoding = "Transfer-Encoding"
-	HeaderDate             = "Date"
 
-	HeaderRange        = "Range"
+	// Controls
+	HeaderCookie         = "Cookie"
+	HeaderExpect         = "Expect"
+	HeaderMaxForwards    = "Max-Forwards"
+	HeaderSetCookie      = "Set-Cookie"
+	HeaderSetCookieLower = "set-cookie"
+
+	// Connection management
+	HeaderConnection      = "Connection"
+	HeaderKeepAlive       = "Keep-Alive"
+	HeaderProxyConnection = "Proxy-Connection"
+
+	// Authentication
+	HeaderAuthorization      = "Authorization"
+	HeaderProxyAuthenticate  = "Proxy-Authenticate"
+	HeaderProxyAuthorization = "Proxy-Authorization"
+	HeaderWWWAuthenticate    = "WWW-Authenticate"
+
+	// Range requests
 	HeaderAcceptRanges = "Accept-Ranges"
 	HeaderContentRange = "Content-Range"
+	HeaderIfRange      = "If-Range"
+	HeaderRange        = "Range"
 
-	HeaderIfModifiedSince = "If-Modified-Since"
-	HeaderLastModified    = "Last-Modified"
+	// Response context
+	HeaderAllow       = "Allow"
+	HeaderServer      = "Server"
+	HeaderServerLower = "server"
+
+	// Request context
+	HeaderFrom           = "From"
+	HeaderHost           = "Host"
+	HeaderReferer        = "Referer"
+	HeaderReferrerPolicy = "Referrer-Policy"
+	HeaderUserAgent      = "User-Agent"
 
 	// Message body information
 	HeaderContentEncoding = "Content-Encoding"
-	HeaderAcceptEncoding  = "Accept-Encoding"
+	HeaderContentLanguage = "Content-Language"
+	HeaderContentLength   = "Content-Length"
+	HeaderContentLocation = "Content-Location"
+	HeaderContentType     = "Content-Type"
 
-	// Redirects
-	HeaderLocation = "Location"
+	// Content negotiation
+	HeaderAccept         = "Accept"
+	HeaderAcceptCharset  = "Accept-Charset"
+	HeaderAcceptEncoding = "Accept-Encoding"
+	HeaderAcceptLanguage = "Accept-Language"
+	HeaderAltSvc         = "Alt-Svc"
 
 	// Protocol
 	HTTP11 = "HTTP/1.1"
diff --git a/pkg/protocol/header.go b/pkg/protocol/header.go
index dfc5c8d..0e1d48a 100644
--- a/pkg/protocol/header.go
+++ b/pkg/protocol/header.go
@@ -85,8 +85,9 @@ type RequestHeader struct {
 	userAgent   []byte
 	mulHeader   [][]byte
 
-	h     []argsKV
-	bufKV argsKV
+	h       []argsKV
+	bufKV   argsKV
+	trailer *Trailer
 
 	cookies []argsKV
 
@@ -124,8 +125,9 @@ type ResponseHeader struct {
 	server      []byte
 	mulHeader   [][]byte
 
-	h     []argsKV
-	bufKV argsKV
+	h       []argsKV
+	bufKV   argsKV
+	trailer *Trailer
 
 	cookies []argsKV
 
@@ -209,6 +211,7 @@ func (h *ResponseHeader) GetHeaders() []argsKV {
 // Reset clears response header.
 func (h *ResponseHeader) Reset() {
 	h.disableNormalizing = false
+	h.Trailer().disableNormalizing = false
 	h.noDefaultContentType = false
 	h.noDefaultDate = false
 	h.ResetSkipNormalize()
@@ -232,6 +235,7 @@ func (h *ResponseHeader) CopyTo(dst *ResponseHeader) {
 	dst.server = append(dst.server[:0], h.server...)
 	dst.h = copyArgs(dst.h, h.h)
 	dst.cookies = copyArgs(dst.cookies, h.cookies)
+	h.Trailer().CopyTo(dst.Trailer())
 }
 
 // Multiple headers with the same key may be added with this function.
@@ -274,6 +278,9 @@ func (h *ResponseHeader) VisitAll(f func(key, value []byte)) {
 			f(bytestr.StrSetCookie, v)
 		})
 	}
+	if !h.Trailer().Empty() {
+		f(bytestr.StrTrailer, h.Trailer().GetBytes())
+	}
 	visitArgs(h.h, f)
 	if h.ConnectionClose() {
 		f(bytestr.StrConnection, bytestr.StrClose)
@@ -392,6 +399,10 @@ func (h *RequestHeader) AppendBytes(dst []byte) []byte {
 		dst = appendHeaderLine(dst, kv.key, kv.value)
 	}
 
+	if !h.Trailer().Empty() {
+		dst = appendHeaderLine(dst, bytestr.StrTrailer, h.Trailer().GetBytes())
+	}
+
 	// there is no need in h.collectCookies() here, since if cookies aren't collected yet,
 	// they all are located in h.h.
 	n := len(h.cookies)
@@ -488,6 +499,7 @@ func (h *ResponseHeader) ResetSkipNormalize() {
 
 	h.h = h.h[:0]
 	h.cookies = h.cookies[:0]
+	h.Trailer().ResetSkipNormalize()
 	h.mulHeader = h.mulHeader[:0]
 }
 
@@ -685,6 +697,8 @@ func (h *ResponseHeader) peek(key []byte) []byte {
 		return h.contentLengthBytes
 	case consts.HeaderSetCookie:
 		return appendResponseCookieBytes(nil, h.cookies)
+	case consts.HeaderTrailer:
+		return h.Trailer().GetBytes()
 	default:
 		return peekArgBytes(h.h, key)
 	}
@@ -875,6 +889,10 @@ func (h *ResponseHeader) AppendBytes(dst []byte) []byte {
 		}
 	}
 
+	if !h.Trailer().Empty() {
+		dst = appendHeaderLine(dst, bytestr.StrTrailer, h.Trailer().GetBytes())
+	}
+
 	n := len(h.cookies)
 	if n > 0 {
 		for i := 0; i < n; i++ {
@@ -961,6 +979,8 @@ func (h *ResponseHeader) del(key []byte) {
 		h.contentLengthBytes = h.contentLengthBytes[:0]
 	case consts.HeaderConnection:
 		h.connectionClose = false
+	case consts.HeaderTrailer:
+		h.Trailer().ResetSkipNormalize()
 	}
 	h.h = delAllArgsBytes(h.h, key)
 }
@@ -992,6 +1012,7 @@ func (h *RequestHeader) Len() int {
 // Reset clears request header.
 func (h *RequestHeader) Reset() {
 	h.disableNormalizing = false
+	h.Trailer().disableNormalizing = false
 	h.ResetSkipNormalize()
 }
 
@@ -1043,6 +1064,8 @@ func (h *RequestHeader) del(key []byte) {
 		h.contentLengthBytes = h.contentLengthBytes[:0]
 	case consts.HeaderConnection:
 		h.connectionClose = false
+	case consts.HeaderTrailer:
+		h.Trailer().ResetSkipNormalize()
 	}
 	h.h = delAllArgsBytes(h.h, key)
 }
@@ -1063,6 +1086,7 @@ func (h *RequestHeader) CopyTo(dst *RequestHeader) {
 	dst.host = append(dst.host[:0], h.host...)
 	dst.contentType = append(dst.contentType[:0], h.contentType...)
 	dst.userAgent = append(dst.userAgent[:0], h.userAgent...)
+	h.Trailer().CopyTo(dst.Trailer())
 	dst.h = copyArgs(dst.h, h.h)
 	dst.cookies = copyArgs(dst.cookies, h.cookies)
 	dst.cookiesCollected = h.cookiesCollected
@@ -1391,6 +1415,7 @@ func (h *RequestHeader) ResetSkipNormalize() {
 
 	h.rawHeaders = h.rawHeaders[:0]
 	h.mulHeader = h.mulHeader[:0]
+	h.Trailer().ResetSkipNormalize()
 }
 
 func peekRawHeader(buf, key []byte) []byte {
@@ -1481,6 +1506,9 @@ func (h *RequestHeader) VisitAll(f func(key, value []byte)) {
 	if len(userAgent) > 0 {
 		f(bytestr.StrUserAgent, userAgent)
 	}
+	if !h.Trailer().Empty() {
+		f(bytestr.StrTrailer, h.Trailer().GetBytes())
+	}
 
 	h.collectCookies()
 	if len(h.cookies) > 0 {
@@ -1561,6 +1589,8 @@ func (h *RequestHeader) peek(key []byte) []byte {
 			return appendRequestCookieBytes(nil, h.cookies)
 		}
 		return peekArgBytes(h.h, key)
+	case consts.HeaderTrailer:
+		return h.Trailer().GetBytes()
 	default:
 		return peekArgBytes(h.h, key)
 	}
@@ -1689,6 +1719,9 @@ func (h *ResponseHeader) setSpecialHeader(key, value []byte) bool {
 		if utils.CaseInsensitiveCompare(bytestr.StrTransferEncoding, key) {
 			// Transfer-Encoding is managed automatically.
 			return true
+		} else if utils.CaseInsensitiveCompare(bytestr.StrTrailer, key) {
+			h.Trailer().SetTrailers(value)
+			return true
 		}
 	case 'd':
 		if utils.CaseInsensitiveCompare(bytestr.StrDate, key) {
@@ -1734,6 +1767,11 @@ func (h *RequestHeader) setSpecialHeader(key, value []byte) bool {
 		if utils.CaseInsensitiveCompare(bytestr.StrTransferEncoding, key) {
 			// Transfer-Encoding is managed automatically.
 			return true
+		} else if utils.CaseInsensitiveCompare(bytestr.StrTrailer, key) {
+			// copy value to avoid panic
+			value = append(h.bufKV.value[:0], value...)
+			h.Trailer().SetTrailers(value)
+			return true
 		}
 	case 'h':
 		if utils.CaseInsensitiveCompare(bytestr.StrHost, key) {
@@ -1749,3 +1787,19 @@ func (h *RequestHeader) setSpecialHeader(key, value []byte) bool {
 
 	return false
 }
+
+// Trailer returns the Trailer of HTTP Header.
+func (h *ResponseHeader) Trailer() *Trailer {
+	if h.trailer == nil {
+		h.trailer = new(Trailer)
+	}
+	return h.trailer
+}
+
+// Trailer returns the Trailer of HTTP Header.
+func (h *RequestHeader) Trailer() *Trailer {
+	if h.trailer == nil {
+		h.trailer = new(Trailer)
+	}
+	return h.trailer
+}
diff --git a/pkg/protocol/http1/ext/common.go b/pkg/protocol/http1/ext/common.go
index 9383174..b31b40c 100644
--- a/pkg/protocol/http1/ext/common.go
+++ b/pkg/protocol/http1/ext/common.go
@@ -43,14 +43,17 @@ package ext
 
 import (
 	"bytes"
+	"errors"
 	"fmt"
 	"io"
+	"strings"
 
 	"github.com/cloudwego/hertz/internal/bytesconv"
 	"github.com/cloudwego/hertz/internal/bytestr"
 	errs "github.com/cloudwego/hertz/pkg/common/errors"
 	"github.com/cloudwego/hertz/pkg/common/utils"
 	"github.com/cloudwego/hertz/pkg/network"
+	"github.com/cloudwego/hertz/pkg/protocol"
 	"github.com/cloudwego/hertz/pkg/protocol/consts"
 )
 
@@ -115,14 +118,14 @@ func WriteBodyChunked(w network.Writer, r io.Reader) error {
 				panic("BUG: io.Reader returned 0, nil")
 			}
 			if err == io.EOF {
-				if err = writeChunk(w, buf[:0]); err != nil {
+				if err = WriteChunk(w, buf[:0], true); err != nil {
 					break
 				}
 				err = nil
 			}
 			break
 		}
-		if err = writeChunk(w, buf[:n]); err != nil {
+		if err = WriteChunk(w, buf[:n], true); err != nil {
 			break
 		}
 	}
@@ -253,6 +256,10 @@ func readBodyChunked(r network.Reader, maxBodySize int, dst []byte) ([]byte, err
 		if err != nil {
 			return dst, err
 		}
+		// If it is the end of chunk, Read CRLF after reading trailer
+		if chunkSize == 0 {
+			return dst, nil
+		}
 		if maxBodySize > 0 && len(dst)+chunkSize > maxBodySize {
 			return dst, errBodyTooLarge
 		}
@@ -264,9 +271,6 @@ func readBodyChunked(r network.Reader, maxBodySize int, dst []byte) ([]byte, err
 			return dst, errBrokenChunk
 		}
 		dst = dst[:len(dst)-strCRLFLen]
-		if chunkSize == 0 {
-			return dst, nil
-		}
 	}
 }
 
@@ -283,7 +287,7 @@ func round2(n int) int {
 	return 1 << x
 }
 
-func writeChunk(w network.Writer, b []byte) (err error) {
+func WriteChunk(w network.Writer, b []byte, withFlush bool) (err error) {
 	n := len(b)
 	if err = bytesconv.WriteHexInt(w, n); err != nil {
 		return err
@@ -293,8 +297,15 @@ func writeChunk(w network.Writer, b []byte) (err error) {
 	if _, err = w.WriteBinary(b); err != nil {
 		return err
 	}
-	w.WriteBinary(bytestr.StrCRLF) //nolint:errcheck
 
+	// If it is the end of chunk, write CRLF after writing trailer
+	if n > 0 {
+		w.WriteBinary(bytestr.StrCRLF) //nolint:errcheck
+	}
+
+	if !withFlush {
+		return nil
+	}
 	err = w.Flush()
 	return
 }
@@ -378,3 +389,152 @@ func stripSpace(b []byte) []byte {
 	}
 	return b
 }
+
+func SkipTrailer(r network.Reader) error {
+	n := 1
+	for {
+		err := trySkipTrailer(r, n)
+		if err == nil {
+			return nil
+		}
+		if !errors.Is(err, errs.ErrNeedMore) {
+			return err
+		}
+		// No more data available on the wire, try block peek(by netpoll)
+		if n == r.Len() {
+			n++
+
+			continue
+		}
+		n = r.Len()
+	}
+}
+
+func trySkipTrailer(r network.Reader, n int) error {
+	b, err := r.Peek(n)
+	if len(b) == 0 {
+		// Return ErrTimeout on any timeout.
+		if err != nil && strings.Contains(err.Error(), "timeout") {
+			return errs.New(errs.ErrTimeout, errs.ErrorTypePublic, "read response header")
+		}
+
+		if n == 1 || err == io.EOF {
+			return io.EOF
+		}
+
+		return errs.NewPublicf("error when reading request trailer: %w", err)
+	}
+	b = MustPeekBuffered(r)
+	headersLen, errParse := skipTrailer(b)
+	if errParse != nil {
+		if err == io.EOF {
+			return err
+		}
+		return HeaderError("response", err, errParse, b)
+	}
+	MustDiscard(r, headersLen)
+	return nil
+}
+
+func skipTrailer(buf []byte) (int, error) {
+	skip := 0
+	strCRLFLen := len(bytestr.StrCRLF)
+	for {
+		index := bytes.Index(buf, bytestr.StrCRLF)
+		if index == -1 {
+			return 0, errs.ErrNeedMore
+		}
+
+		buf = buf[index+strCRLFLen:]
+		skip += index + strCRLFLen
+
+		if index == 0 {
+			return skip, nil
+		}
+	}
+}
+
+func ReadTrailer(t *protocol.Trailer, r network.Reader) error {
+	n := 1
+	for {
+		err := tryReadTrailer(t, r, n)
+		if err == nil {
+			return nil
+		}
+		if !errors.Is(err, errs.ErrNeedMore) {
+			t.ResetSkipNormalize()
+			return err
+		}
+		// No more data available on the wire, try block peek(by netpoll)
+		if n == r.Len() {
+			n++
+
+			continue
+		}
+		n = r.Len()
+	}
+}
+
+func tryReadTrailer(t *protocol.Trailer, r network.Reader, n int) error {
+	b, err := r.Peek(n)
+	if len(b) == 0 {
+		// Return ErrTimeout on any timeout.
+		if err != nil && strings.Contains(err.Error(), "timeout") {
+			return errs.New(errs.ErrTimeout, errs.ErrorTypePublic, "read response header")
+		}
+
+		if n == 1 || err == io.EOF {
+			return io.EOF
+		}
+
+		return errs.NewPublicf("error when reading request trailer: %w", err)
+	}
+	b = MustPeekBuffered(r)
+	headersLen, errParse := parseTrailer(t, b)
+	if errParse != nil {
+		if err == io.EOF {
+			return err
+		}
+		return HeaderError("response", err, errParse, b)
+	}
+	MustDiscard(r, headersLen)
+	return nil
+}
+
+func parseTrailer(t *protocol.Trailer, buf []byte) (int, error) {
+	// Skip any 0 length chunk.
+	if buf[0] == '0' {
+		skip := len(bytestr.StrCRLF) + 1
+		if len(buf) < skip {
+			return 0, io.EOF
+		}
+		buf = buf[skip:]
+	}
+
+	var s HeaderScanner
+	s.B = buf
+	s.DisableNormalizing = t.IsDisableNormalizing()
+	var err error
+	for s.Next() {
+		if len(s.Key) > 0 {
+			if bytes.IndexByte(s.Key, ' ') != -1 || bytes.IndexByte(s.Key, '\t') != -1 {
+				err = fmt.Errorf("invalid trailer key %q", s.Key)
+				continue
+			}
+			err = t.UpdateArgBytes(s.Key, s.Value)
+		}
+	}
+	if s.Err != nil {
+		return 0, s.Err
+	}
+	if err != nil {
+		return 0, err
+	}
+	return s.HLen, nil
+}
+
+// writeTrailer writes response trailer to w
+func WriteTrailer(t *protocol.Trailer, w network.Writer) error {
+	_, err := w.WriteBinary(t.Header())
+	return err
+}
diff --git a/pkg/protocol/http1/ext/stream.go b/pkg/protocol/http1/ext/stream.go
index d72e995..a09e1e3 100644
--- a/pkg/protocol/http1/ext/stream.go
+++ b/pkg/protocol/http1/ext/stream.go
@@ -46,10 +46,12 @@ import (
 	"io"
 	"sync"
 
+	"github.com/cloudwego/hertz/internal/bytestr"
 	"github.com/cloudwego/hertz/pkg/common/bytebufferpool"
 	errs "github.com/cloudwego/hertz/pkg/common/errors"
 	"github.com/cloudwego/hertz/pkg/common/utils"
 	"github.com/cloudwego/hertz/pkg/network"
+	"github.com/cloudwego/hertz/pkg/protocol"
 )
 
 var (
@@ -62,25 +64,20 @@ var (
 	}
 )
 
+// Deprecated: Use github.com/cloudwego/hertz/pkg/protocol.NoBody instead.
+var NoBody = protocol.NoBody
+
 type bodyStream struct {
 	prefetchedBytes *bytes.Reader
 	reader          network.Reader
+	trailer         *protocol.Trailer
 	offset          int
 	contentLength   int
 	chunkLeft       int
+	// whether the chunk has reached the EOF
+	chunkEOF bool
 }
 
-// NoBody is an io.ReadCloser with no bytes. Read always returns EOF
-// and Close always returns nil. It can be used in an outgoing client
-// request to explicitly signal that a request has zero bytes.
-// An alternative, however, is to simply set Request.Body to nil.
-var NoBody = noBody{}
-
-type noBody struct{}
-
-func (noBody) Read([]byte) (int, error) { return 0, io.EOF }
-func (noBody) Close() error             { return nil }
-
 func ReadBodyWithStreaming(zr network.Reader, contentLength, maxBodySize int, dst []byte) (b []byte, err error) {
 	if contentLength == -1 {
 		// handled in requestStream.Read()
@@ -114,11 +111,13 @@ func ReadBodyWithStreaming(zr network.Reader, contentLength, maxBodySize int, ds
 	return b, nil
 }
 
-func AcquireBodyStream(b *bytebufferpool.ByteBuffer, r network.Reader, contentLength int) io.Reader {
+func AcquireBodyStream(b *bytebufferpool.ByteBuffer, r network.Reader, t *protocol.Trailer, contentLength int) io.Reader {
 	rs := bodyStreamPool.Get().(*bodyStream)
 	rs.prefetchedBytes = bytes.NewReader(b.B)
 	rs.reader = r
 	rs.contentLength = contentLength
+	rs.trailer = t
+	rs.chunkEOF = false
 
 	return rs
 }
@@ -130,14 +129,19 @@ func (rs *bodyStream) Read(p []byte) (int, error) {
 		}
 	}()
 	if rs.contentLength == -1 {
+		if rs.chunkEOF {
+			return 0, io.EOF
+		}
+
 		if rs.chunkLeft == 0 {
 			chunkSize, err := utils.ParseChunkSize(rs.reader)
 			if err != nil {
 				return 0, err
 			}
 			if chunkSize == 0 {
-				err = utils.SkipCRLF(rs.reader)
+				err = ReadTrailer(rs.trailer, rs.reader)
 				if err == nil {
+					rs.chunkEOF = true
 					err = io.EOF
 				}
 				return 0, err
@@ -231,6 +235,45 @@ func (rs *bodyStream) skipRest() error {
 	if rs.prefetchedBytes == nil {
 		return nil
 	}
+
+	// the request is chunked encoding
+	if rs.contentLength == -1 {
+		if rs.chunkEOF {
+			return nil
+		}
+
+		strCRLFLen := len(bytestr.StrCRLF)
+		for {
+			chunkSize, err := utils.ParseChunkSize(rs.reader)
+			if err != nil {
+				return err
+			}
+
+			if chunkSize == 0 {
+				rs.chunkEOF = true
+				return SkipTrailer(rs.reader)
+			}
+
+			err = rs.reader.Skip(chunkSize)
+			if err != nil {
+				return err
+			}
+
+			crlf, err := rs.reader.Peek(strCRLFLen)
+			if err != nil {
+				return err
+			}
+
+			if !bytes.Equal(crlf, bytestr.StrCRLF) {
+				return errBrokenChunk
+			}
+
+			err = rs.reader.Skip(strCRLFLen)
+			if err != nil {
+				return err
+			}
+		}
+	}
 	// max value of pSize is 8193, it's safe.
 	pSize := int(rs.prefetchedBytes.Size())
 	if rs.contentLength <= pSize || rs.offset == rs.contentLength {
@@ -274,6 +317,7 @@ func ReleaseBodyStream(requestReader io.Reader) (err error) {
 		rs.prefetchedBytes = nil
 		rs.offset = 0
 		rs.reader = nil
+		rs.trailer = nil
 		bodyStreamPool.Put(rs)
 	}
 	return
diff --git a/pkg/protocol/http1/req/header.go b/pkg/protocol/http1/req/header.go
index b5f35d2..da7e729 100644
--- a/pkg/protocol/http1/req/header.go
+++ b/pkg/protocol/http1/req/header.go
@@ -232,6 +232,14 @@ func parseHeaders(h *protocol.RequestHeader, buf []byte) (int, error) {
 					}
 					continue
 				}
+				if utils.CaseInsensitiveCompare(s.Key, bytestr.StrTrailer) {
+					if nerr := h.Trailer().SetTrailers(s.Value); nerr != nil {
+						if err == nil {
+							err = nerr
+						}
+					}
+					continue
+				}
 			}
 		}
 		h.AddArgBytes(s.Key, s.Value, protocol.ArgsHasValue)
diff --git a/pkg/protocol/http1/req/request.go b/pkg/protocol/http1/req/request.go
index 13b45ad..f010747 100644
--- a/pkg/protocol/http1/req/request.go
+++ b/pkg/protocol/http1/req/request.go
@@ -289,19 +289,19 @@ func ContinueReadBodyStream(req *protocol.Request, zr network.Reader, maxBodySiz
 	if err != nil {
 		if errors.Is(err, errs.ErrBodyTooLarge) {
 			req.Header.SetContentLength(contentLength)
-			req.ConstructBodyStream(bodyBuf, ext.AcquireBodyStream(bodyBuf, zr, contentLength))
+			req.ConstructBodyStream(bodyBuf, ext.AcquireBodyStream(bodyBuf, zr, req.Header.Trailer(), contentLength))
 
 			return nil
 		}
 		if errors.Is(err, errs.ErrChunkedStream) {
-			req.ConstructBodyStream(bodyBuf, ext.AcquireBodyStream(bodyBuf, zr, contentLength))
+			req.ConstructBodyStream(bodyBuf, ext.AcquireBodyStream(bodyBuf, zr, req.Header.Trailer(), contentLength))
 			return nil
 		}
 		req.Reset()
 		return err
 	}
 
-	req.ConstructBodyStream(bodyBuf, ext.AcquireBodyStream(bodyBuf, zr, contentLength))
+	req.ConstructBodyStream(bodyBuf, ext.AcquireBodyStream(bodyBuf, zr, req.Header.Trailer(), contentLength))
 	return nil
 }
 
@@ -358,6 +358,14 @@ func ContinueReadBody(req *protocol.Request, r network.Reader, maxBodySize int,
 		req.Reset()
 		return err
 	}
+
+	if req.Header.ContentLength() == -1 {
+		err = ext.ReadTrailer(req.Header.Trailer(), r)
+		if err != nil && err != io.EOF {
+			return err
+		}
+	}
+
 	req.Header.SetContentLength(len(bodyBuf.B))
 	return nil
 }
@@ -416,9 +424,13 @@ func writeBodyStream(req *protocol.Request, w network.Writer) error {
 		}
 	} else {
 		req.Header.SetContentLength(-1)
-		if err = WriteHeader(&req.Header, w); err == nil {
+		err = WriteHeader(&req.Header, w)
+		if err == nil {
 			err = ext.WriteBodyChunked(w, req.BodyStream())
 		}
+		if err == nil {
+			err = ext.WriteTrailer(req.Header.Trailer(), w)
+		}
 	}
 	err1 := req.CloseBodyStream()
 	if err == nil {
diff --git a/pkg/protocol/http1/resp/header.go b/pkg/protocol/http1/resp/header.go
index 0944ef7..622c72b 100644
--- a/pkg/protocol/http1/resp/header.go
+++ b/pkg/protocol/http1/resp/header.go
@@ -183,6 +183,10 @@ func parseHeaders(h *protocol.ResponseHeader, buf []byte) (int, error) {
 					}
 					continue
 				}
+				if utils.CaseInsensitiveCompare(s.Key, bytestr.StrTrailer) {
+					err = h.Trailer().SetTrailers(s.Value)
+					continue
+				}
 			}
 			h.AddArgBytes(s.Key, s.Value, protocol.ArgsHasValue)
 		}
@@ -205,7 +209,7 @@ func parseHeaders(h *protocol.ResponseHeader, buf []byte) (int, error) {
 		h.SetConnectionClose(!ext.HasHeaderValue(v, bytestr.StrKeepAlive))
 	}
 
-	return len(buf) - len(s.B), nil
+	return len(buf) - len(s.B), err
 }
 
 func parse(h *protocol.ResponseHeader, buf []byte) (int, error) {
diff --git a/pkg/protocol/http1/resp/response.go b/pkg/protocol/http1/resp/response.go
index c23039b..819d39d 100644
--- a/pkg/protocol/http1/resp/response.go
+++ b/pkg/protocol/http1/resp/response.go
@@ -45,6 +45,7 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"runtime"
 	"sync"
 
 	"github.com/cloudwego/hertz/pkg/common/bytebufferpool"
@@ -106,6 +107,8 @@ func ReadHeaderAndLimitBody(resp *protocol.Response, r network.Reader, maxBodySi
 		}
 	}
 
+	var cLen int
+
 	if !resp.MustSkipBody() {
 		bodyBuf := resp.BodyBuffer()
 		bodyBuf.Reset()
@@ -113,8 +116,20 @@ func ReadHeaderAndLimitBody(resp *protocol.Response, r network.Reader, maxBodySi
 		if err != nil {
 			return err
 		}
-		resp.Header.SetContentLength(len(bodyBuf.B))
+		cLen = len(bodyBuf.B)
+	}
+
+	if resp.Header.ContentLength() == -1 {
+		err = ext.ReadTrailer(resp.Header.Trailer(), r)
+		if err != nil && err != io.EOF {
+			return err
+		}
 	}
+
+	if !resp.MustSkipBody() {
+		resp.Header.SetContentLength(cLen)
+	}
+
 	return nil
 }
 
@@ -124,6 +139,7 @@ type clientRespStream struct {
 }
 
 func (c *clientRespStream) Close() (err error) {
+	runtime.SetFinalizer(c, nil)
 	ext.ReleaseBodyStream(c.r)
 	if c.closeCallback != nil {
 		err = c.closeCallback()
@@ -152,6 +168,7 @@ func convertClientRespStream(bs io.Reader, fn func() error) *clientRespStream {
 	clientStream := clientRespStreamPool.Get().(*clientRespStream)
 	clientStream.r = bs
 	clientStream.closeCallback = fn
+	runtime.SetFinalizer(clientStream, (*clientRespStream).Close)
 	return clientStream
 }
 
@@ -179,13 +196,13 @@ func ReadBodyStream(resp *protocol.Response, r network.Reader, maxBodySize int,
 	bodyBuf.B, err = ext.ReadBodyWithStreaming(r, resp.Header.ContentLength(), maxBodySize, bodyBuf.B)
 	if err != nil {
 		if errors.Is(err, errs.ErrBodyTooLarge) {
-			bodyStream := ext.AcquireBodyStream(bodyBuf, r, resp.Header.ContentLength())
+			bodyStream := ext.AcquireBodyStream(bodyBuf, r, resp.Header.Trailer(), resp.Header.ContentLength())
 			resp.ConstructBodyStream(bodyBuf, convertClientRespStream(bodyStream, closeCallBack))
 			return nil
 		}
 
 		if errors.Is(err, errs.ErrChunkedStream) {
-			bodyStream := ext.AcquireBodyStream(bodyBuf, r, -1)
+			bodyStream := ext.AcquireBodyStream(bodyBuf, r, resp.Header.Trailer(), -1)
 			resp.ConstructBodyStream(bodyBuf, convertClientRespStream(bodyStream, closeCallBack))
 			return nil
 		}
@@ -194,7 +211,7 @@ func ReadBodyStream(resp *protocol.Response, r network.Reader, maxBodySize int,
 		return err
 	}
 
-	bodyStream := ext.AcquireBodyStream(bodyBuf, r, resp.Header.ContentLength())
+	bodyStream := ext.AcquireBodyStream(bodyBuf, r, resp.Header.Trailer(), resp.Header.ContentLength())
 	resp.ConstructBodyStream(bodyBuf, convertClientRespStream(bodyStream, closeCallBack))
 	return nil
 }
@@ -277,6 +294,9 @@ func writeBodyStream(resp *protocol.Response, w network.Writer, sendBody bool) (
 			if err == nil {
 				err = ext.WriteBodyChunked(w, resp.BodyStream())
 			}
+			if err == nil {
+				err = ext.WriteTrailer(resp.Header.Trailer(), w)
+			}
 		}
 	}
 	err1 := resp.CloseBodyStream()
diff --git a/pkg/protocol/http1/resp/writer.go b/pkg/protocol/http1/resp/writer.go
new file mode 100644
index 0000000..745ab4c
--- /dev/null
+++ b/pkg/protocol/http1/resp/writer.go
@@ -0,0 +1,76 @@
+/*
+ * 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 resp
+
+import (
+	"sync"
+
+	"github.com/cloudwego/hertz/pkg/network"
+	"github.com/cloudwego/hertz/pkg/protocol"
+	"github.com/cloudwego/hertz/pkg/protocol/http1/ext"
+)
+
+type chunkedBodyWriter struct {
+	sync.Once
+	finalizeErr error
+	wroteHeader bool
+	r           *protocol.Response
+	w           network.Writer
+}
+
+// Write will encode chunked p before writing
+// It will only return the length of p and a nil error if the writing is successful or 0, error otherwise.
+//
+// NOTE: Write will use the user buffer to flush.
+// Before flush successfully, the buffer b should be valid.
+func (c *chunkedBodyWriter) Write(p []byte) (n int, err error) {
+	if !c.wroteHeader {
+		c.r.Header.SetContentLength(-1)
+		if err = WriteHeader(&c.r.Header, c.w); err != nil {
+			return
+		}
+		c.wroteHeader = true
+	}
+	if err = ext.WriteChunk(c.w, p, false); err != nil {
+		return
+	}
+	return len(p), nil
+}
+
+func (c *chunkedBodyWriter) Flush() error {
+	return c.w.Flush()
+}
+
+// Finalize will write the ending chunk as well as trailer and flush the writer.
+// Warning: do not call this method by yourself, unless you know what you are doing.
+func (c *chunkedBodyWriter) Finalize() error {
+	c.Do(func() {
+		c.finalizeErr = ext.WriteChunk(c.w, nil, true)
+		if c.finalizeErr != nil {
+			return
+		}
+		c.finalizeErr = ext.WriteTrailer(c.r.Header.Trailer(), c.w)
+	})
+	return c.finalizeErr
+}
+
+func NewChunkedBodyWriter(r *protocol.Response, w network.Writer) network.ExtWriter {
+	return &chunkedBodyWriter{
+		r: r,
+		w: w,
+	}
+}
diff --git a/pkg/protocol/http1/server.go b/pkg/protocol/http1/server.go
index 99d2da7..ba8915a 100644
--- a/pkg/protocol/http1/server.go
+++ b/pkg/protocol/http1/server.go
@@ -413,6 +413,11 @@ func writeErrorResponse(zw network.Writer, ctx *app.RequestContext, serverName [
 }
 
 func writeResponse(ctx *app.RequestContext, w network.Writer) error {
+	// Skip default response writing logic if it has been hijacked
+	if ctx.Response.GetHijackWriter() != nil {
+		return ctx.Response.GetHijackWriter().Finalize()
+	}
+
 	err := resp.Write(&ctx.Response, w)
 	if err != nil {
 		return err
diff --git a/pkg/protocol/request.go b/pkg/protocol/request.go
index 8b86067..f6c138a 100644
--- a/pkg/protocol/request.go
+++ b/pkg/protocol/request.go
@@ -62,7 +62,6 @@ import (
 	"github.com/cloudwego/hertz/pkg/common/utils"
 	"github.com/cloudwego/hertz/pkg/network"
 	"github.com/cloudwego/hertz/pkg/protocol/consts"
-	"github.com/cloudwego/hertz/pkg/protocol/http1/ext"
 )
 
 var (
@@ -75,6 +74,17 @@ var (
 	requestPool sync.Pool
 )
 
+// NoBody is an io.ReadCloser with no bytes. Read always returns EOF
+// and Close always returns nil. It can be used in an outgoing client
+// request to explicitly signal that a request has zero bytes.
+// An alternative, however, is to simply set Request.Body to nil.
+var NoBody = noBody{}
+
+type noBody struct{}
+
+func (noBody) Read([]byte) (int, error) { return 0, io.EOF }
+func (noBody) Close() error             { return nil }
+
 type Request struct {
 	noCopy nocopy.NoCopy //lint:ignore U1000 until noCopy is used
 
@@ -615,12 +625,12 @@ func (req *Request) HasMultipartForm() bool {
 
 // IsBodyStream returns true if body is set via SetBodyStream*
 func (req *Request) IsBodyStream() bool {
-	return req.bodyStream != nil && req.bodyStream != ext.NoBody
+	return req.bodyStream != nil && req.bodyStream != NoBody
 }
 
 func (req *Request) BodyStream() io.Reader {
 	if req.bodyStream == nil {
-		req.bodyStream = ext.NoBody
+		req.bodyStream = NoBody
 	}
 	return req.bodyStream
 }
diff --git a/pkg/protocol/response.go b/pkg/protocol/response.go
index 04440f3..016af92 100644
--- a/pkg/protocol/response.go
+++ b/pkg/protocol/response.go
@@ -46,6 +46,7 @@ import (
 	"net"
 	"sync"
 
+	"github.com/cloudwego/hertz/internal/bytesconv"
 	"github.com/cloudwego/hertz/internal/nocopy"
 	"github.com/cloudwego/hertz/pkg/common/bytebufferpool"
 	"github.com/cloudwego/hertz/pkg/common/compress"
@@ -90,6 +91,17 @@ type Response struct {
 	raddr net.Addr
 	// Local TCPAddr from concurrently net.Conn
 	laddr net.Addr
+
+	// If set a hijackWriter, hertz will skip the default header/body writer process.
+	hijackWriter network.ExtWriter
+}
+
+func (resp *Response) GetHijackWriter() network.ExtWriter {
+	return resp.hijackWriter
+}
+
+func (resp *Response) HijackWriter(writer network.ExtWriter) {
+	resp.hijackWriter = writer
 }
 
 type responseBodyWriter struct {
@@ -133,7 +145,7 @@ func (resp *Response) ConstructBodyStream(body *bytebufferpool.ByteBuffer, bodyS
 // BodyWriter returns writer for populating response body.
 //
 // If used inside RequestHandler, the returned writer must not be used
-// after returning from RequestHandler. Use RequestCtx.Write
+// after returning from RequestHandler. Use RequestContext.Write
 // or SetBodyStreamWriter in this case.
 func (resp *Response) BodyWriter() io.Writer {
 	resp.w.r = resp
@@ -265,6 +277,7 @@ func (resp *Response) Reset() {
 	resp.raddr = nil
 	resp.laddr = nil
 	resp.ImmediateHeaderFlush = false
+	resp.hijackWriter = nil
 }
 
 func (resp *Response) resetSkipHeader() {
@@ -302,8 +315,22 @@ func (resp *Response) StatusCode() int {
 //
 // It is safe re-using body argument after the function returns.
 func (resp *Response) SetBody(body []byte) {
-	resp.CloseBodyStream()      //nolint:errcheck
-	resp.BodyBuffer().Set(body) //nolint:errcheck
+	resp.CloseBodyStream() //nolint:errcheck
+	if resp.GetHijackWriter() == nil {
+		resp.BodyBuffer().Set(body) //nolint:errcheck
+		return
+	}
+
+	// If the hijack writer support .SetBody() api, then use it.
+	if setter, ok := resp.GetHijackWriter().(interface {
+		SetBody(b []byte)
+	}); ok {
+		setter.SetBody(body)
+		return
+	}
+
+	// Otherwise, call .Write() api instead.
+	resp.GetHijackWriter().Write(body) //nolint:errcheck
 }
 
 func (resp *Response) BodyStream() io.Reader {
@@ -314,13 +341,21 @@ func (resp *Response) BodyStream() io.Reader {
 //
 // It is safe re-using p after the function returns.
 func (resp *Response) AppendBody(p []byte) {
-	resp.CloseBodyStream()     //nolint:errcheck
+	resp.CloseBodyStream() //nolint:errcheck
+	if resp.hijackWriter != nil {
+		resp.hijackWriter.Write(p) //nolint:errcheck
+		return
+	}
 	resp.BodyBuffer().Write(p) //nolint:errcheck
 }
 
 // AppendBodyString appends s to response body.
 func (resp *Response) AppendBodyString(s string) {
-	resp.CloseBodyStream()           //nolint:errcheck
+	resp.CloseBodyStream() //nolint:errcheck
+	if resp.hijackWriter != nil {
+		resp.hijackWriter.Write(bytesconv.S2b(s)) //nolint:errcheck
+		return
+	}
 	resp.BodyBuffer().WriteString(s) //nolint:errcheck
 }
 
diff --git a/pkg/protocol/trailer.go b/pkg/protocol/trailer.go
new file mode 100644
index 0000000..6820f87
--- /dev/null
+++ b/pkg/protocol/trailer.go
@@ -0,0 +1,224 @@
+/*
+ * 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 protocol
+
+import (
+	"bytes"
+
+	"github.com/cloudwego/hertz/internal/bytestr"
+	errs "github.com/cloudwego/hertz/pkg/common/errors"
+	"github.com/cloudwego/hertz/pkg/common/utils"
+	"github.com/cloudwego/hertz/pkg/protocol/consts"
+)
+
+type Trailer struct {
+	h                  []argsKV
+	bufKV              argsKV
+	disableNormalizing bool
+}
+
+// Get returns trailer value for the given key.
+func (t *Trailer) Get(key string) string {
+	return string(t.Peek(key))
+}
+
+// Peek returns trailer value for the given key.
+//
+// Returned value is valid until the next call to Trailer.
+// Do not store references to returned value. Make copies instead.
+func (t *Trailer) Peek(key string) []byte {
+	k := getHeaderKeyBytes(&t.bufKV, key, t.disableNormalizing)
+	return peekArgBytes(t.h, k)
+}
+
+// Del deletes trailer with the given key.
+func (t *Trailer) Del(key string) {
+	k := getHeaderKeyBytes(&t.bufKV, key, t.disableNormalizing)
+	t.h = delAllArgsBytes(t.h, k)
+}
+
+// VisitAll calls f for each header.
+func (t *Trailer) VisitAll(f func(key, value []byte)) {
+	visitArgs(t.h, f)
+}
+
+// Set sets the given 'key: value' trailer.
+//
+// If the key is forbidden by RFC 7230, section 4.1.2, Set will return error
+func (t *Trailer) Set(key, value string) error {
+	initHeaderKV(&t.bufKV, key, value, t.disableNormalizing)
+	return t.setArgBytes(t.bufKV.key, t.bufKV.value, ArgsHasValue)
+}
+
+// Add adds the given 'key: value' trailer.
+//
+// Multiple headers with the same key may be added with this function.
+// Use Set for setting a single header for the given key.
+//
+// If the key is forbidden by RFC 7230, section 4.1.2, Add will return error
+func (t *Trailer) Add(key, value string) error {
+	initHeaderKV(&t.bufKV, key, value, t.disableNormalizing)
+	return t.addArgBytes(t.bufKV.key, t.bufKV.value, ArgsHasValue)
+}
+
+func (t *Trailer) addArgBytes(key, value []byte, noValue bool) error {
+	if IsBadTrailer(key) {
+		return errs.NewPublicf("forbidden trailer key: %q", key)
+	}
+	t.h = appendArgBytes(t.h, key, value, noValue)
+	return nil
+}
+
+func (t *Trailer) setArgBytes(key, value []byte, noValue bool) error {
+	if IsBadTrailer(key) {
+		return errs.NewPublicf("forbidden trailer key: %q", key)
+	}
+	t.h = setArgBytes(t.h, key, value, noValue)
+	return nil
+}
+
+func (t *Trailer) UpdateArgBytes(key, value []byte) error {
+	if IsBadTrailer(key) {
+		return errs.NewPublicf("forbidden trailer key: %q", key)
+	}
+
+	t.h = updateArgBytes(t.h, key, value)
+	return nil
+}
+
+func (t *Trailer) GetTrailers() []argsKV {
+	return t.h
+}
+
+func (t *Trailer) Empty() bool {
+	return len(t.h) == 0
+}
+
+// GetBytes return the 'Trailer' Header which is composed by the Trailer key
+func (t *Trailer) GetBytes() []byte {
+	var dst []byte
+	for i, n := 0, len(t.h); i < n; i++ {
+		kv := &t.h[i]
+		dst = append(dst, kv.key...)
+		if i+1 < n {
+			dst = append(dst, bytestr.StrCommaSpace...)
+		}
+	}
+	return dst
+}
+
+func (t *Trailer) ResetSkipNormalize() {
+	t.h = t.h[:0]
+}
+
+func (t *Trailer) Reset() {
+	t.disableNormalizing = false
+	t.ResetSkipNormalize()
+}
+
+func (t *Trailer) DisableNormalizing() {
+	t.disableNormalizing = true
+}
+
+func (t *Trailer) IsDisableNormalizing() bool {
+	return t.disableNormalizing
+}
+
+// CopyTo copies all the trailer to dst.
+func (t *Trailer) CopyTo(dst *Trailer) {
+	dst.Reset()
+
+	dst.disableNormalizing = t.disableNormalizing
+	dst.h = copyArgs(dst.h, t.h)
+}
+
+func (t *Trailer) SetTrailers(trailers []byte) (err error) {
+	t.ResetSkipNormalize()
+	for i := -1; i+1 < len(trailers); {
+		trailers = trailers[i+1:]
+		i = bytes.IndexByte(trailers, ',')
+		if i < 0 {
+			i = len(trailers)
+		}
+		trailerKey := trailers[:i]
+		for len(trailerKey) > 0 && trailerKey[0] == ' ' {
+			trailerKey = trailerKey[1:]
+		}
+		for len(trailerKey) > 0 && trailerKey[len(trailerKey)-1] == ' ' {
+			trailerKey = trailerKey[:len(trailerKey)-1]
+		}
+
+		utils.NormalizeHeaderKey(trailerKey, t.disableNormalizing)
+		err = t.addArgBytes(trailerKey, nilByteSlice, argsNoValue)
+	}
+	return
+}
+
+func (t *Trailer) Header() []byte {
+	t.bufKV.value = t.AppendBytes(t.bufKV.value[:0])
+	return t.bufKV.value
+}
+
+func (t *Trailer) AppendBytes(dst []byte) []byte {
+	for i, n := 0, len(t.h); i < n; i++ {
+		kv := &t.h[i]
+		dst = appendHeaderLine(dst, kv.key, kv.value)
+	}
+
+	dst = append(dst, bytestr.StrCRLF...)
+	return dst
+}
+
+func IsBadTrailer(key []byte) bool {
+	switch key[0] | 0x20 {
+	case 'a':
+		return utils.CaseInsensitiveCompare(key, bytestr.StrAuthorization)
+	case 'c':
+		if len(key) >= len(consts.HeaderContentType) && utils.CaseInsensitiveCompare(key[:8], bytestr.StrContentType[:8]) {
+			// skip compare prefix 'Content-'
+			return utils.CaseInsensitiveCompare(key[8:], bytestr.StrContentEncoding[8:]) ||
+				utils.CaseInsensitiveCompare(key[8:], bytestr.StrContentLength[8:]) ||
+				utils.CaseInsensitiveCompare(key[8:], bytestr.StrContentType[8:]) ||
+				utils.CaseInsensitiveCompare(key[8:], bytestr.StrContentRange[8:])
+		}
+		return utils.CaseInsensitiveCompare(key, bytestr.StrConnection)
+	case 'e':
+		return utils.CaseInsensitiveCompare(key, bytestr.StrExpect)
+	case 'h':
+		return utils.CaseInsensitiveCompare(key, bytestr.StrHost)
+	case 'k':
+		return utils.CaseInsensitiveCompare(key, bytestr.StrKeepAlive)
+	case 'm':
+		return utils.CaseInsensitiveCompare(key, bytestr.StrMaxForwards)
+	case 'p':
+		if len(key) >= len(consts.HeaderProxyConnection) && utils.CaseInsensitiveCompare(key[:6], bytestr.StrProxyConnection[:6]) {
+			// skip compare prefix 'Proxy-'
+			return utils.CaseInsensitiveCompare(key[6:], bytestr.StrProxyConnection[6:]) ||
+				utils.CaseInsensitiveCompare(key[6:], bytestr.StrProxyAuthenticate[6:]) ||
+				utils.CaseInsensitiveCompare(key[6:], bytestr.StrProxyAuthorization[6:])
+		}
+	case 'r':
+		return utils.CaseInsensitiveCompare(key, bytestr.StrRange)
+	case 't':
+		return utils.CaseInsensitiveCompare(key, bytestr.StrTE) ||
+			utils.CaseInsensitiveCompare(key, bytestr.StrTrailer) ||
+			utils.CaseInsensitiveCompare(key, bytestr.StrTransferEncoding)
+	case 'w':
+		return utils.CaseInsensitiveCompare(key, bytestr.StrWWWAuthenticate)
+	}
+	return false
+}
diff --git a/version.go b/version.go
index 98ad92c..16b3997 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.5.2"
+	Version = "v0.6.0"
 )


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 1adafe4..b3e284c 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -6,7 +6,7 @@ jobs:
   lint-and-ut:
     strategy:
       matrix:
-        version: [ 1.18, 1.19 ]
+        version: [ '1.18', '1.19', '1.20' ]
     runs-on: [ self-hosted, X64 ]
     steps:
       - uses: actions/checkout@v3
@@ -16,17 +16,12 @@ jobs:
         with:
           go-version: ${{ matrix.version }}
 
-      - uses: actions/cache@v3
-        with:
-          path: ~/go/pkg/mod
-          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
-          restore-keys: |
-            ${{ runner.os }}-go-
       - name: Golangci Lint
         # https://golangci-lint.run/
         uses: golangci/golangci-lint-action@v3
         with:
           version: latest
+          skip-cache: true
       - name: Unit Test
         run: go test -race -covermode=atomic -coverprofile=coverage.txt ./...
 
@@ -36,7 +31,7 @@ jobs:
   ut-windows:
     strategy:
       matrix:
-        version: [ 1.18, 1.19 ]
+        version: [ '1.18', '1.19', '1.20' ]
     runs-on: windows-latest
     steps:
       - uses: actions/checkout@v3
@@ -46,11 +41,5 @@ jobs:
         with:
           go-version: ${{ matrix.version }}
 
-      - uses: actions/cache@v3
-        with:
-          path: $HOME/go/pkg/mod
-          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
-          restore-keys: |
-            ${{ runner.os }}-go-
       - name: Unit Test
         run: go test -race -covermode=atomic  ./...
diff --git a/cmd/hz/protobuf/plugin_test.go b/cmd/hz/protobuf/plugin_test.go
index 7d64709..b34d8b8 100644
--- a/cmd/hz/protobuf/plugin_test.go
+++ b/cmd/hz/protobuf/plugin_test.go
@@ -17,7 +17,7 @@
 package protobuf
 
 import (
-	"os"
+	"io/ioutil"
 	"strings"
 	"testing"
 
@@ -27,7 +27,7 @@ import (
 )
 
 func TestPlugin_Handle(t *testing.T) {
-	in, err := os.ReadFile("../testdata/request_protoc.out")
+	in, err := ioutil.ReadFile("../testdata/request_protoc.out")
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/cmd/hz/protobuf/tag_test.go b/cmd/hz/protobuf/tag_test.go
index 52fd146..7e6528b 100644
--- a/cmd/hz/protobuf/tag_test.go
+++ b/cmd/hz/protobuf/tag_test.go
@@ -17,7 +17,7 @@
 package protobuf
 
 import (
-	"os"
+	"io/ioutil"
 	"strings"
 	"testing"
 
@@ -104,7 +104,7 @@ func TestTagGenerate(t *testing.T) {
 		},
 	}
 
-	in, err := os.ReadFile("./test_data/protobuf_tag_test.out")
+	in, err := ioutil.ReadFile("./test_data/protobuf_tag_test.out")
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/cmd/hz/thrift/plugin_test.go b/cmd/hz/thrift/plugin_test.go
index a4535f6..5e471b6 100644
--- a/cmd/hz/thrift/plugin_test.go
+++ b/cmd/hz/thrift/plugin_test.go
@@ -17,17 +17,16 @@
 package thrift
 
 import (
-	"os"
+	"io/ioutil"
 	"testing"
 
 	"github.com/cloudwego/hertz/cmd/hz/generator"
 	"github.com/cloudwego/hertz/cmd/hz/meta"
-	"github.com/cloudwego/hertz/cmd/hz/util"
 	"github.com/cloudwego/thriftgo/plugin"
 )
 
 func TestRun(t *testing.T) {
-	data, err := os.ReadFile("../testdata/request_thrift.out")
+	data, err := ioutil.ReadFile("../testdata/request_thrift.out")
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -54,7 +53,7 @@ func TestRun(t *testing.T) {
 	}
 
 	args := plu.args
-	cf, _ := util.GetColonPair(args.CustomizePackage)
+	customPackageTemplate := args.CustomizePackage
 	pkg, err := args.GetGoPackage()
 	if err != nil {
 		t.Fatal(err)
@@ -76,7 +75,7 @@ func TestRun(t *testing.T) {
 		t.Fatal(err)
 	}
 	sg := generator.HttpPackageGenerator{
-		ConfigPath: cf,
+		ConfigPath: customPackageTemplate,
 		HandlerDir: handlerDir,
 		RouterDir:  routerDir,
 		ModelDir:   modelDir,
diff --git a/cmd/hz/thrift/tag_test.go b/cmd/hz/thrift/tag_test.go
index 143fd8b..108dea8 100644
--- a/cmd/hz/thrift/tag_test.go
+++ b/cmd/hz/thrift/tag_test.go
@@ -17,7 +17,7 @@
 package thrift
 
 import (
-	"os"
+	"io/ioutil"
 	"strings"
 	"testing"
 
@@ -26,7 +26,7 @@ import (
 )
 
 func TestInsertTag(t *testing.T) {
-	data, err := os.ReadFile("./test_data/thrift_tag_test.out")
+	data, err := ioutil.ReadFile("./test_data/thrift_tag_test.out")
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/internal/bytestr/bytes.go b/internal/bytestr/bytes.go
index 85e488b..a93bd2d 100644
--- a/internal/bytestr/bytes.go
+++ b/internal/bytestr/bytes.go
@@ -42,6 +42,7 @@ var (
 	StrStar             = []byte("*")
 	StrColonSlashSlash  = []byte("://")
 	StrColonSpace       = []byte(": ")
+	StrCommaSpace       = []byte(", ")
 	StrAt               = []byte("@")
 	StrSD               = []byte("sd")
 
@@ -66,18 +67,25 @@ var (
 	StrServer           = []byte(consts.HeaderServer)
 	StrTransferEncoding = []byte(consts.HeaderTransferEncoding)
 
-	StrUserAgent       = []byte(consts.HeaderUserAgent)
-	StrCookie          = []byte(consts.HeaderCookie)
-	StrLocation        = []byte(consts.HeaderLocation)
-	StrContentRange    = []byte(consts.HeaderContentRange)
-	StrContentEncoding = []byte(consts.HeaderContentEncoding)
-	StrAcceptEncoding  = []byte(consts.HeaderAcceptEncoding)
-	StrSetCookie       = []byte(consts.HeaderSetCookie)
-	StrAuthorization   = []byte(consts.HeaderAuthorization)
-	StrRange           = []byte(consts.HeaderRange)
-	StrLastModified    = []byte(consts.HeaderLastModified)
-	StrAcceptRanges    = []byte(consts.HeaderAcceptRanges)
-	StrIfModifiedSince = []byte(consts.HeaderIfModifiedSince)
+	StrUserAgent          = []byte(consts.HeaderUserAgent)
+	StrCookie             = []byte(consts.HeaderCookie)
+	StrLocation           = []byte(consts.HeaderLocation)
+	StrContentRange       = []byte(consts.HeaderContentRange)
+	StrContentEncoding    = []byte(consts.HeaderContentEncoding)
+	StrAcceptEncoding     = []byte(consts.HeaderAcceptEncoding)
+	StrSetCookie          = []byte(consts.HeaderSetCookie)
+	StrAuthorization      = []byte(consts.HeaderAuthorization)
+	StrRange              = []byte(consts.HeaderRange)
+	StrLastModified       = []byte(consts.HeaderLastModified)
+	StrAcceptRanges       = []byte(consts.HeaderAcceptRanges)
+	StrIfModifiedSince    = []byte(consts.HeaderIfModifiedSince)
+	StrTE                 = []byte(consts.HeaderTE)
+	StrTrailer            = []byte(consts.HeaderTrailer)
+	StrMaxForwards        = []byte(consts.HeaderMaxForwards)
+	StrProxyConnection    = []byte(consts.HeaderProxyConnection)
+	StrProxyAuthenticate  = []byte(consts.HeaderProxyAuthenticate)
+	StrProxyAuthorization = []byte(consts.HeaderProxyAuthorization)
+	StrWWWAuthenticate    = []byte(consts.HeaderWWWAuthenticate)
 
 	StrCookieExpires        = []byte("expires")
 	StrCookieDomain         = []byte("domain")
diff --git a/pkg/app/client/client_test.go b/pkg/app/client/client_test.go
index 23b1eea..1754044 100644
--- a/pkg/app/client/client_test.go
+++ b/pkg/app/client/client_test.go
@@ -48,6 +48,7 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"io/ioutil"
 	"net"
 	"net/http"
 	"net/http/httptest"
@@ -1501,7 +1502,7 @@ func TestClientReadResponseBodyStream(t *testing.T) {
 	if string(p) != part1 {
 		t.Errorf("read len=%v, read content=%v; want len=%v, want content=%v", r, string(p), len(part1), part1)
 	}
-	left, _ := io.ReadAll(bodyStream)
+	left, _ := ioutil.ReadAll(bodyStream)
 	if string(left) != part2 {
 		t.Errorf("left len=%v, left content=%v; want len=%v, want content=%v", len(left), string(left), len(part2), part2)
 	}
@@ -1868,13 +1869,13 @@ func TestClientReadResponseBodyStreamWithDoubleRequest(t *testing.T) {
 	if bodyStream1 == nil {
 		t.Errorf("bodystream1 is nil")
 	}
-	data, _ := io.ReadAll(bodyStream1)
+	data, _ := ioutil.ReadAll(bodyStream1)
 	if string(data) != part1+part2 {
 		t.Errorf("read len=%v, read content=%v; want len=%v, want content=%v", len(data), data, len(part1+part2), part1+part2)
 	}
 
 	// read left bodystream
-	left, _ := io.ReadAll(bodyStream)
+	left, _ := ioutil.ReadAll(bodyStream)
 	if string(left) != part2 {
 		t.Errorf("left len=%v, left content=%v; want len=%v, want content=%v", len(left), string(left), len(part2), part2)
 	}
diff --git a/pkg/app/context_test.go b/pkg/app/context_test.go
index fc3c147..31d6da0 100644
--- a/pkg/app/context_test.go
+++ b/pkg/app/context_test.go
@@ -20,7 +20,7 @@ import (
 	"context"
 	"errors"
 	"fmt"
-	"io"
+	"io/ioutil"
 	"reflect"
 	"strings"
 	"testing"
@@ -401,7 +401,7 @@ tailfoobar`
 	if err := req.Read(&ctx.Request, mr); err != nil {
 		t.Fatalf("unexpected error: %s", err)
 	}
-	tail, err := io.ReadAll(mr)
+	tail, err := ioutil.ReadAll(mr)
 	if err != nil {
 		t.Fatalf("unexpected error: %s", err)
 	}
@@ -673,18 +673,48 @@ func TestContextContentType(t *testing.T) {
 func TestClientIp(t *testing.T) {
 	c := NewContext(0)
 	c.conn = mock.NewConn("")
-	// Case 1
-	c.Request.Header.Set("X-Forwarded-For", "126.0.0.2")
+	// 0.0.0.0 simulates a trusted proxy server
+	c.Request.Header.Set("X-Forwarded-For", "  126.0.0.2, 0.0.0.0 ")
 	val := c.ClientIP()
 	if val != "126.0.0.2" {
 		t.Fatalf("unexpected %v. Expecting %v", val, "126.0.0.2")
 	}
-	// Case 2
+	// no proxy server
+	c = NewContext(0)
+	c.conn = mock.NewConn("")
 	c.Request.Header.Set("X-Real-Ip", "126.0.0.1")
 	val = c.ClientIP()
 	if val != "126.0.0.1" {
 		t.Fatalf("unexpected %v. Expecting %v", val, "126.0.0.1")
 	}
+	// custom RemoteIPHeaders and TrustedProxies
+	opts := ClientIPOptions{
+		RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
+		TrustedProxies: map[string]bool{
+			"0.0.0.0": true,
+		},
+	}
+	c = NewContext(0)
+	c.SetClientIPFunc(ClientIPWithOption(opts))
+	c.conn = mock.NewConn("")
+	c.Request.Header.Set("X-Forwarded-For", "  126.0.0.2, 0.0.0.0 ")
+	val = c.ClientIP()
+	if val != "126.0.0.2" {
+		t.Fatalf("unexpected %v. Expecting %v", val, "126.0.0.2")
+	}
+	// no trusted proxy server
+	opts = ClientIPOptions{
+		RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
+		TrustedProxies:  nil,
+	}
+	c = NewContext(0)
+	c.SetClientIPFunc(ClientIPWithOption(opts))
+	c.conn = mock.NewConn("")
+	c.Request.Header.Set("X-Forwarded-For", "  126.0.0.2, 0.0.0.0 ")
+	val = c.ClientIP()
+	if val != "0.0.0.0" {
+		t.Fatalf("unexpected %v. Expecting %v", val, "0.0.0.0")
+	}
 }
 
 func TestSetClientIPFunc(t *testing.T) {
diff --git a/pkg/app/fs_test.go b/pkg/app/fs_test.go
index 36cc0d4..389fe9e 100644
--- a/pkg/app/fs_test.go
+++ b/pkg/app/fs_test.go
@@ -46,6 +46,7 @@ import (
 	"context"
 	"fmt"
 	"io"
+	"io/ioutil"
 	"math/rand"
 	"os"
 	"path"
@@ -184,13 +185,13 @@ func TestServeFileSmallNoReadFrom(t *testing.T) {
 
 	teststr := "hello, world!"
 
-	tempdir, err := os.MkdirTemp("", "httpexpect")
+	tempdir, err := ioutil.TempDir("", "httpexpect")
 	if err != nil {
 		t.Fatal(err)
 	}
 	defer os.RemoveAll(tempdir)
 
-	if err := os.WriteFile(
+	if err := ioutil.WriteFile(
 		path.Join(tempdir, "hello"), []byte(teststr), 0o666); err != nil {
 		t.Fatal(err)
 	}
@@ -398,7 +399,7 @@ func getFileContents(path string) ([]byte, error) {
 		return nil, err
 	}
 	defer f.Close()
-	return io.ReadAll(f)
+	return ioutil.ReadAll(f)
 }
 
 func TestParseByteRangeSuccess(t *testing.T) {
diff --git a/pkg/app/server/hertz_test.go b/pkg/app/server/hertz_test.go
index 9df769a..aef7824 100644
--- a/pkg/app/server/hertz_test.go
+++ b/pkg/app/server/hertz_test.go
@@ -23,6 +23,7 @@ import (
 	"fmt"
 	"html/template"
 	"io"
+	"io/ioutil"
 	"net"
 	"net/http"
 	"strings"
@@ -335,7 +336,7 @@ func TestNotEnoughBodySize(t *testing.T) {
 	resp, err := http.Post("http://127.0.0.1:8889/test", "application/x-www-form-urlencoded", body)
 	assert.Nil(t, err)
 	assert.DeepEqual(t, 413, resp.StatusCode)
-	bodyBytes, _ := io.ReadAll(resp.Body)
+	bodyBytes, _ := ioutil.ReadAll(resp.Body)
 	assert.DeepEqual(t, "Request Entity Too Large", string(bodyBytes))
 }
 
diff --git a/pkg/app/server/render/html_test.go b/pkg/app/server/render/html_test.go
index aca0966..fb94458 100644
--- a/pkg/app/server/render/html_test.go
+++ b/pkg/app/server/render/html_test.go
@@ -17,6 +17,7 @@
 package render
 
 import (
+	"io/ioutil"
 	"os"
 	"testing"
 	"time"
@@ -38,7 +39,7 @@ func TestHTMLDebug_StartChecker_timer(t *testing.T) {
 }
 
 func TestHTMLDebug_StartChecker_fs_watcher(t *testing.T) {
-	f, _ := os.CreateTemp("./", "test.tmpl")
+	f, _ := ioutil.TempFile("./", "test.tmpl")
 	defer func() {
 		f.Close()
 		os.Remove(f.Name())
diff --git a/pkg/common/adaptor/request_test.go b/pkg/common/adaptor/request_test.go
index 4aff35d..ba3ec6a 100644
--- a/pkg/common/adaptor/request_test.go
+++ b/pkg/common/adaptor/request_test.go
@@ -18,7 +18,7 @@ package adaptor
 
 import (
 	"context"
-	"io"
+	"io/ioutil"
 	"net/http"
 	"net/url"
 	"strings"
@@ -87,7 +87,7 @@ func makeACall(t *testing.T, method, url string, header http.Header, body string
 		}
 	}
 
-	b, err := io.ReadAll(resp.Body)
+	b, err := ioutil.ReadAll(resp.Body)
 	if err != nil {
 		t.Fatalf("Read body error: %s", err)
 	}
@@ -116,7 +116,7 @@ func handlerAndCheck(t *testing.T, writer http.ResponseWriter, request *http.Req
 		}
 	}
 
-	body, err := io.ReadAll(request.Body)
+	body, err := ioutil.ReadAll(request.Body)
 	if err != nil {
 		t.Fatalf("Read body error: %s", err)
 	}
diff --git a/pkg/common/stackless/writer_test.go b/pkg/common/stackless/writer_test.go
index 73347cb..cae68bf 100644
--- a/pkg/common/stackless/writer_test.go
+++ b/pkg/common/stackless/writer_test.go
@@ -47,6 +47,7 @@ import (
 	"compress/gzip"
 	"fmt"
 	"io"
+	"io/ioutil"
 	"testing"
 	"time"
 )
@@ -128,7 +129,7 @@ func testWriterReuse(w Writer, r io.Reader, newReader func(io.Reader) io.Reader)
 	w.Close()
 
 	zr := newReader(r)
-	data, err := io.ReadAll(zr)
+	data, err := ioutil.ReadAll(zr)
 	if err != nil {
 		return fmt.Errorf("unexpected error: %s, data=%q", err, data)
 	}
diff --git a/pkg/common/test/assert/assert.go b/pkg/common/test/assert/assert.go
index 44ed32e..4d52201 100644
--- a/pkg/common/test/assert/assert.go
+++ b/pkg/common/test/assert/assert.go
@@ -17,8 +17,6 @@
 package assert
 
 import (
-	"bytes"
-	"io"
 	"reflect"
 	"testing"
 )
@@ -114,13 +112,3 @@ func NotPanic(t testing.TB, fn func()) {
 	}()
 	fn()
 }
-
-func VerifyTrailer(t *testing.T, r io.Reader, expectedTrailer string) {
-	trailer, err := io.ReadAll(r)
-	if err != nil {
-		t.Fatalf("Cannot read trailer: %s", err)
-	}
-	if !bytes.Equal(trailer, []byte(expectedTrailer)) {
-		t.Fatalf("Unexpected trailer %q. Expected %q", trailer, expectedTrailer)
-	}
-}
diff --git a/pkg/common/test/mock/network.go b/pkg/common/test/mock/network.go
index bb08cb4..22c8266 100644
--- a/pkg/common/test/mock/network.go
+++ b/pkg/common/test/mock/network.go
@@ -32,6 +32,12 @@ type Conn struct {
 	readTimeout time.Duration
 	zr          network.Reader
 	zw          network.ReadWriter
+	wroteLen    int
+}
+
+type Recorder interface {
+	network.Reader
+	WroteLen() int
 }
 
 func (m *Conn) SetWriteTimeout(t time.Duration) error {
@@ -98,19 +104,31 @@ func (m *Conn) Len() int {
 }
 
 func (m *Conn) Malloc(n int) (buf []byte, err error) {
+	m.wroteLen += n
 	return m.zw.Malloc(n)
 }
 
 func (m *Conn) WriteBinary(b []byte) (n int, err error) {
-	return m.zw.WriteBinary(b)
+	n, err = m.zw.WriteBinary(b)
+	m.wroteLen += n
+	return n, err
 }
 
 func (m *Conn) Flush() error {
 	return m.zw.Flush()
 }
 
-func (m *Conn) WriterRecorder() network.Reader {
-	return m.zw
+func (m *Conn) WriterRecorder() Recorder {
+	return &recorder{c: m, Reader: m.zw}
+}
+
+type recorder struct {
+	c *Conn
+	network.Reader
+}
+
+func (r *recorder) WroteLen() int {
+	return r.c.wroteLen
 }
 
 func (m *SlowReadConn) Peek(i int) ([]byte, error) {
diff --git a/pkg/common/test/mock/writer.go b/pkg/common/test/mock/writer.go
new file mode 100644
index 0000000..b326242
--- /dev/null
+++ b/pkg/common/test/mock/writer.go
@@ -0,0 +1,47 @@
+/*
+ * 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 mock
+
+import "bytes"
+
+type ExtWriter struct {
+	tmp     []byte
+	Buf     *bytes.Buffer
+	IsFinal *bool
+}
+
+func (m *ExtWriter) Write(p []byte) (n int, err error) {
+	m.tmp = p
+	return len(p), nil
+}
+
+func (m *ExtWriter) Flush() error {
+	_, err := m.Buf.Write(m.tmp)
+	return err
+}
+
+func (m *ExtWriter) Finalize() error {
+	if !*m.IsFinal {
+		*m.IsFinal = true
+	}
+	return nil
+}
+
+func (m *ExtWriter) SetBody(body []byte) {
+	m.Buf.Reset()
+	m.tmp = body
+}
diff --git a/pkg/protocol/header_test.go b/pkg/protocol/header_test.go
index fc58a29..a48e663 100644
--- a/pkg/protocol/header_test.go
+++ b/pkg/protocol/header_test.go
@@ -134,6 +134,7 @@ func TestSetCanonical(t *testing.T) {
 	h.SetCanonical([]byte(consts.HeaderContentLength), []byte("3"))
 	h.SetCanonical([]byte(consts.HeaderConnection), []byte("foo4"))
 	h.SetCanonical([]byte(consts.HeaderTransferEncoding), []byte("foo5"))
+	h.SetCanonical([]byte(consts.HeaderTrailer), []byte("foo7"))
 	h.SetCanonical([]byte("bar"), []byte("foo6"))
 
 	assert.DeepEqual(t, []byte("foo"), h.ContentType())
@@ -142,6 +143,7 @@ func TestSetCanonical(t *testing.T) {
 	assert.DeepEqual(t, 3, h.ContentLength())
 	assert.DeepEqual(t, false, h.ConnectionClose())
 	assert.DeepEqual(t, false, strings.Contains(string(h.ContentType()), "foo5"))
+	assert.DeepEqual(t, true, strings.Contains(string(h.Header()), "Trailer: Foo7"))
 	assert.DeepEqual(t, true, strings.Contains(string(h.Header()), "bar: foo6"))
 }
 
@@ -211,6 +213,7 @@ func TestRequestHeaderDel(t *testing.T) {
 	h.Set(consts.HeaderContentType, "aaa")
 	h.Set(consts.HeaderServer, "aaabbb")
 	h.Set(consts.HeaderContentLength, "1123")
+	h.Set(consts.HeaderTrailer, "foo, bar")
 	h.SetHost("foobar")
 	h.SetCookie("foo", "bar")
 
@@ -221,6 +224,7 @@ func TestRequestHeaderDel(t *testing.T) {
 	h.del([]byte("Content-Length"))
 	h.del([]byte("Set-Cookie"))
 	h.del([]byte("Host"))
+	h.del([]byte(consts.HeaderTrailer))
 	h.DelCookie("foo")
 
 	hv := h.Peek("aaa")
@@ -255,6 +259,10 @@ func TestRequestHeaderDel(t *testing.T) {
 	if len(hv) > 0 {
 		t.Fatalf("non-zero value: %q", hv)
 	}
+	hv = h.Peek(consts.HeaderTrailer)
+	if len(hv) > 0 {
+		t.Fatalf("non-zero value: %q", hv)
+	}
 	if h.ContentLength() != 0 {
 		t.Fatalf("unexpected content-length: %d. Expecting 0", h.ContentLength())
 	}
@@ -271,6 +279,7 @@ func TestResponseHeaderDel(t *testing.T) {
 	h.Set(consts.HeaderContentEncoding, "gzip")
 	h.Set(consts.HeaderServer, "aaabbb")
 	h.Set(consts.HeaderContentLength, "1123")
+	h.Set(consts.HeaderTrailer, "foo, bar")
 
 	var c Cookie
 	c.SetKey("foo")
@@ -284,6 +293,7 @@ func TestResponseHeaderDel(t *testing.T) {
 	h.Del("content-length")
 	h.Del("set-cookie")
 	h.Del("content-encoding")
+	h.Del(consts.HeaderTrailer)
 
 	hv := h.Peek("aaa")
 	if string(hv) != "bbb" {
@@ -314,6 +324,11 @@ func TestResponseHeaderDel(t *testing.T) {
 		t.Fatalf("non-zero value: %q", hv)
 	}
 
+	hv = h.Peek(consts.HeaderTrailer)
+	if len(hv) > 0 {
+		t.Fatalf("non-zero value: %q", hv)
+	}
+
 	if h.Cookie(&c) {
 		t.Fatalf("unexpected cookie obtained: %v", &c)
 	}
diff --git a/pkg/protocol/http1/client_test.go b/pkg/protocol/http1/client_test.go
index d8d915c..1e4e013 100644
--- a/pkg/protocol/http1/client_test.go
+++ b/pkg/protocol/http1/client_test.go
@@ -47,7 +47,7 @@ import (
 	"crypto/tls"
 	"errors"
 	"fmt"
-	"io"
+	"io/ioutil"
 	"net"
 	"strings"
 	"sync"
@@ -56,9 +56,8 @@ import (
 	"time"
 
 	"github.com/cloudwego/hertz/pkg/common/config"
-	"github.com/cloudwego/hertz/pkg/common/test/assert"
-
 	errs "github.com/cloudwego/hertz/pkg/common/errors"
+	"github.com/cloudwego/hertz/pkg/common/test/assert"
 	"github.com/cloudwego/hertz/pkg/common/test/mock"
 	"github.com/cloudwego/hertz/pkg/network"
 	"github.com/cloudwego/hertz/pkg/protocol"
@@ -187,7 +186,7 @@ func testContinueReadResponseBodyStream(t *testing.T, header, body string, maxBo
 		t.Fatalf("should read %d from stream body, but got %d", firstRead, sR)
 	}
 
-	leftB, _ := io.ReadAll(r.BodyStream())
+	leftB, _ := ioutil.ReadAll(r.BodyStream())
 	if len(leftB) != leftBytes {
 		t.Fatalf("should left %d bytes from stream body, but left %d", leftBytes, len(leftB))
 	}
diff --git a/pkg/protocol/http1/client_unix_test.go b/pkg/protocol/http1/client_unix_test.go
new file mode 100644
index 0000000..ff65bc4
--- /dev/null
+++ b/pkg/protocol/http1/client_unix_test.go
@@ -0,0 +1,63 @@
+// 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.
+//
+//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
+// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris
+
+package http1
+
+import (
+	"context"
+	"net/http"
+	"runtime"
+	"testing"
+	"time"
+
+	"github.com/cloudwego/hertz/pkg/common/test/assert"
+	"github.com/cloudwego/hertz/pkg/network/netpoll"
+	"github.com/cloudwego/hertz/pkg/protocol"
+	"github.com/cloudwego/hertz/pkg/protocol/consts"
+)
+
+func TestGcBodyStream(t *testing.T) {
+	srv := &http.Server{Addr: ":11001", Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+		w.Write([]byte("hello\n"))
+		time.Sleep(time.Second)
+		w.Write([]byte("world\n"))
+	})}
+	go srv.ListenAndServe()
+	time.Sleep(100 * time.Millisecond)
+
+	c := &HostClient{
+		ClientOptions: &ClientOptions{
+			Dialer:             netpoll.NewDialer(),
+			ResponseBodyStream: true,
+		},
+		Addr: "127.0.0.1:11001",
+	}
+
+	for i := 0; i < 10; i++ {
+		req, resp := protocol.AcquireRequest(), protocol.AcquireResponse()
+		req.SetRequestURI("http://127.0.0.1:11001")
+		req.SetMethod(consts.MethodPost)
+		err := c.Do(context.Background(), req, resp)
+		if err != nil {
+			t.Errorf("client Do error=%v", err.Error())
+		}
+	}
+
+	runtime.GC()
+	c.CloseIdleConnections()
+	assert.DeepEqual(t, 0, c.ConnPoolState().TotalConnNum)
+}
diff --git a/pkg/protocol/http1/ext/common_test.go b/pkg/protocol/http1/ext/common_test.go
index 0f3fda6..5ad9c00 100644
--- a/pkg/protocol/http1/ext/common_test.go
+++ b/pkg/protocol/http1/ext/common_test.go
@@ -17,10 +17,13 @@
 package ext
 
 import (
+	"bytes"
 	"strings"
 	"testing"
 
 	"github.com/cloudwego/hertz/pkg/common/test/assert"
+	"github.com/cloudwego/hertz/pkg/common/test/mock"
+	"github.com/cloudwego/hertz/pkg/protocol"
 )
 
 func Test_stripSpace(t *testing.T) {
@@ -43,3 +46,52 @@ func Test_isOnlyCRLF(t *testing.T) {
 	assert.True(t, isOnlyCRLF([]byte("\r\n")))
 	assert.True(t, isOnlyCRLF([]byte("\n")))
 }
+
+func TestReadTrailer(t *testing.T) {
+	exceptedTrailers := map[string]string{"Hertz": "test"}
+	zr := mock.NewZeroCopyReader("0\r\nHertz: test\r\n\r\n")
+	trailer := protocol.Trailer{}
+	keys := make([]string, 0, len(exceptedTrailers))
+	for k := range exceptedTrailers {
+		keys = append(keys, k)
+	}
+	trailer.SetTrailers([]byte(strings.Join(keys, ", ")))
+	err := ReadTrailer(&trailer, zr)
+	if err != nil {
+		t.Fatalf("Cannot read trailer: %v", err)
+	}
+
+	for k, v := range exceptedTrailers {
+		got := trailer.Peek(k)
+		if !bytes.Equal(got, []byte(v)) {
+			t.Fatalf("Unexpected trailer %q. Expected %q. Got %q", k, v, got)
+		}
+	}
+}
+
+func TestReadTrailerError(t *testing.T) {
+	// with bad trailer
+	zr := mock.NewZeroCopyReader("0\r\nHertz: test\r\nContent-Type: aaa\r\n\r\n")
+	trailer := protocol.Trailer{}
+	err := ReadTrailer(&trailer, zr)
+	if err == nil {
+		t.Fatalf("expecting error.")
+	}
+}
+
+func TestReadTrailer1(t *testing.T) {
+	exceptedTrailers := map[string]string{}
+	zr := mock.NewZeroCopyReader("0\r\n\r\n")
+	trailer := protocol.Trailer{}
+	err := ReadTrailer(&trailer, zr)
+	if err != nil {
+		t.Fatalf("Cannot read trailer: %v", err)
+	}
+
+	for k, v := range exceptedTrailers {
+		got := trailer.Peek(k)
+		if !bytes.Equal(got, []byte(v)) {
+			t.Fatalf("Unexpected trailer %q. Expected %q. Got %q", k, v, got)
+		}
+	}
+}
diff --git a/pkg/protocol/http1/ext/stream_test.go b/pkg/protocol/http1/ext/stream_test.go
new file mode 100644
index 0000000..57cad37
--- /dev/null
+++ b/pkg/protocol/http1/ext/stream_test.go
@@ -0,0 +1,90 @@
+/*
+ * 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 ext
+
+import (
+	"fmt"
+	"io"
+	"testing"
+
+	"github.com/cloudwego/hertz/pkg/common/bytebufferpool"
+	"github.com/cloudwego/hertz/pkg/common/test/assert"
+	"github.com/cloudwego/hertz/pkg/common/test/mock"
+	"github.com/cloudwego/hertz/pkg/protocol"
+)
+
+func createChunkedBody(body, rest []byte, trailer map[string]string, hasTrailer bool) []byte {
+	var b []byte
+	chunkSize := 1
+	for len(body) > 0 {
+		if chunkSize > len(body) {
+			chunkSize = len(body)
+		}
+		b = append(b, []byte(fmt.Sprintf("%x\r\n", chunkSize))...)
+		b = append(b, body[:chunkSize]...)
+		b = append(b, []byte("\r\n")...)
+		body = body[chunkSize:]
+		chunkSize++
+	}
+	if hasTrailer {
+		b = append(b, "0\r\n"...)
+		for k, v := range trailer {
+			b = append(b, k...)
+			b = append(b, ": "...)
+			b = append(b, v...)
+			b = append(b, "\r\n"...)
+		}
+		b = append(b, "\r\n"...)
+	}
+	return append(b, rest...)
+}
+
+func testChunkedSkipRest(t *testing.T, data, rest string) {
+	var pool bytebufferpool.Pool
+	reader := mock.NewZeroCopyReader(data)
+
+	bs := AcquireBodyStream(pool.Get(), reader, &protocol.Trailer{}, -1)
+	err := bs.(*bodyStream).skipRest()
+	assert.Nil(t, err)
+
+	rest_data, err := io.ReadAll(reader)
+	assert.Nil(t, err)
+	assert.DeepEqual(t, rest, string(rest_data))
+}
+
+func testChunkedSkipRestWithBodySize(t *testing.T, bodySize int) {
+	body := mock.CreateFixedBody(bodySize)
+	rest := mock.CreateFixedBody(bodySize)
+	data := createChunkedBody(body, rest, map[string]string{"foo": "bar"}, true)
+
+	testChunkedSkipRest(t, string(data), string(rest))
+}
+
+func TestChunkedSkipRest(t *testing.T) {
+	t.Parallel()
+
+	testChunkedSkipRest(t, "0\r\n\r\n", "")
+	testChunkedSkipRest(t, "0\r\n\r\nHTTP/1.1 / POST", "HTTP/1.1 / POST")
+	testChunkedSkipRest(t, "0\r\nHertz: test\r\nfoo: bar\r\n\r\nHTTP/1.1 / POST", "HTTP/1.1 / POST")
+
+	testChunkedSkipRestWithBodySize(t, 5)
+
+	// medium-size body
+	testChunkedSkipRestWithBodySize(t, 43488)
+
+	// big body
+	testChunkedSkipRestWithBodySize(t, 3*1024*1024)
+}
diff --git a/pkg/protocol/http1/req/request_test.go b/pkg/protocol/http1/req/request_test.go
index 01e3f17..a0a29de 100644
--- a/pkg/protocol/http1/req/request_test.go
+++ b/pkg/protocol/http1/req/request_test.go
@@ -47,6 +47,7 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"io/ioutil"
 	"mime/multipart"
 	"net/url"
 	"strings"
@@ -58,6 +59,8 @@ import (
 	errs "github.com/cloudwego/hertz/pkg/common/errors"
 	"github.com/cloudwego/hertz/pkg/common/test/assert"
 	"github.com/cloudwego/hertz/pkg/common/test/mock"
+	"github.com/cloudwego/hertz/pkg/common/utils"
+	"github.com/cloudwego/hertz/pkg/network"
 	"github.com/cloudwego/hertz/pkg/protocol"
 	"github.com/cloudwego/hertz/pkg/protocol/consts"
 	"github.com/cloudwego/hertz/pkg/protocol/http1/ext"
@@ -397,7 +400,8 @@ func TestChunkedUnexpectedEOF(t *testing.T) {
 	}
 
 	var pool bytebufferpool.Pool
-	bs := ext.AcquireBodyStream(pool.Get(), reader, -1)
+	var req1 protocol.Request
+	bs := ext.AcquireBodyStream(pool.Get(), reader, req1.Header.Trailer(), -1)
 	byteSlice := make([]byte, 4096)
 	_, err = bs.Read(byteSlice)
 	if err != io.ErrUnexpectedEOF {
@@ -483,21 +487,21 @@ func TestRequestWriteRequestURINoHost(t *testing.T) {
 func TestSetRequestBodyStreamChunked(t *testing.T) {
 	t.Parallel()
 
-	testSetRequestBodyStream(t, "", true)
+	testSetRequestBodyStreamChunked(t, "", map[string]string{"Foo": "bar"})
 
 	body := "foobar baz aaa bbb ccc"
-	testSetRequestBodyStream(t, body, true)
+	testSetRequestBodyStreamChunked(t, body, nil)
 
 	body = string(mock.CreateFixedBody(10001))
-	testSetRequestBodyStream(t, body, true)
+	testSetRequestBodyStreamChunked(t, body, map[string]string{"Foo": "test", "Bar": "test"})
 }
 
 func TestSetRequestBodyStreamFixedSize(t *testing.T) {
 	t.Parallel()
 
-	testSetRequestBodyStream(t, "a", false)
-	testSetRequestBodyStream(t, string(mock.CreateFixedBody(4097)), false)
-	testSetRequestBodyStream(t, string(mock.CreateFixedBody(100500)), false)
+	testSetRequestBodyStream(t, "a")
+	testSetRequestBodyStream(t, string(mock.CreateFixedBody(4097)))
+	testSetRequestBodyStream(t, string(mock.CreateFixedBody(100500)))
 }
 
 func TestRequestHostFromRequestURI(t *testing.T) {
@@ -656,7 +660,7 @@ func TestRequestReadPostNoBody(t *testing.T) {
 		t.Fatalf("unexpected content-length: %d. Expecting 0", r.Header.ContentLength())
 	}
 
-	tail, err := io.ReadAll(zr)
+	tail, err := ioutil.ReadAll(zr)
 	if err != nil {
 		t.Fatalf("unexpected error: %s", err)
 	}
@@ -748,8 +752,8 @@ func TestRequestReadChunked(t *testing.T) {
 
 	var req protocol.Request
 
-	s := "POST /foo HTTP/1.1\r\nHost: google.com\r\nTransfer-Encoding: chunked\r\nContent-Type: aa/bb\r\n\r\n3\r\nabc\r\n5\r\n12345\r\n0\r\n\r\ntrail"
-	zr := mock.NewZeroCopyReader(s)
+	s := "POST /foo HTTP/1.1\r\nHost: google.com\r\nTransfer-Encoding: chunked\r\nContent-Type: aa/bb\r\n\r\n3\r\nabc\r\n5\r\n12345\r\n0\r\n\r\nTrail: test\r\n\r\n"
+	zr := netpoll.NewReader(bytes.NewBufferString(s))
 	err := Read(&req, zr)
 	if err != nil {
 		t.Fatalf("Unexpected error when reading chunked request: %s", err)
@@ -759,7 +763,30 @@ func TestRequestReadChunked(t *testing.T) {
 		t.Fatalf("Unexpected body %q. Expected %q", req.Body(), expectedBody)
 	}
 	verifyRequestHeader(t, &req.Header, 8, "/foo", "google.com", "", "aa/bb")
-	assert.VerifyTrailer(t, zr, "trail")
+	verifyTrailer(t, zr, map[string]string{"Trail": "test"})
+}
+
+func verifyTrailer(t *testing.T, r network.Reader, exceptedTrailers map[string]string) {
+	trailer := protocol.Trailer{}
+	keys := make([]string, 0, len(exceptedTrailers))
+	for k := range exceptedTrailers {
+		keys = append(keys, k)
+	}
+	trailer.SetTrailers([]byte(strings.Join(keys, ", ")))
+	err := ext.ReadTrailer(&trailer, r)
+	if err == io.EOF && exceptedTrailers == nil {
+		return
+	}
+	if err != nil {
+		t.Fatalf("Cannot read trailer: %v", err)
+	}
+
+	for k, v := range exceptedTrailers {
+		got := trailer.Peek(k)
+		if !bytes.Equal(got, []byte(v)) {
+			t.Fatalf("Unexpected trailer %q. Expected %q. Got %q", k, v, got)
+		}
+	}
 }
 
 func TestRequestChunkedWhitespace(t *testing.T) {
@@ -801,15 +828,12 @@ func testRequestReadLimitBodySuccess(t *testing.T, s string, maxBodySize int) {
 	}
 }
 
-func testSetRequestBodyStream(t *testing.T, body string, chunked bool) {
+func testSetRequestBodyStream(t *testing.T, body string) {
 	var req protocol.Request
 	req.Header.SetHost("foobar.com")
 	req.Header.SetMethod(consts.MethodPost)
 
 	bodySize := len(body)
-	if chunked {
-		bodySize = -1
-	}
 	if req.IsBodyStream() {
 		t.Fatalf("IsBodyStream must return false")
 	}
@@ -841,6 +865,51 @@ func testSetRequestBodyStream(t *testing.T, body string, chunked bool) {
 	}
 }
 
+func testSetRequestBodyStreamChunked(t *testing.T, body string, trailer map[string]string) {
+	var req protocol.Request
+	req.Header.SetHost("foobar.com")
+	req.Header.SetMethod(consts.MethodPost)
+
+	if req.IsBodyStream() {
+		t.Fatalf("IsBodyStream must return false")
+	}
+	req.SetBodyStream(bytes.NewBufferString(body), -1)
+	if !req.IsBodyStream() {
+		t.Fatalf("IsBodyStream must return true")
+	}
+
+	var w bytes.Buffer
+	zw := netpoll.NewWriter(&w)
+	for k, v := range trailer {
+		err := req.Header.Trailer().Add(k, v)
+		if err != nil {
+			t.Fatalf("unexpected error: %v", err)
+		}
+	}
+	if err := Write(&req, zw); err != nil {
+		t.Fatalf("unexpected error when writing request: %v, body=%q", err, body)
+	}
+	if err := zw.Flush(); err != nil {
+		t.Fatalf("unexpected error when flushing request: %v, body=%q", err, body)
+	}
+
+	var req1 protocol.Request
+	br := bufio.NewReader(&w)
+	zr := netpoll.NewReader(br)
+	if err := Read(&req1, zr); err != nil {
+		t.Fatalf("unexpected error when reading request: %v. body=%q", err, body)
+	}
+	if string(req1.Body()) != body {
+		t.Fatalf("unexpected body %q. Expecting %q", req1.Body(), body)
+	}
+	for k, v := range trailer {
+		r := req.Header.Trailer().Peek(k)
+		if string(r) != v {
+			t.Fatalf("unexpected trailer %q. Expecting %q. Got %q", k, v, r)
+		}
+	}
+}
+
 func TestRequestMultipartForm(t *testing.T) {
 	t.Parallel()
 
@@ -967,9 +1036,8 @@ func testRequestMultipartFormNotPreParse(t *testing.T, boundary string, formData
 
 func testReadBodyChunked(t *testing.T, bodySize int) {
 	body := mock.CreateFixedBody(bodySize)
-	chunkedBody := createChunkedBody(body)
-	expectedTrailer := []byte("chunked shit")
-	chunkedBody = append(chunkedBody, expectedTrailer...)
+	expectedTrailer := map[string]string{"Foo": "chunked shit"}
+	chunkedBody := createChunkedBody(body, expectedTrailer, true)
 
 	zr := mock.NewZeroCopyReader(string(chunkedBody))
 
@@ -981,10 +1049,10 @@ func testReadBodyChunked(t *testing.T, bodySize int) {
 	if !bytes.Equal(b, body) {
 		t.Fatalf("Unexpected response read for bodySize=%d: %q. Expected %q. chunkedBody=%q", bodySize, b, body, chunkedBody)
 	}
-	assert.VerifyTrailer(t, zr, string(expectedTrailer))
+	verifyTrailer(t, zr, expectedTrailer)
 }
 
-func createChunkedBody(body []byte) []byte {
+func createChunkedBody(body []byte, trailer map[string]string, hasTrailer bool) []byte {
 	var b []byte
 	chunkSize := 1
 	for len(body) > 0 {
@@ -997,15 +1065,23 @@ func createChunkedBody(body []byte) []byte {
 		body = body[chunkSize:]
 		chunkSize++
 	}
-	return append(b, []byte("0\r\n\r\n")...)
+	if hasTrailer {
+		b = append(b, "0\r\n"...)
+		for k, v := range trailer {
+			b = append(b, k...)
+			b = append(b, ": "...)
+			b = append(b, v...)
+			b = append(b, "\r\n"...)
+		}
+		b = append(b, "\r\n"...)
+	}
+	return b
 }
 
 func testReadBodyFixedSize(t *testing.T, bodySize int) {
 	body := mock.CreateFixedBody(bodySize)
-	expectedTrailer := []byte("traler aaaa")
-	bodyWithTrailer := append(body, expectedTrailer...)
 
-	zr := mock.NewZeroCopyReader(string(bodyWithTrailer))
+	zr := mock.NewZeroCopyReader(string(body))
 	b, err := ext.ReadBody(zr, bodySize, 0, nil)
 	if err != nil {
 		t.Fatalf("Unexpected error in ReadResponseBody(%d): %s", bodySize, err)
@@ -1013,7 +1089,7 @@ func testReadBodyFixedSize(t *testing.T, bodySize int) {
 	if !bytes.Equal(b, body) {
 		t.Fatalf("Unexpected response read for bodySize=%d: %q. Expected %q", bodySize, b, body)
 	}
-	assert.VerifyTrailer(t, zr, string(expectedTrailer))
+	verifyTrailer(t, zr, nil)
 }
 
 func TestRequestFormFile(t *testing.T) {
@@ -1048,7 +1124,7 @@ tailfoobar`
 		t.Fatalf("unexpected error: %s", err)
 	}
 
-	tail, err := io.ReadAll(mr)
+	tail, err := ioutil.ReadAll(mr)
 	if err != nil {
 		t.Fatalf("unexpected error: %s", err)
 	}
@@ -1129,7 +1205,7 @@ func testReadIncompleteStream(t *testing.T, header, body string) {
 	if err := ContinueReadBodyStream(&r, mr, 1, true); err != nil {
 		t.Fatalf("error when reading request body stream: %s", err)
 	}
-	readBody, err := io.ReadAll(r.BodyStream())
+	readBody, err := ioutil.ReadAll(r.BodyStream())
 	if !bytes.Equal(readBody, []byte(body)) || len(readBody) != len(body) {
 		t.Fatalf("readBody is not equal to the rawBody: %b(len: %d)", readBody, len(readBody))
 	}
@@ -1159,7 +1235,7 @@ func testReadChunked(t *testing.T, header, body string, firstRead, leftBytes int
 	if fr != firstRead {
 		t.Fatalf("should read %d from stream body, but got %d", streamRead, fr)
 	}
-	leftB, _ := io.ReadAll(r.BodyStream())
+	leftB, _ := ioutil.ReadAll(r.BodyStream())
 	if len(leftB) != leftBytes {
 		t.Fatalf("should left %d bytes from stream body, but left %d", leftBytes, len(leftB))
 	}
@@ -1182,7 +1258,7 @@ func testContinueReadBodyStream(t *testing.T, header, body string, maxBodySize,
 		t.Fatalf("should read %d from stream body, but got %d", firstRead, sR)
 	}
 
-	leftB, _ := io.ReadAll(r.BodyStream())
+	leftB, _ := ioutil.ReadAll(r.BodyStream())
 	if len(leftB) != leftBytes {
 		t.Fatalf("should left %d bytes from stream body, but left %d", leftBytes, len(leftB))
 	}
@@ -1254,7 +1330,7 @@ tailfoobar`
 		t.Fatalf("unexpected error: %s", err)
 	}
 
-	tail, err := io.ReadAll(mr)
+	tail, err := ioutil.ReadAll(mr)
 	if err != nil {
 		t.Fatalf("unexpected error: %s", err)
 	}
@@ -1332,3 +1408,64 @@ func TestStreamNotEnoughData(t *testing.T) {
 	assert.Nil(t, err)
 	assert.DeepEqual(t, 0, len(conn.Data))
 }
+
+func TestRequestBodyStreamWithTrailer(t *testing.T) {
+	t.Parallel()
+
+	testRequestBodyStreamWithTrailer(t, []byte("test"), false)
+	testRequestBodyStreamWithTrailer(t, mock.CreateFixedBody(4097), false)
+	testRequestBodyStreamWithTrailer(t, mock.CreateFixedBody(105000), false)
+}
+
+func testRequestBodyStreamWithTrailer(t *testing.T, body []byte, disableNormalizing bool) {
+	expectedTrailer := map[string]string{
+		"foo": "testfoo",
+		"bar": "testbar",
+	}
+
+	var req1 protocol.Request
+	if disableNormalizing {
+		req1.Header.DisableNormalizing()
+	}
+	req1.SetHost("google.com")
+	req1.SetBodyStream(bytes.NewBuffer(body), -1)
+	for k, v := range expectedTrailer {
+		err := req1.Header.Trailer().Set(k, v)
+		if err != nil {
+			t.Fatalf("unexpected error: %s", err)
+		}
+	}
+
+	w := &bytes.Buffer{}
+	zw := netpoll.NewWriter(w)
+	if err := Write(&req1, zw); err != nil {
+		t.Fatalf("unexpected error: %s", err)
+	}
+	if err := zw.Flush(); err != nil {
+		t.Fatalf("unexpected error: %s", err)
+	}
+
+	var req2 protocol.Request
+	if disableNormalizing {
+		req2.Header.DisableNormalizing()
+	}
+
+	br := netpoll.NewReader(w)
+	if err := Read(&req2, br); err != nil {
+		t.Fatalf("unexpected error: %s", err)
+	}
+
+	reqBody := req2.Body()
+	if !bytes.Equal(reqBody, body) {
+		t.Fatalf("unexpected body: %q. Excepting %q", reqBody, body)
+	}
+
+	for k, v := range expectedTrailer {
+		kBytes := []byte(k)
+		utils.NormalizeHeaderKey(kBytes, disableNormalizing)
+		r := req2.Header.Trailer().Peek(k)
+		if string(r) != v {
+			t.Fatalf("unexpected trailer header %q: %q. Expecting %s", kBytes, r, v)
+		}
+	}
+}
diff --git a/pkg/protocol/http1/resp/response_test.go b/pkg/protocol/http1/resp/response_test.go
index cf45f65..af146f0 100644
--- a/pkg/protocol/http1/resp/response_test.go
+++ b/pkg/protocol/http1/resp/response_test.go
@@ -46,6 +46,7 @@ import (
 	"bytes"
 	"errors"
 	"io"
+	"io/ioutil"
 	"strings"
 	"testing"
 
@@ -53,6 +54,7 @@ import (
 	errs "github.com/cloudwego/hertz/pkg/common/errors"
 	"github.com/cloudwego/hertz/pkg/common/test/assert"
 	"github.com/cloudwego/hertz/pkg/common/test/mock"
+	"github.com/cloudwego/hertz/pkg/common/utils"
 	"github.com/cloudwego/hertz/pkg/protocol"
 	"github.com/cloudwego/hertz/pkg/protocol/consts"
 	"github.com/cloudwego/netpoll"
@@ -155,11 +157,11 @@ func testResponseReadError(t *testing.T, resp *protocol.Response, response strin
 	}
 
 	testResponseReadSuccess(t, resp, "HTTP/1.1 303 Redisred sedfs sdf\r\nContent-Type: aaa\r\nContent-Length: 5\r\n\r\nHELLOaaa",
-		consts.StatusSeeOther, 5, "aaa", "HELLO", "aaa")
+		consts.StatusSeeOther, 5, "aaa", "HELLO", nil)
 }
 
 func testResponseReadSuccess(t *testing.T, resp *protocol.Response, response string, expectedStatusCode, expectedContentLength int,
-	expectedContentType, expectedBody, expectedTrailer string,
+	expectedContentType, expectedBody string, expectedTrailer map[string]string,
 ) {
 	zr := mock.NewZeroCopyReader(response)
 	err := Read(resp, zr)
@@ -171,7 +173,7 @@ func testResponseReadSuccess(t *testing.T, resp *protocol.Response, response str
 	if !bytes.Equal(resp.Body(), []byte(expectedBody)) {
 		t.Fatalf("Unexpected body %q. Expected %q", resp.Body(), []byte(expectedBody))
 	}
-	assert.VerifyTrailer(t, zr, expectedTrailer)
+	verifyResponseTrailer(t, &resp.Header, expectedTrailer)
 }
 
 func TestResponseReadSuccess(t *testing.T) {
@@ -181,40 +183,46 @@ func TestResponseReadSuccess(t *testing.T) {
 
 	// usual response
 	testResponseReadSuccess(t, resp, "HTTP/1.1 200 OK\r\nContent-Length: 10\r\nContent-Type: foo/bar\r\n\r\n0123456789",
-		consts.StatusOK, 10, "foo/bar", "0123456789", "")
+		consts.StatusOK, 10, "foo/bar", "0123456789", nil)
 
 	// zero response
 	testResponseReadSuccess(t, resp, "HTTP/1.1 500 OK\r\nContent-Length: 0\r\nContent-Type: foo/bar\r\n\r\n",
-		consts.StatusInternalServerError, 0, "foo/bar", "", "")
+		consts.StatusInternalServerError, 0, "foo/bar", "", nil)
 
 	// response with trailer
-	testResponseReadSuccess(t, resp, "HTTP/1.1 300 OK\r\nContent-Length: 5\r\nContent-Type: bar\r\n\r\n56789aaa",
-		consts.StatusMultipleChoices, 5, "bar", "56789", "aaa")
+	testResponseReadSuccess(t, resp, "HTTP/1.1 300 OK\r\nTransfer-Encoding: chunked\r\nTrailer: foo\r\nContent-Type: bar\r\n\r\n5\r\n56789\r\n0\r\nfoo: bar\r\n\r\n",
+		consts.StatusMultipleChoices, 5, "bar", "56789", map[string]string{"Foo": "bar"})
+
+	// response with trailer disableNormalizing
+	resp.Header.DisableNormalizing()
+	resp.Header.Trailer().DisableNormalizing()
+	testResponseReadSuccess(t, resp, "HTTP/1.1 300 OK\r\nTransfer-Encoding: chunked\r\nTrailer: foo\r\nContent-Type: bar\r\n\r\n5\r\n56789\r\n0\r\nfoo: bar\r\n\r\n",
+		consts.StatusMultipleChoices, 5, "bar", "56789", map[string]string{"foo": "bar"})
 
 	// no content-length ('identity' transfer-encoding)
-	testResponseReadSuccess(t, resp, "HTTP/1.1 200 OK\r\nContent-Type: foobar\r\n\r\nzxxc",
-		consts.StatusOK, 4, "foobar", "zxxc", "")
+	testResponseReadSuccess(t, resp, "HTTP/1.1 200 OK\r\nContent-Type: foobar\r\n\r\nzxxxx",
+		consts.StatusOK, 5, "foobar", "zxxxx", nil)
 
 	// explicitly stated 'Transfer-Encoding: identity'
 	testResponseReadSuccess(t, resp, "HTTP/1.1 234 ss\r\nContent-Type: xxx\r\n\r\nxag",
-		234, 3, "xxx", "xag", "")
+		234, 3, "xxx", "xag", nil)
 
 	// big 'identity' response
 	body := string(mock.CreateFixedBody(100500))
 	testResponseReadSuccess(t, resp, "HTTP/1.1 200 OK\r\nContent-Type: aa\r\n\r\n"+body,
-		consts.StatusOK, 100500, "aa", body, "")
+		consts.StatusOK, 100500, "aa", body, nil)
 
 	// chunked response
-	testResponseReadSuccess(t, resp, "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nTransfer-Encoding: chunked\r\n\r\n4\r\nqwer\r\n2\r\nty\r\n0\r\n\r\nzzzzz",
-		consts.StatusOK, 6, "text/html", "qwerty", "zzzzz")
+	testResponseReadSuccess(t, resp, "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nTrailer: Foo2\r\nTransfer-Encoding: chunked\r\n\r\n4\r\nqwer\r\n2\r\nty\r\n0\r\nFoo2: bar2\r\n\r\n",
+		200, 6, "text/html", "qwerty", map[string]string{"Foo2": "bar2"})
 
 	// chunked response with non-chunked Transfer-Encoding.
-	testResponseReadSuccess(t, resp, "HTTP/1.1 230 OK\r\nContent-Type: text\r\nTransfer-Encoding: aaabbb\r\n\r\n2\r\ner\r\n2\r\nty\r\n0\r\n\r\nwe",
-		230, 4, "text", "erty", "we")
+	testResponseReadSuccess(t, resp, "HTTP/1.1 230 OK\r\nContent-Type: text\r\nTrailer: Foo3\r\nTransfer-Encoding: aaabbb\r\n\r\n2\r\ner\r\n2\r\nty\r\n0\r\nFoo3: bar3\r\n\r\n",
+		230, 4, "text", "erty", map[string]string{"Foo3": "bar3"})
 
-	// zero chunked response
-	testResponseReadSuccess(t, resp, "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\nzzz",
-		consts.StatusOK, 0, "text/html", "", "zzz")
+	// chunked response with empty body
+	testResponseReadSuccess(t, resp, "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nTrailer: Foo5\r\nTransfer-Encoding: chunked\r\n\r\n0\r\nFoo5: bar5\r\n\r\n",
+		consts.StatusOK, 0, "text/html", "", map[string]string{"Foo5": "bar5"})
 }
 
 func TestResponseReadError(t *testing.T) {
@@ -391,10 +399,12 @@ func TestResponseReadLimitBody(t *testing.T) {
 	testResponseReadLimitBodySuccess(t, "HTTP/1.1 200 OK\r\nContent-Type: aa\r\nContent-Encoding: gzip\r\n\r\n9876543210", 10)
 	// chunked response
 	testResponseReadLimitBodySuccess(t, "HTTP/1.1 200 OK\r\nContent-Type: aa\r\nTransfer-Encoding: chunked\r\n\r\n6\r\nfoobar\r\n3\r\nbaz\r\n0\r\n\r\n", 9)
+	testResponseReadLimitBodySuccess(t, "HTTP/1.1 200 OK\r\nContent-Type: aa\r\nTransfer-Encoding: chunked\r\n\r\n6\r\nfoobar\r\n3\r\nbaz\r\n0\r\nFoo: bar\r\n\r\n", 9)
 	testResponseReadLimitBodySuccess(t, "HTTP/1.1 200 OK\r\nContent-Type: aa\r\nTransfer-Encoding: chunked\r\n\r\n6\r\nfoobar\r\n3\r\nbaz\r\n0\r\n\r\n", 100)
+	testResponseReadLimitBodySuccess(t, "HTTP/1.1 200 OK\r\nContent-Type: aa\r\nTransfer-Encoding: chunked\r\n\r\n6\r\nfoobar\r\n3\r\nbaz\r\n0\r\nfoobar\r\n\r\n", 100)
 	testResponseReadLimitBodyError(t, "HTTP/1.1 200 OK\r\nContent-Type: aa\r\nTransfer-Encoding: chunked\r\n\r\n6\r\nfoobar\r\n3\r\nbaz\r\n0\r\n\r\n", 2)
-	//
-	//// identity response
+
+	// identity response
 	testResponseReadLimitBodySuccess(t, "HTTP/1.1 400 OK\r\nContent-Type: aa\r\n\r\n123456", 6)
 	testResponseReadLimitBodySuccess(t, "HTTP/1.1 400 OK\r\nContent-Type: aa\r\n\r\n123456", 106)
 	testResponseReadLimitBodyError(t, "HTTP/1.1 400 OK\r\nContent-Type: aa\r\n\r\n123456", 5)
@@ -405,21 +415,21 @@ func TestResponseReadWithoutBody(t *testing.T) {
 
 	var resp protocol.Response
 
-	testResponseReadWithoutBody(t, &resp, "HTTP/1.1 304 Not Modified\r\nContent-Type: aa\r\nContent-Encoding: gzip\r\nContent-Length: 1235\r\n\r\nfoobar", false,
-		consts.StatusNotModified, 1235, "aa", "foobar", "gzip")
+	testResponseReadWithoutBody(t, &resp, "HTTP/1.1 304 Not Modified\r\nContent-Type: aa\r\nContent-Encoding: gzip\r\nContent-Length: 1235\r\n\r\n", false,
+		consts.StatusNotModified, 1235, "aa", nil, "gzip")
 
-	testResponseReadWithoutBody(t, &resp, "HTTP/1.1 204 Foo Bar\r\nContent-Type: aab\r\nContent-Encoding: deflate\r\nTransfer-Encoding: chunked\r\n\r\n123\r\nss", false,
-		consts.StatusNoContent, -1, "aab", "123\r\nss", "deflate")
+	testResponseReadWithoutBody(t, &resp, "HTTP/1.1 204 Foo Bar\r\nContent-Type: aab\r\nTrailer: Foo\r\nContent-Encoding: deflate\r\nTransfer-Encoding: chunked\r\n\r\n0\r\nFoo: bar\r\n\r\n", false,
+		consts.StatusNoContent, -1, "aab", map[string]string{"Foo": "bar"}, "deflate")
 
-	testResponseReadWithoutBody(t, &resp, "HTTP/1.1 123 AAA\r\nContent-Type: xxx\r\nContent-Encoding: gzip\r\nContent-Length: 3434\r\n\r\naaaa", false,
-		123, 3434, "xxx", "aaaa", "gzip")
+	testResponseReadWithoutBody(t, &resp, "HTTP/1.1 123 AAA\r\nContent-Type: xxx\r\nContent-Encoding: gzip\r\nContent-Length: 3434\r\n\r\n", false,
+		123, 3434, "xxx", nil, "gzip")
 
-	testResponseReadWithoutBody(t, &resp, "HTTP 200 OK\r\nContent-Type: text/xml\r\nContent-Encoding: deflate\r\nContent-Length: 123\r\n\r\nxxxx", true,
-		consts.StatusOK, 123, "text/xml", "xxxx", "deflate")
+	testResponseReadWithoutBody(t, &resp, "HTTP 200 OK\r\nContent-Type: text/xml\r\nContent-Encoding: deflate\r\nContent-Length: 123\r\n\r\nfoobar\r\n", true,
+		consts.StatusOK, 123, "text/xml", nil, "deflate")
 
 	// '100 Continue' must be skipped.
-	testResponseReadWithoutBody(t, &resp, "HTTP/1.1 100 Continue\r\nFoo-bar: baz\r\n\r\nHTTP/1.1 329 aaa\r\nContent-Type: qwe\r\nContent-Encoding: gzip\r\nContent-Length: 894\r\n\r\nfoobar", true,
-		329, 894, "qwe", "foobar", "gzip")
+	testResponseReadWithoutBody(t, &resp, "HTTP/1.1 100 Continue\r\nFoo-bar: baz\r\n\r\nHTTP/1.1 329 aaa\r\nContent-Type: qwe\r\nContent-Encoding: gzip\r\nContent-Length: 894\r\n\r\n", true,
+		329, 894, "qwe", nil, "gzip")
 }
 
 func verifyResponseHeader(t *testing.T, h *protocol.ResponseHeader, expectedStatusCode, expectedContentLength int, expectedContentType, expectedContentEncoding string) {
@@ -482,7 +492,7 @@ func testResponseSuccess(t *testing.T, statusCode int, contentType, serverName,
 }
 
 func testResponseReadWithoutBody(t *testing.T, resp *protocol.Response, s string, skipBody bool,
-	expectedStatusCode, expectedContentLength int, expectedContentType, expectedTrailer, expectedContentEncoding string,
+	expectedStatusCode, expectedContentLength int, expectedContentType string, expectedTrailer map[string]string, expectedContentEncoding string,
 ) {
 	zr := mock.NewZeroCopyReader(s)
 	resp.SkipBody = skipBody
@@ -493,13 +503,23 @@ func testResponseReadWithoutBody(t *testing.T, resp *protocol.Response, s string
 	if len(resp.Body()) != 0 {
 		t.Fatalf("Unexpected response body %q. Expected %q. response=%q", resp.Body(), "", s)
 	}
+
 	verifyResponseHeader(t, &resp.Header, expectedStatusCode, expectedContentLength, expectedContentType, expectedContentEncoding)
-	assert.VerifyTrailer(t, zr, expectedTrailer)
+	verifyResponseTrailer(t, &resp.Header, expectedTrailer)
 
 	// verify that ordinal response is read after null-body response
 	resp.SkipBody = false
-	testResponseReadSuccess(t, resp, "HTTP/1.1 300 OK\r\nContent-Length: 5\r\nContent-Type: bar\r\n\r\n56789aaa",
-		consts.StatusMultipleChoices, 5, "bar", "56789", "aaa")
+	testResponseReadSuccess(t, resp, "HTTP/1.1 300 OK\r\nContent-Length: 5\r\nContent-Type: bar\r\n\r\n56789",
+		consts.StatusMultipleChoices, 5, "bar", "56789", nil)
+}
+
+func verifyResponseTrailer(t *testing.T, h *protocol.ResponseHeader, expectedTrailers map[string]string) {
+	for k, v := range expectedTrailers {
+		got := h.Trailer().Peek(k)
+		if !bytes.Equal(got, []byte(v)) {
+			t.Fatalf("Unexpected trailer %q. Expected %q. Got %q", k, v, got)
+		}
+	}
 }
 
 func testResponseReadLimitBodyError(t *testing.T, s string, maxBodySize int) {
@@ -522,8 +542,161 @@ func testResponseReadLimitBodySuccess(t *testing.T, s string, maxBodySize int) {
 	}
 }
 
+func TestResponseBodyStreamWithTrailer(t *testing.T) {
+	t.Parallel()
+
+	testResponseBodyStreamWithTrailer(t, nil, false)
+
+	body := mock.CreateFixedBody(1e5)
+	testResponseBodyStreamWithTrailer(t, body, false)
+	testResponseBodyStreamWithTrailer(t, body, true)
+}
+
+func testResponseBodyStreamWithTrailer(t *testing.T, body []byte, disableNormalizing bool) {
+	expectedTrailer := map[string]string{
+		"foo": "testfoo",
+		"bar": "testbar",
+	}
+	var resp1 protocol.Response
+	if disableNormalizing {
+		resp1.Header.DisableNormalizing()
+	}
+	resp1.SetBodyStream(bytes.NewReader(body), -1)
+	for k, v := range expectedTrailer {
+		err := resp1.Header.Trailer().Add(k, v)
+		if err != nil {
+			t.Fatalf("unexpected error: %s", err)
+		}
+	}
+
+	var w bytes.Buffer
+	zw := netpoll.NewWriter(&w)
+	if err := Write(&resp1, zw); err != nil {
+		t.Fatalf("unexpected error: %s", err)
+	}
+	if err := zw.Flush(); err != nil {
+		t.Fatalf("unexpected error: %s", err)
+	}
+
+	var resp2 protocol.Response
+	if disableNormalizing {
+		resp2.Header.DisableNormalizing()
+	}
+	br := bufio.NewReader(&w)
+	zr := netpoll.NewReader(br)
+	if err := Read(&resp2, zr); err != nil {
+		t.Fatalf("unexpected error: %s", err)
+	}
+
+	respBody := resp2.Body()
+	if !bytes.Equal(respBody, body) {
+		t.Fatalf("unexpected body: %q. Expecting %q", respBody, body)
+	}
+
+	for k, v := range expectedTrailer {
+		kBytes := []byte(k)
+		utils.NormalizeHeaderKey(kBytes, disableNormalizing)
+		r := resp2.Header.Trailer().Peek(k)
+		if string(r) != v {
+			t.Fatalf("unexpected trailer header %q: %q. Expecting %s", kBytes, r, v)
+		}
+	}
+}
+
+func TestSetResponseBodyStreamFixedSize(t *testing.T) {
+	t.Parallel()
+
+	testSetResponseBodyStream(t, "a")
+	testSetResponseBodyStream(t, string(mock.CreateFixedBody(4097)))
+	testSetResponseBodyStream(t, string(mock.CreateFixedBody(100500)))
+}
+
+func TestSetResponseBodyStreamChunked(t *testing.T) {
+	t.Parallel()
+
+	testSetResponseBodyStreamChunked(t, "", map[string]string{"Foo": "bar"})
+
+	body := "foobar baz aaa bbb ccc"
+	testSetResponseBodyStreamChunked(t, body, nil)
+
+	body = string(mock.CreateFixedBody(10001))
+	testSetResponseBodyStreamChunked(t, body, map[string]string{"Foo": "test", "Bar": "test"})
+}
+
+func testSetResponseBodyStream(t *testing.T, body string) {
+	var resp protocol.Response
+	bodySize := len(body)
+	if resp.IsBodyStream() {
+		t.Fatalf("IsBodyStream must return false")
+	}
+	resp.SetBodyStream(bytes.NewBufferString(body), bodySize)
+	if !resp.IsBodyStream() {
+		t.Fatalf("IsBodyStream must return true")
+	}
+
+	var w bytes.Buffer
+	zw := netpoll.NewWriter(&w)
+	if err := Write(&resp, zw); err != nil {
+		t.Fatalf("unexpected error when writing response: %s. body=%q", err, body)
+	}
+	if err := zw.Flush(); err != nil {
+		t.Fatalf("unexpected error when flushing response: %s. body=%q", err, body)
+	}
+
+	var resp1 protocol.Response
+	br := bufio.NewReader(&w)
+	zr := netpoll.NewReader(br)
+	if err := Read(&resp1, zr); err != nil {
+		t.Fatalf("unexpected error when reading response: %s. body=%q", err, body)
+	}
+	if string(resp1.Body()) != body {
+		t.Fatalf("unexpected body %q. Expecting %q", resp1.Body(), body)
+	}
+}
+
+func testSetResponseBodyStreamChunked(t *testing.T, body string, trailer map[string]string) {
+	var resp protocol.Response
+	if resp.IsBodyStream() {
+		t.Fatalf("IsBodyStream must return false")
+	}
+	resp.SetBodyStream(bytes.NewBufferString(body), -1)
+	if !resp.IsBodyStream() {
+		t.Fatalf("IsBodyStream must return true")
+	}
+
+	var w bytes.Buffer
+	zw := netpoll.NewWriter(&w)
+	for k, v := range trailer {
+		err := resp.Header.Trailer().Add(k, v)
+		if err != nil {
+			t.Fatalf("unexpected error: %s", err)
+		}
+	}
+	if err := Write(&resp, zw); err != nil {
+		t.Fatalf("unexpected error when writing response: %s. body=%q", err, body)
+	}
+	if err := zw.Flush(); err != nil {
+		t.Fatalf("unexpected error when flushing response: %s. body=%q", err, body)
+	}
+	var resp1 protocol.Response
+	br := bufio.NewReader(&w)
+	zr := netpoll.NewReader(br)
+	if err := Read(&resp1, zr); err != nil {
+		t.Fatalf("unexpected error when reading response: %s. body=%q", err, body)
+	}
+	if string(resp1.Body()) != body {
+		t.Fatalf("unexpected body %q. Expecting %q", resp1.Body(), body)
+	}
+	for k, v := range trailer {
+		r := resp.Header.Trailer().Peek(k)
+		if string(r) != v {
+			t.Fatalf("unexpected trailer %s. Expecting %s. Got %q", k, v, r)
+		}
+	}
+}
+
 func testResponseReadBodyStreamSuccess(t *testing.T, resp *protocol.Response, response string, expectedStatusCode, expectedContentLength int,
-	expectedContentType, expectedBody, expectedTrailer string,
+	expectedContentType, expectedBody string, expectedTrailer map[string]string,
 ) {
 	zr := mock.NewZeroCopyReader(response)
 	err := ReadBodyStream(resp, zr, 0, nil)
@@ -532,7 +705,7 @@ func testResponseReadBodyStreamSuccess(t *testing.T, resp *protocol.Response, re
 	}
 	assert.True(t, resp.IsBodyStream())
 
-	body, err := io.ReadAll(resp.BodyStream())
+	body, err := ioutil.ReadAll(resp.BodyStream())
 	if err != nil && err != io.EOF {
 		t.Fatalf("Unexpected error: %s", err)
 	}
@@ -540,7 +713,21 @@ func testResponseReadBodyStreamSuccess(t *testing.T, resp *protocol.Response, re
 	if !bytes.Equal(body, []byte(expectedBody)) {
 		t.Fatalf("Unexpected body %q. Expected %q", resp.Body(), []byte(expectedBody))
 	}
-	assert.VerifyTrailer(t, zr, expectedTrailer)
+	verifyResponseTrailer(t, &resp.Header, expectedTrailer)
+}
+
+func testResponseReadBodyStreamBadTrailer(t *testing.T, resp *protocol.Response, response string) {
+	zr := mock.NewZeroCopyReader(response)
+	err := ReadBodyStream(resp, zr, 0, nil)
+	if err != nil {
+		t.Fatalf("Unexpected error: %s", err)
+	}
+	assert.True(t, resp.IsBodyStream())
+
+	_, err = ioutil.ReadAll(resp.BodyStream())
+	if err == nil || err == io.EOF {
+		t.Fatalf("expected error when reading response.")
+	}
 }
 
 func TestResponseReadBodyStream(t *testing.T) {
@@ -550,38 +737,53 @@ func TestResponseReadBodyStream(t *testing.T) {
 
 	// usual response
 	testResponseReadBodyStreamSuccess(t, resp, "HTTP/1.1 200 OK\r\nContent-Length: 10\r\nContent-Type: foo/bar\r\n\r\n0123456789",
-		consts.StatusOK, 10, "foo/bar", "0123456789", "")
+		consts.StatusOK, 10, "foo/bar", "0123456789", nil)
 
 	// zero response
 	testResponseReadBodyStreamSuccess(t, resp, "HTTP/1.1 500 OK\r\nContent-Length: 0\r\nContent-Type: foo/bar\r\n\r\n",
-		consts.StatusInternalServerError, 0, "foo/bar", "", "")
+		consts.StatusInternalServerError, 0, "foo/bar", "", nil)
 
 	// response with trailer
-	testResponseReadBodyStreamSuccess(t, resp, "HTTP/1.1 300 OK\r\nContent-Length: 5\r\nContent-Type: bar\r\n\r\n56789aaa",
-		consts.StatusMultipleChoices, 5, "bar", "56789", "aaa")
+	testResponseReadBodyStreamSuccess(t, resp, "HTTP/1.1 300 OK\r\nTransfer-Encoding: chunked\r\nTrailer: Foo\r\nContent-Type: bar\r\n\r\n5\r\n56789\r\n0\r\nfoo: bar\r\n\r\n",
+		consts.StatusMultipleChoices, -1, "bar", "56789", map[string]string{"Foo": "bar"})
+
+	// response with trailer disableNormalizing
+	resp.Header.DisableNormalizing()
+	resp.Header.Trailer().DisableNormalizing()
+	testResponseReadBodyStreamSuccess(t, resp, "HTTP/1.1 300 OK\r\nTransfer-Encoding: chunked\r\nTrailer: foo\r\nContent-Type: bar\r\n\r\n5\r\n56789\r\n0\r\nfoo: bar\r\n\r\n",
+		consts.StatusMultipleChoices, -1, "bar", "56789", map[string]string{"foo": "bar"})
 
 	// no content-length ('identity' transfer-encoding)
-	testResponseReadBodyStreamSuccess(t, resp, "HTTP/1.1 200 OK\r\nContent-Type: foobar\r\n\r\nzxxc",
-		consts.StatusOK, -2, "foobar", "zxxc", "")
+	testResponseReadBodyStreamSuccess(t, resp, "HTTP/1.1 200 OK\r\nContent-Type: foobar\r\n\r\nzxxxx",
+		consts.StatusOK, -2, "foobar", "zxxxx", nil)
 
 	// explicitly stated 'Transfer-Encoding: identity'
 	testResponseReadBodyStreamSuccess(t, resp, "HTTP/1.1 234 ss\r\nContent-Type: xxx\r\n\r\nxag",
-		234, -2, "xxx", "xag", "")
+		234, -2, "xxx", "xag", nil)
 
 	// big 'identity' response
 	body := string(mock.CreateFixedBody(100500))
 	testResponseReadBodyStreamSuccess(t, resp, "HTTP/1.1 200 OK\r\nContent-Type: aa\r\n\r\n"+body,
-		consts.StatusOK, -2, "aa", body, "")
+		consts.StatusOK, -2, "aa", body, nil)
 
 	// chunked response
-	testResponseReadBodyStreamSuccess(t, resp, "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nTransfer-Encoding: chunked\r\n\r\n4\r\nqwer\r\n2\r\nty\r\n0\r\n\r\nzzzzz",
-		consts.StatusOK, -1, "text/html", "qwerty", "zzzzz")
+	testResponseReadBodyStreamSuccess(t, resp, "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nTrailer: Foo2\r\nTransfer-Encoding: chunked\r\n\r\n4\r\nqwer\r\n2\r\nty\r\n0\r\nFoo2: bar2\r\n\r\n",
+		200, -1, "text/html", "qwerty", map[string]string{"Foo2": "bar2"})
 
 	// chunked response with non-chunked Transfer-Encoding.
-	testResponseReadBodyStreamSuccess(t, resp, "HTTP/1.1 230 OK\r\nContent-Type: text\r\nTransfer-Encoding: aaabbb\r\n\r\n2\r\ner\r\n2\r\nty\r\n0\r\n\r\nwe",
-		230, -1, "text", "erty", "we")
+	testResponseReadBodyStreamSuccess(t, resp, "HTTP/1.1 230 OK\r\nContent-Type: text\r\nTrailer: Foo3\r\nTransfer-Encoding: aaabbb\r\n\r\n2\r\ner\r\n2\r\nty\r\n0\r\nFoo3: bar3\r\n\r\n",
+		230, -1, "text", "erty", map[string]string{"Foo3": "bar3"})
+
+	// chunked response with empty body
+	testResponseReadBodyStreamSuccess(t, resp, "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nTrailer: Foo5\r\nTransfer-Encoding: chunked\r\n\r\n0\r\nFoo5: bar5\r\n\r\n",
+		consts.StatusOK, -1, "text/html", "", map[string]string{"Foo5": "bar5"})
+}
+
+func TestResponseReadBodyStreamBadTrailer(t *testing.T) {
+	t.Parallel()
+
+	resp := &protocol.Response{}
 
-	// zero chunked response
-	testResponseReadBodyStreamSuccess(t, resp, "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\nzzz",
-		consts.StatusOK, -1, "text/html", "", "zzz")
+	testResponseReadBodyStreamBadTrailer(t, resp, "HTTP/1.1 300 OK\r\nTransfer-Encoding: chunked\r\nContent-Type: bar\r\n\r\n5\r\n56789\r\n0\r\ncontent-type: bar\r\n\r\n")
+	testResponseReadBodyStreamBadTrailer(t, resp, "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nTransfer-Encoding: chunked\r\n\r\n4\r\nqwer\r\n2\r\nty\r\n0\r\nproxy-connection: bar2\r\n\r\n")
 }
diff --git a/pkg/protocol/http1/resp/writer_test.go b/pkg/protocol/http1/resp/writer_test.go
new file mode 100644
index 0000000..bca2d43
--- /dev/null
+++ b/pkg/protocol/http1/resp/writer_test.go
@@ -0,0 +1,53 @@
+/*
+ * 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 resp
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/cloudwego/hertz/internal/bytestr"
+	"github.com/cloudwego/hertz/pkg/common/test/assert"
+	"github.com/cloudwego/hertz/pkg/common/test/mock"
+	"github.com/cloudwego/hertz/pkg/protocol"
+)
+
+func TestNewChunkedBodyWriter(t *testing.T) {
+	response := protocol.AcquireResponse()
+	mockConn := mock.NewConn("")
+	w := NewChunkedBodyWriter(response, mockConn)
+	w.Write([]byte("hello"))
+	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), "5"+string(bytestr.StrCRLF)+"hello"))
+	assert.False(t, strings.Contains(string(out), "0"+string(bytestr.StrCRLF)+string(bytestr.StrCRLF)))
+}
+
+func TestNewChunkedBodyWriter1(t *testing.T) {
+	response := protocol.AcquireResponse()
+	mockConn := mock.NewConn("")
+	w := NewChunkedBodyWriter(response, mockConn)
+	w.Write([]byte("hello"))
+	w.Flush()
+	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), "5"+string(bytestr.StrCRLF)+"hello"))
+	assert.True(t, strings.Contains(string(out), "0"+string(bytestr.StrCRLF)+string(bytestr.StrCRLF)))
+}
diff --git a/pkg/protocol/http1/server_test.go b/pkg/protocol/http1/server_test.go
index 4a2a15a..dfa8b78 100644
--- a/pkg/protocol/http1/server_test.go
+++ b/pkg/protocol/http1/server_test.go
@@ -17,6 +17,7 @@
 package http1
 
 import (
+	"bytes"
 	"context"
 	"errors"
 	"sync"
@@ -31,6 +32,8 @@ import (
 	"github.com/cloudwego/hertz/pkg/common/tracer/stats"
 	"github.com/cloudwego/hertz/pkg/common/tracer/traceinfo"
 	"github.com/cloudwego/hertz/pkg/network"
+	"github.com/cloudwego/hertz/pkg/protocol"
+	"github.com/cloudwego/hertz/pkg/protocol/http1/resp"
 )
 
 var pool = &sync.Pool{New: func() interface{} {
@@ -189,9 +192,65 @@ func TestEventStack(t *testing.T) {
 	}
 }
 
+func TestDefaultWriter(t *testing.T) {
+	server := &Server{}
+	reqCtx := &app.RequestContext{}
+	server.Core = &mockCore{
+		ctxPool: &sync.Pool{New: func() interface{} {
+			return reqCtx
+		}},
+		mockHandler: func(c context.Context, ctx *app.RequestContext) {
+			ctx.Write([]byte("hello, hertz"))
+			ctx.Flush()
+		},
+	}
+	defaultConn := mock.NewConn("GET / HTTP/1.1\nHost: foobar.com\n\n")
+	err := server.Serve(context.TODO(), defaultConn)
+	assert.True(t, errors.Is(err, errs.ErrShortConnection))
+	defaultResponseResult := defaultConn.WriterRecorder()
+	assert.DeepEqual(t, 0, defaultResponseResult.Len()) // all data is flushed so the buffer length is 0
+	response := protocol.AcquireResponse()
+	resp.Read(response, defaultResponseResult)
+	assert.DeepEqual(t, "hello, hertz", string(response.Body()))
+}
+
+func TestHijackResponseWriter(t *testing.T) {
+	server := &Server{}
+	reqCtx := &app.RequestContext{}
+	buf := new(bytes.Buffer)
+	isFinal := false
+	server.Core = &mockCore{
+		ctxPool: &sync.Pool{New: func() interface{} {
+			return reqCtx
+		}},
+		mockHandler: func(c context.Context, ctx *app.RequestContext) {
+			// response before write will be dropped
+			ctx.Write([]byte("invalid data"))
+
+			ctx.Response.HijackWriter(&mock.ExtWriter{
+				Buf:     buf,
+				IsFinal: &isFinal,
+			})
+
+			ctx.Write([]byte("hello, hertz"))
+			ctx.Flush()
+		},
+	}
+	defaultConn := mock.NewConn("GET / HTTP/1.1\nHost: foobar.com\n\n")
+	err := server.Serve(context.TODO(), defaultConn)
+	assert.True(t, errors.Is(err, errs.ErrShortConnection))
+	defaultResponseResult := defaultConn.WriterRecorder()
+	response := protocol.AcquireResponse()
+	resp.Read(response, defaultResponseResult)
+	assert.DeepEqual(t, 0, len(response.Body()))
+	assert.DeepEqual(t, "hello, hertz", buf.String())
+	assert.True(t, isFinal)
+}
+
 type mockCore struct {
-	ctxPool    *sync.Pool
-	controller tracer.Controller
+	ctxPool     *sync.Pool
+	controller  tracer.Controller
+	mockHandler func(c context.Context, ctx *app.RequestContext)
 }
 
 func (m *mockCore) IsRunning() bool {
@@ -202,7 +261,11 @@ func (m *mockCore) GetCtxPool() *sync.Pool {
 	return m.ctxPool
 }
 
-func (m *mockCore) ServeHTTP(c context.Context, ctx *app.RequestContext) {}
+func (m *mockCore) ServeHTTP(c context.Context, ctx *app.RequestContext) {
+	if m.mockHandler != nil {
+		m.mockHandler(c, ctx)
+	}
+}
 
 func (m *mockCore) GetTracer() tracer.Controller {
 	return m.controller
diff --git a/pkg/protocol/request_test.go b/pkg/protocol/request_test.go
index 850b73d..0f52686 100644
--- a/pkg/protocol/request_test.go
+++ b/pkg/protocol/request_test.go
@@ -46,6 +46,7 @@ import (
 	"encoding/base64"
 	"fmt"
 	"io"
+	"io/ioutil"
 	"math"
 	"mime/multipart"
 	"strings"
@@ -147,7 +148,7 @@ tailfoobar`
 	defer r.RemoveMultipartFormFiles()
 
 	// verify tail
-	tail, err := io.ReadAll(mr)
+	tail, err := ioutil.ReadAll(mr)
 	if err != nil {
 		t.Fatalf("unexpected error: %s", err)
 	}
@@ -229,7 +230,7 @@ tailfoobar`
 	defer r.RemoveMultipartFormFiles()
 
 	// all data must be consumed if the content length is unknown
-	tail, err := io.ReadAll(mr)
+	tail, err := ioutil.ReadAll(mr)
 	if err != nil {
 		t.Fatalf("unexpected error: %s", err)
 	}
diff --git a/pkg/protocol/response_test.go b/pkg/protocol/response_test.go
index 1ecb806..20a18ff 100644
--- a/pkg/protocol/response_test.go
+++ b/pkg/protocol/response_test.go
@@ -51,6 +51,7 @@ import (
 	"github.com/cloudwego/hertz/pkg/common/bytebufferpool"
 	"github.com/cloudwego/hertz/pkg/common/compress"
 	"github.com/cloudwego/hertz/pkg/common/test/assert"
+	"github.com/cloudwego/hertz/pkg/common/test/mock"
 	"github.com/cloudwego/hertz/pkg/protocol/consts"
 )
 
@@ -272,3 +273,24 @@ func TestRespSafeCopy(t *testing.T) {
 		assert.DeepEqual(t, []byte{byte(i)}, resps[i].Body())
 	}
 }
+
+func TestResponse_HijackWriter(t *testing.T) {
+	resp := AcquireResponse()
+	buf := new(bytes.Buffer)
+	isFinal := false
+	resp.HijackWriter(&mock.ExtWriter{Buf: buf, IsFinal: &isFinal})
+	resp.AppendBody([]byte("hello"))
+	assert.DeepEqual(t, 0, buf.Len())
+	resp.GetHijackWriter().Flush()
+	assert.DeepEqual(t, "hello", buf.String())
+	resp.AppendBodyString(", world")
+	assert.DeepEqual(t, "hello", buf.String())
+	resp.GetHijackWriter().Flush()
+	assert.DeepEqual(t, "hello, world", buf.String())
+	resp.SetBody([]byte("hello, hertz"))
+	resp.GetHijackWriter().Flush()
+	assert.DeepEqual(t, "hello, hertz", buf.String())
+	assert.False(t, isFinal)
+	resp.GetHijackWriter().Finalize()
+	assert.True(t, isFinal)
+}
diff --git a/pkg/protocol/trailer_test.go b/pkg/protocol/trailer_test.go
new file mode 100644
index 0000000..ced1cc2
--- /dev/null
+++ b/pkg/protocol/trailer_test.go
@@ -0,0 +1,161 @@
+/*
+ * 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 protocol
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/cloudwego/hertz/internal/bytestr"
+
+	"github.com/cloudwego/hertz/pkg/common/test/assert"
+	"github.com/cloudwego/hertz/pkg/protocol/consts"
+)
+
+func TestTrailerAdd(t *testing.T) {
+	var tr Trailer
+	assert.Nil(t, tr.Add("foo", "value1"))
+	assert.Nil(t, tr.Add("foo", "value2"))
+	assert.Nil(t, tr.Add("bar", "value3"))
+	assert.True(t, strings.Contains(string(tr.Header()), "Foo: value1"))
+	assert.True(t, strings.Contains(string(tr.Header()), "Foo: value2"))
+	assert.True(t, strings.Contains(string(tr.Header()), "Bar: value3"))
+}
+
+func TestHeaderTrailerSet(t *testing.T) {
+	h := &RequestHeader{}
+
+	// only one trailer
+	h.Set("Trailer", "Foo")
+	assert.True(t, strings.Contains(string(h.Trailer().Header()), "Foo:"))
+
+	// multi trailer
+	h.Set("Trailer", "Foo, bar, HERtz")
+	assert.True(t, strings.Contains(string(h.Trailer().Header()), "Foo:"))
+	assert.True(t, strings.Contains(string(h.Trailer().Header()), "Bar:"))
+	assert.True(t, strings.Contains(string(h.Trailer().Header()), "Hertz:"))
+
+	// all lowercase
+	h.Set("Trailer", "foo,hertz,aaa")
+	assert.True(t, strings.Contains(string(h.Trailer().Header()), "Foo:"))
+	assert.True(t, strings.Contains(string(h.Trailer().Header()), "Hertz:"))
+	assert.True(t, strings.Contains(string(h.Trailer().Header()), "Aaa:"))
+
+	// all uppercase
+	h.Set("Trailer", "FOO,HERTZ,AAA")
+	assert.True(t, strings.Contains(string(h.Trailer().Header()), "Foo:"))
+	assert.True(t, strings.Contains(string(h.Trailer().Header()), "Hertz:"))
+	assert.True(t, strings.Contains(string(h.Trailer().Header()), "Aaa:"))
+
+	// with '-'
+	h.Set("Trailer", "FOO-HERTZ-AAA")
+	assert.True(t, strings.Contains(string(h.Trailer().Header()), "Foo-Hertz-Aaa:"))
+
+	// more space
+	h.Set("Trailer", "      foo,      hertz       ,        aaa      ")
+	assert.True(t, strings.Contains(string(h.Trailer().Header()), "Foo:"))
+	assert.True(t, strings.Contains(string(h.Trailer().Header()), "Hertz:"))
+	assert.True(t, strings.Contains(string(h.Trailer().Header()), "Aaa:"))
+}
+
+func TestTrailerAddError(t *testing.T) {
+	var tr Trailer
+	assert.NotNil(t, tr.Add(consts.HeaderContentType, ""))
+	assert.NotNil(t, tr.Set(consts.HeaderProxyConnection, ""))
+}
+
+func TestTrailerDel(t *testing.T) {
+	var tr Trailer
+	assert.Nil(t, tr.Add("foo", "value1"))
+	assert.Nil(t, tr.Add("foo", "value2"))
+	assert.Nil(t, tr.Add("bar", "value3"))
+	tr.Del("foo")
+	assert.False(t, strings.Contains(string(tr.Header()), "Foo: value1"))
+	assert.False(t, strings.Contains(string(tr.Header()), "Foo: value2"))
+	assert.True(t, strings.Contains(string(tr.Header()), "Bar: value3"))
+}
+
+func TestTrailerSet(t *testing.T) {
+	var tr Trailer
+	assert.Nil(t, tr.Set("foo", "value1"))
+	assert.Nil(t, tr.Set("foo", "value2"))
+	assert.Nil(t, tr.Set("bar", "value3"))
+	assert.False(t, strings.Contains(string(tr.Header()), "Foo: value1"))
+	assert.True(t, strings.Contains(string(tr.Header()), "Foo: value2"))
+	assert.True(t, strings.Contains(string(tr.Header()), "Bar: value3"))
+}
+
+func TestTrailerGet(t *testing.T) {
+	var tr Trailer
+	assert.Nil(t, tr.Add("foo", "value1"))
+	assert.Nil(t, tr.Add("bar", "value3"))
+	assert.DeepEqual(t, tr.Get("foo"), "value1")
+	assert.DeepEqual(t, tr.Get("bar"), "value3")
+}
+
+func TestTrailerUpdateArgBytes(t *testing.T) {
+	var tr Trailer
+	assert.Nil(t, tr.addArgBytes([]byte("Foo"), []byte("value0"), argsNoValue))
+	assert.Nil(t, tr.UpdateArgBytes([]byte("Foo"), []byte("value1")))
+	assert.Nil(t, tr.UpdateArgBytes([]byte("Foo"), []byte("value2")))
+	assert.Nil(t, tr.UpdateArgBytes([]byte("Bar"), []byte("value3")))
+	assert.True(t, strings.Contains(string(tr.Header()), "Foo: value1"))
+	assert.False(t, strings.Contains(string(tr.Header()), "Foo: value2"))
+	assert.False(t, strings.Contains(string(tr.Header()), "Bar: value3"))
+}
+
+func TestTrailerEmpty(t *testing.T) {
+	var tr Trailer
+	assert.DeepEqual(t, tr.Empty(), true)
+	assert.Nil(t, tr.Set("foo", ""))
+	assert.DeepEqual(t, tr.Empty(), false)
+}
+
+func TestTrailerVisitAll(t *testing.T) {
+	var tr Trailer
+	assert.Nil(t, tr.Add("foo", "value1"))
+	assert.Nil(t, tr.Add("bar", "value2"))
+	tr.VisitAll(
+		func(k, v []byte) {
+			key := string(k)
+			value := string(v)
+			if (key != "Foo" || value != "value1") && (key != "Bar" || value != "value2") {
+				t.Fatalf("Unexpected (%v, %v). Expected %v", key, value, "(foo, value1) or (bar, value2)")
+			}
+		})
+}
+
+func TestIsBadTrailer(t *testing.T) {
+	assert.True(t, IsBadTrailer(bytestr.StrAuthorization))
+	assert.True(t, IsBadTrailer(bytestr.StrContentEncoding))
+	assert.True(t, IsBadTrailer(bytestr.StrContentLength))
+	assert.True(t, IsBadTrailer(bytestr.StrContentType))
+	assert.True(t, IsBadTrailer(bytestr.StrContentRange))
+	assert.True(t, IsBadTrailer(bytestr.StrConnection))
+	assert.True(t, IsBadTrailer(bytestr.StrExpect))
+	assert.True(t, IsBadTrailer(bytestr.StrHost))
+	assert.True(t, IsBadTrailer(bytestr.StrKeepAlive))
+	assert.True(t, IsBadTrailer(bytestr.StrMaxForwards))
+	assert.True(t, IsBadTrailer(bytestr.StrProxyConnection))
+	assert.True(t, IsBadTrailer(bytestr.StrProxyAuthenticate))
+	assert.True(t, IsBadTrailer(bytestr.StrProxyAuthorization))
+	assert.True(t, IsBadTrailer(bytestr.StrRange))
+	assert.True(t, IsBadTrailer(bytestr.StrTE))
+	assert.True(t, IsBadTrailer(bytestr.StrTrailer))
+	assert.True(t, IsBadTrailer(bytestr.StrTransferEncoding))
+	assert.True(t, IsBadTrailer(bytestr.StrWWWAuthenticate))
+}
diff --git a/pkg/route/engine_test.go b/pkg/route/engine_test.go
index d73bc8c..fe528b4 100644
--- a/pkg/route/engine_test.go
+++ b/pkg/route/engine_test.go
@@ -46,7 +46,7 @@ import (
 	"errors"
 	"fmt"
 	"html/template"
-	"io"
+	"io/ioutil"
 	"net"
 	"net/http"
 	"sync/atomic"
@@ -411,7 +411,7 @@ func TestRenderHtml(t *testing.T) {
 		})
 	})
 	rr := performRequest(e, "GET", "/templateName")
-	b, _ := io.ReadAll(rr.Body)
+	b, _ := ioutil.ReadAll(rr.Body)
 	assert.DeepEqual(t, consts.StatusOK, rr.Code)
 	assert.DeepEqual(t, []byte("<h1>Date: 2017/07/01</h1>"), b)
 	assert.DeepEqual(t, "text/html; charset=utf-8", rr.Header().Get("Content-Type"))
@@ -458,7 +458,7 @@ func TestRenderHtmlOfGlobWithAutoRender(t *testing.T) {
 		})
 	})
 	rr := performRequest(e, "GET", "/templateName")
-	b, _ := io.ReadAll(rr.Body)
+	b, _ := ioutil.ReadAll(rr.Body)
 	assert.DeepEqual(t, consts.StatusOK, rr.Code)
 	assert.DeepEqual(t, []byte("<h1>Date: 2017/07/01</h1>"), b)
 	assert.DeepEqual(t, "text/html; charset=utf-8", rr.Header().Get("Content-Type"))
@@ -496,7 +496,7 @@ func TestRenderHtmlOfFilesWithAutoRender(t *testing.T) {
 		})
 	})
 	rr := performRequest(e, "GET", "/templateName")
-	b, _ := io.ReadAll(rr.Body)
+	b, _ := ioutil.ReadAll(rr.Body)
 	assert.DeepEqual(t, consts.StatusOK, rr.Code)
 	assert.DeepEqual(t, []byte("<h1>Date: 2017/07/01</h1>"), b)
 	assert.DeepEqual(t, "text/html; charset=utf-8", rr.Header().Get("Content-Type"))
diff --git a/pkg/route/routergroup_test.go b/pkg/route/routergroup_test.go
index 5f72d21..3a6a1c0 100644
--- a/pkg/route/routergroup_test.go
+++ b/pkg/route/routergroup_test.go
@@ -42,7 +42,7 @@ package route
 
 import (
 	"context"
-	"io"
+	"io/ioutil"
 	"net/http"
 	"os"
 	"testing"
@@ -137,7 +137,7 @@ func TestRouterGroupStatic(t *testing.T) {
 	}
 	assert.DeepEqual(t, http.StatusOK, w.Code)
 	defer fd.Close()
-	content, err := io.ReadAll(fd)
+	content, err := ioutil.ReadAll(fd)
 	if err != nil {
 		panic(err)
 	}
@@ -154,7 +154,7 @@ func TestRouterGroupStaticFile(t *testing.T) {
 		panic(err)
 	}
 	defer fd.Close()
-	content, err := io.ReadAll(fd)
+	content, err := ioutil.ReadAll(fd)
 	if err != nil {
 		panic(err)
 	}
diff --git a/pkg/route/routes_test.go b/pkg/route/routes_test.go
index c94baa0..1e76d67 100644
--- a/pkg/route/routes_test.go
+++ b/pkg/route/routes_test.go
@@ -43,6 +43,7 @@ package route
 import (
 	"context"
 	"fmt"
+	"io/ioutil"
 	"net/http"
 	"net/http/httptest"
 	"os"
@@ -397,7 +398,7 @@ func TestRouteParamsByNameWithExtraSlash(t *testing.T) {
 func TestRouteStaticFile(t *testing.T) {
 	// SETUP file
 	testRoot, _ := os.Getwd()
-	f, err := os.CreateTemp(testRoot, "")
+	f, err := ioutil.TempFile(testRoot, "")
 	if err != nil {
 		t.Error(err)
 	}
diff --git a/pkg/route/routes_timing_test.go b/pkg/route/routes_timing_test.go
index d6de0b4..4beb71d 100644
--- a/pkg/route/routes_timing_test.go
+++ b/pkg/route/routes_timing_test.go
@@ -50,171 +50,170 @@ import (
 )
 
 type Route struct {
-	method string
-	path   string
+	path string
 }
 
 func BenchmarkTree_FindStatic(b *testing.B) {
 	tree := &router{method: "GET", root: &node{}, hasTsrHandler: make(map[string]bool)}
 
 	static := []*Route{
-		{"GET", "/"},
-		{"GET", "/cmd.html"},
-		{"GET", "/code.html"},
-		{"GET", "/contrib.html"},
-		{"GET", "/contribute.html"},
-		{"GET", "/debugging_with_gdb.html"},
-		{"GET", "/docs.html"},
-		{"GET", "/effective_go.html"},
-		{"GET", "/files.log"},
-		{"GET", "/gccgo_contribute.html"},
-		{"GET", "/gccgo_install.html"},
-		{"GET", "/go-logo-black.png"},
-		{"GET", "/go-logo-blue.png"},
-		{"GET", "/go-logo-white.png"},
-		{"GET", "/go1.1.html"},
-		{"GET", "/go1.2.html"},
-		{"GET", "/go1.html"},
-		{"GET", "/go1compat.html"},
-		{"GET", "/go_faq.html"},
-		{"GET", "/go_mem.html"},
-		{"GET", "/go_spec.html"},
-		{"GET", "/help.html"},
-		{"GET", "/ie.css"},
-		{"GET", "/install-source.html"},
-		{"GET", "/install.html"},
-		{"GET", "/logo-153x55.png"},
-		{"GET", "/Makefile"},
-		{"GET", "/root.html"},
-		{"GET", "/share.png"},
-		{"GET", "/sieve.gif"},
-		{"GET", "/tos.html"},
-		{"GET", "/articles/"},
-		{"GET", "/articles/go_command.html"},
-		{"GET", "/articles/index.html"},
-		{"GET", "/articles/wiki/"},
-		{"GET", "/articles/wiki/edit.html"},
-		{"GET", "/articles/wiki/final-noclosure.go"},
-		{"GET", "/articles/wiki/final-noerror.go"},
-		{"GET", "/articles/wiki/final-parsetemplate.go"},
-		{"GET", "/articles/wiki/final-template.go"},
-		{"GET", "/articles/wiki/final.go"},
-		{"GET", "/articles/wiki/get.go"},
-		{"GET", "/articles/wiki/http-sample.go"},
-		{"GET", "/articles/wiki/index.html"},
-		{"GET", "/articles/wiki/Makefile"},
-		{"GET", "/articles/wiki/notemplate.go"},
-		{"GET", "/articles/wiki/part1-noerror.go"},
-		{"GET", "/articles/wiki/part1.go"},
-		{"GET", "/articles/wiki/part2.go"},
-		{"GET", "/articles/wiki/part3-errorhandling.go"},
-		{"GET", "/articles/wiki/part3.go"},
-		{"GET", "/articles/wiki/test.bash"},
-		{"GET", "/articles/wiki/test_edit.good"},
-		{"GET", "/articles/wiki/test_Test.txt.good"},
-		{"GET", "/articles/wiki/test_view.good"},
-		{"GET", "/articles/wiki/view.html"},
-		{"GET", "/codewalk/"},
-		{"GET", "/codewalk/codewalk.css"},
-		{"GET", "/codewalk/codewalk.js"},
-		{"GET", "/codewalk/codewalk.xml"},
-		{"GET", "/codewalk/functions.xml"},
-		{"GET", "/codewalk/markov.go"},
-		{"GET", "/codewalk/markov.xml"},
-		{"GET", "/codewalk/pig.go"},
-		{"GET", "/codewalk/popout.png"},
-		{"GET", "/codewalk/run"},
-		{"GET", "/codewalk/sharemem.xml"},
-		{"GET", "/codewalk/urlpoll.go"},
-		{"GET", "/devel/"},
-		{"GET", "/devel/release.html"},
-		{"GET", "/devel/weekly.html"},
-		{"GET", "/gopher/"},
-		{"GET", "/gopher/appenginegopher.jpg"},
-		{"GET", "/gopher/appenginegophercolor.jpg"},
-		{"GET", "/gopher/appenginelogo.gif"},
-		{"GET", "/gopher/bumper.png"},
-		{"GET", "/gopher/bumper192x108.png"},
-		{"GET", "/gopher/bumper320x180.png"},
-		{"GET", "/gopher/bumper480x270.png"},
-		{"GET", "/gopher/bumper640x360.png"},
-		{"GET", "/gopher/doc.png"},
-		{"GET", "/gopher/frontpage.png"},
-		{"GET", "/gopher/gopherbw.png"},
-		{"GET", "/gopher/gophercolor.png"},
-		{"GET", "/gopher/gophercolor16x16.png"},
-		{"GET", "/gopher/help.png"},
-		{"GET", "/gopher/pkg.png"},
-		{"GET", "/gopher/project.png"},
-		{"GET", "/gopher/ref.png"},
-		{"GET", "/gopher/run.png"},
-		{"GET", "/gopher/talks.png"},
-		{"GET", "/gopher/pencil/"},
-		{"GET", "/gopher/pencil/gopherhat.jpg"},
-		{"GET", "/gopher/pencil/gopherhelmet.jpg"},
-		{"GET", "/gopher/pencil/gophermega.jpg"},
-		{"GET", "/gopher/pencil/gopherrunning.jpg"},
-		{"GET", "/gopher/pencil/gopherswim.jpg"},
-		{"GET", "/gopher/pencil/gopherswrench.jpg"},
-		{"GET", "/play/"},
-		{"GET", "/play/fib.go"},
-		{"GET", "/play/hello.go"},
-		{"GET", "/play/life.go"},
-		{"GET", "/play/peano.go"},
-		{"GET", "/play/pi.go"},
-		{"GET", "/play/sieve.go"},
-		{"GET", "/play/solitaire.go"},
-		{"GET", "/play/tree.go"},
-		{"GET", "/progs/"},
-		{"GET", "/progs/cgo1.go"},
-		{"GET", "/progs/cgo2.go"},
-		{"GET", "/progs/cgo3.go"},
-		{"GET", "/progs/cgo4.go"},
-		{"GET", "/progs/defer.go"},
-		{"GET", "/progs/defer.out"},
-		{"GET", "/progs/defer2.go"},
-		{"GET", "/progs/defer2.out"},
-		{"GET", "/progs/eff_bytesize.go"},
-		{"GET", "/progs/eff_bytesize.out"},
-		{"GET", "/progs/eff_qr.go"},
-		{"GET", "/progs/eff_sequence.go"},
-		{"GET", "/progs/eff_sequence.out"},
-		{"GET", "/progs/eff_unused1.go"},
-		{"GET", "/progs/eff_unused2.go"},
-		{"GET", "/progs/error.go"},
-		{"GET", "/progs/error2.go"},
-		{"GET", "/progs/error3.go"},
-		{"GET", "/progs/error4.go"},
-		{"GET", "/progs/go1.go"},
-		{"GET", "/progs/gobs1.go"},
-		{"GET", "/progs/gobs2.go"},
-		{"GET", "/progs/image_draw.go"},
-		{"GET", "/progs/image_package1.go"},
-		{"GET", "/progs/image_package1.out"},
-		{"GET", "/progs/image_package2.go"},
-		{"GET", "/progs/image_package2.out"},
-		{"GET", "/progs/image_package3.go"},
-		{"GET", "/progs/image_package3.out"},
-		{"GET", "/progs/image_package4.go"},
-		{"GET", "/progs/image_package4.out"},
-		{"GET", "/progs/image_package5.go"},
-		{"GET", "/progs/image_package5.out"},
-		{"GET", "/progs/image_package6.go"},
-		{"GET", "/progs/image_package6.out"},
-		{"GET", "/progs/interface.go"},
-		{"GET", "/progs/interface2.go"},
-		{"GET", "/progs/interface2.out"},
-		{"GET", "/progs/json1.go"},
-		{"GET", "/progs/json2.go"},
-		{"GET", "/progs/json2.out"},
-		{"GET", "/progs/json3.go"},
-		{"GET", "/progs/json4.go"},
-		{"GET", "/progs/json5.go"},
-		{"GET", "/progs/run"},
-		{"GET", "/progs/slices.go"},
-		{"GET", "/progs/timeout1.go"},
-		{"GET", "/progs/timeout2.go"},
-		{"GET", "/progs/update.bash"},
+		{"/"},
+		{"/cmd.html"},
+		{"/code.html"},
+		{"/contrib.html"},
+		{"/contribute.html"},
+		{"/debugging_with_gdb.html"},
+		{"/docs.html"},
+		{"/effective_go.html"},
+		{"/files.log"},
+		{"/gccgo_contribute.html"},
+		{"/gccgo_install.html"},
+		{"/go-logo-black.png"},
+		{"/go-logo-blue.png"},
+		{"/go-logo-white.png"},
+		{"/go1.1.html"},
+		{"/go1.2.html"},
+		{"/go1.html"},
+		{"/go1compat.html"},
+		{"/go_faq.html"},
+		{"/go_mem.html"},
+		{"/go_spec.html"},
+		{"/help.html"},
+		{"/ie.css"},
+		{"/install-source.html"},
+		{"/install.html"},
+		{"/logo-153x55.png"},
+		{"/Makefile"},
+		{"/root.html"},
+		{"/share.png"},
+		{"/sieve.gif"},
+		{"/tos.html"},
+		{"/articles/"},
+		{"/articles/go_command.html"},
+		{"/articles/index.html"},
+		{"/articles/wiki/"},
+		{"/articles/wiki/edit.html"},
+		{"/articles/wiki/final-noclosure.go"},
+		{"/articles/wiki/final-noerror.go"},
+		{"/articles/wiki/final-parsetemplate.go"},
+		{"/articles/wiki/final-template.go"},
+		{"/articles/wiki/final.go"},
+		{"/articles/wiki/get.go"},
+		{"/articles/wiki/http-sample.go"},
+		{"/articles/wiki/index.html"},
+		{"/articles/wiki/Makefile"},
+		{"/articles/wiki/notemplate.go"},
+		{"/articles/wiki/part1-noerror.go"},
+		{"/articles/wiki/part1.go"},
+		{"/articles/wiki/part2.go"},
+		{"/articles/wiki/part3-errorhandling.go"},
+		{"/articles/wiki/part3.go"},
+		{"/articles/wiki/test.bash"},
+		{"/articles/wiki/test_edit.good"},
+		{"/articles/wiki/test_Test.txt.good"},
+		{"/articles/wiki/test_view.good"},
+		{"/articles/wiki/view.html"},
+		{"/codewalk/"},
+		{"/codewalk/codewalk.css"},
+		{"/codewalk/codewalk.js"},
+		{"/codewalk/codewalk.xml"},
+		{"/codewalk/functions.xml"},
+		{"/codewalk/markov.go"},
+		{"/codewalk/markov.xml"},
+		{"/codewalk/pig.go"},
+		{"/codewalk/popout.png"},
+		{"/codewalk/run"},
+		{"/codewalk/sharemem.xml"},
+		{"/codewalk/urlpoll.go"},
+		{"/devel/"},
+		{"/devel/release.html"},
+		{"/devel/weekly.html"},
+		{"/gopher/"},
+		{"/gopher/appenginegopher.jpg"},
+		{"/gopher/appenginegophercolor.jpg"},
+		{"/gopher/appenginelogo.gif"},
+		{"/gopher/bumper.png"},
+		{"/gopher/bumper192x108.png"},
+		{"/gopher/bumper320x180.png"},
+		{"/gopher/bumper480x270.png"},
+		{"/gopher/bumper640x360.png"},
+		{"/gopher/doc.png"},
+		{"/gopher/frontpage.png"},
+		{"/gopher/gopherbw.png"},
+		{"/gopher/gophercolor.png"},
+		{"/gopher/gophercolor16x16.png"},
+		{"/gopher/help.png"},
+		{"/gopher/pkg.png"},
+		{"/gopher/project.png"},
+		{"/gopher/ref.png"},
+		{"/gopher/run.png"},
+		{"/gopher/talks.png"},
+		{"/gopher/pencil/"},
+		{"/gopher/pencil/gopherhat.jpg"},
+		{"/gopher/pencil/gopherhelmet.jpg"},
+		{"/gopher/pencil/gophermega.jpg"},
+		{"/gopher/pencil/gopherrunning.jpg"},
+		{"/gopher/pencil/gopherswim.jpg"},
+		{"/gopher/pencil/gopherswrench.jpg"},
+		{"/play/"},
+		{"/play/fib.go"},
+		{"/play/hello.go"},
+		{"/play/life.go"},
+		{"/play/peano.go"},
+		{"/play/pi.go"},
+		{"/play/sieve.go"},
+		{"/play/solitaire.go"},
+		{"/play/tree.go"},
+		{"/progs/"},
+		{"/progs/cgo1.go"},
+		{"/progs/cgo2.go"},
+		{"/progs/cgo3.go"},
+		{"/progs/cgo4.go"},
+		{"/progs/defer.go"},
+		{"/progs/defer.out"},
+		{"/progs/defer2.go"},
+		{"/progs/defer2.out"},
+		{"/progs/eff_bytesize.go"},
+		{"/progs/eff_bytesize.out"},
+		{"/progs/eff_qr.go"},
+		{"/progs/eff_sequence.go"},
+		{"/progs/eff_sequence.out"},
+		{"/progs/eff_unused1.go"},
+		{"/progs/eff_unused2.go"},
+		{"/progs/error.go"},
+		{"/progs/error2.go"},
+		{"/progs/error3.go"},
+		{"/progs/error4.go"},
+		{"/progs/go1.go"},
+		{"/progs/gobs1.go"},
+		{"/progs/gobs2.go"},
+		{"/progs/image_draw.go"},
+		{"/progs/image_package1.go"},
+		{"/progs/image_package1.out"},
+		{"/progs/image_package2.go"},
+		{"/progs/image_package2.out"},
+		{"/progs/image_package3.go"},
+		{"/progs/image_package3.out"},
+		{"/progs/image_package4.go"},
+		{"/progs/image_package4.out"},
+		{"/progs/image_package5.go"},
+		{"/progs/image_package5.out"},
+		{"/progs/image_package6.go"},
+		{"/progs/image_package6.out"},
+		{"/progs/interface.go"},
+		{"/progs/interface2.go"},
+		{"/progs/interface2.out"},
+		{"/progs/json1.go"},
+		{"/progs/json2.go"},
+		{"/progs/json2.out"},
+		{"/progs/json3.go"},
+		{"/progs/json4.go"},
+		{"/progs/json5.go"},
+		{"/progs/run"},
+		{"/progs/slices.go"},
+		{"/progs/timeout1.go"},
+		{"/progs/timeout2.go"},
+		{"/progs/update.bash"},
 	}
 
 	for _, route := range static {
@@ -235,162 +234,162 @@ func BenchmarkTree_FindGithub(b *testing.B) {
 
 	static := []*Route{
 		// OAuth Authorizations
-		{"GET", "/authorizations"},
-		{"GET", "/authorizations/:id"},
-		//{"POST", "/authorizations"},
-		//{"PUT", "/authorizations/clients/:client_id"},
-		//{"PATCH", "/authorizations/:id"},
-		//{"DELETE", "/authorizations/:id"},
-		{"GET", "/applications/:client_id/tokens/:access_token"},
-		{"DELETE", "/applications/:client_id/tokens"},
-		//{"DELETE", "/applications/:client_id/tokens/:access_token"},
+		{"/authorizations"},
+		{"/authorizations/:id"},
+		//{"/authorizations"},
+		//{"/authorizations/clients/:client_id"},
+		//{"/authorizations/:id"},
+		//{"/authorizations/:id"},
+		{"/applications/:client_id/tokens/:access_token"},
+		{"/applications/:client_id/tokens"},
+		//{"/applications/:client_id/tokens/:access_token"},
 
 		// Activity
-		{"GET", "/events"},
-		{"GET", "/repos/:owner/:repo/events"},
-		{"GET", "/networks/:owner/:repo/events"},
-		{"GET", "/orgs/:org/events"},
-		{"GET", "/users/:user/received_events"},
-		{"GET", "/users/:user/received_events/public"},
-		{"GET", "/users/:user/events"},
-		{"GET", "/users/:user/events/public"},
-		{"GET", "/users/:user/events/orgs/:org"},
-		{"GET", "/feeds"},
-		//{"GET", "/notifications"},
-		{"GET", "/repos/:owner/:repo/notifications"},
-		{"PUT", "/notifications"},
-		//{"PUT", "/repos/:owner/:repo/notifications"},
-		{"GET", "/notifications/threads/:id"},
-		//{"PATCH", "/notifications/threads/:id"},
-		{"GET", "/notifications/threads/:id/subscription"},
-		//{"PUT", "/notifications/threads/:id/subscription"},
-		//{"DELETE", "/notifications/threads/:id/subscription"},
-		{"GET", "/repos/:owner/:repo/stargazers"},
-		{"GET", "/users/:user/starred"},
-		{"GET", "/user/starred"},
-		{"GET", "/user/starred/:owner/:repo"},
-		//{"PUT", "/user/starred/:owner/:repo"},
-		//{"DELETE", "/user/starred/:owner/:repo"},
-		{"GET", "/repos/:owner/:repo/subscribers"},
-		{"GET", "/users/:user/subscriptions"},
-		{"GET", "/user/subscriptions"},
-		{"GET", "/repos/:owner/:repo/subscription"},
-		//{"PUT", "/repos/:owner/:repo/subscription"},
-		//{"DELETE", "/repos/:owner/:repo/subscription"},
-		{"GET", "/user/subscriptions/:owner/:repo"},
+		{"/events"},
+		{"/repos/:owner/:repo/events"},
+		{"/networks/:owner/:repo/events"},
+		{"/orgs/:org/events"},
+		{"/users/:user/received_events"},
+		{"/users/:user/received_events/public"},
+		{"/users/:user/events"},
+		{"/users/:user/events/public"},
+		{"/users/:user/events/orgs/:org"},
+		{"/feeds"},
+		//{"/notifications"},
+		{"/repos/:owner/:repo/notifications"},
+		{"/notifications"},
+		//{"/repos/:owner/:repo/notifications"},
+		{"/notifications/threads/:id"},
+		//{"/notifications/threads/:id"},
+		{"/notifications/threads/:id/subscription"},
+		//{"/notifications/threads/:id/subscription"},
+		//{"/notifications/threads/:id/subscription"},
+		{"/repos/:owner/:repo/stargazers"},
+		{"/users/:user/starred"},
+		{"/user/starred"},
+		{"/user/starred/:owner/:repo"},
+		//{"/user/starred/:owner/:repo"},
+		//{"/user/starred/:owner/:repo"},
+		{"/repos/:owner/:repo/subscribers"},
+		{"/users/:user/subscriptions"},
+		{"/user/subscriptions"},
+		{"/repos/:owner/:repo/subscription"},
+		//{"/repos/:owner/:repo/subscription"},
+		//{"/repos/:owner/:repo/subscription"},
+		{"/user/subscriptions/:owner/:repo"},
 		//{"PUT", "/user/subscriptions/:owner/:repo"},
 		//{"DELETE", "/user/subscriptions/:owner/:repo"},
 
 		// Gists
-		{"GET", "/users/:user/gists"},
-		{"GET", "/gists"},
+		{"/users/:user/gists"},
+		{"/gists"},
 		//{"GET", "/gists/public"},
 		//{"GET", "/gists/starred"},
-		{"GET", "/gists/:id"},
+		{"/gists/:id"},
 		//{"POST", "/gists"},
 		//{"PATCH", "/gists/:id"},
-		{"PUT", "/gists/:id/star"},
+		{"/gists/:id/star"},
 		//{"DELETE", "/gists/:id/star"},
 		//{"GET", "/gists/:id/star"},
-		{"POST", "/gists/:id/forks"},
+		{"/gists/:id/forks"},
 		//{"DELETE", "/gists/:id"},
 
 		// Git Data
-		{"GET", "/repos/:owner/:repo/git/blobs/:sha"},
-		{"POST", "/repos/:owner/:repo/git/blobs"},
-		{"GET", "/repos/:owner/:repo/git/commits/:sha"},
-		{"POST", "/repos/:owner/:repo/git/commits"},
+		{"/repos/:owner/:repo/git/blobs/:sha"},
+		{"/repos/:owner/:repo/git/blobs"},
+		{"/repos/:owner/:repo/git/commits/:sha"},
+		{"/repos/:owner/:repo/git/commits"},
 		//{"GET", "/repos/:owner/:repo/git/refs/*ref"},
-		{"GET", "/repos/:owner/:repo/git/refs"},
+		{"/repos/:owner/:repo/git/refs"},
 		//{"POST", "/repos/:owner/:repo/git/refs"},
 		//{"PATCH", "/repos/:owner/:repo/git/refs/*ref"},
 		//{"DELETE", "/repos/:owner/:repo/git/refs/*ref"},
-		{"GET", "/repos/:owner/:repo/git/tags/:sha"},
-		{"POST", "/repos/:owner/:repo/git/tags"},
-		{"GET", "/repos/:owner/:repo/git/trees/:sha"},
-		{"POST", "/repos/:owner/:repo/git/trees"},
-
-		{"GET", "/issues"},
-		{"GET", "/user/issues"},
-		{"GET", "/orgs/:org/issues"},
-		{"GET", "/repos/:owner/:repo/issues"},
-		{"GET", "/repos/:owner/:repo/issues/:number"},
+		{"/repos/:owner/:repo/git/tags/:sha"},
+		{"/repos/:owner/:repo/git/tags"},
+		{"/repos/:owner/:repo/git/trees/:sha"},
+		{"/repos/:owner/:repo/git/trees"},
+
+		{"/issues"},
+		{"/user/issues"},
+		{"/orgs/:org/issues"},
+		{"/repos/:owner/:repo/issues"},
+		{"/repos/:owner/:repo/issues/:number"},
 		//{"POST", "/repos/:owner/:repo/issues"},
 		//{"PATCH", "/repos/:owner/:repo/issues/:number"},
-		{"GET", "/repos/:owner/:repo/assignees"},
-		{"GET", "/repos/:owner/:repo/assignees/:assignee"},
-		{"GET", "/repos/:owner/:repo/issues/:number/comments"},
+		{"/repos/:owner/:repo/assignees"},
+		{"/repos/:owner/:repo/assignees/:assignee"},
+		{"/repos/:owner/:repo/issues/:number/comments"},
 		//{"GET", "/repos/:owner/:repo/issues/comments"},
 		//{"GET", "/repos/:owner/:repo/issues/comments/:id"},
 		//{"POST", "/repos/:owner/:repo/issues/:number/comments"},
 		//{"PATCH", "/repos/:owner/:repo/issues/comments/:id"},
 		//{"DELETE", "/repos/:owner/:repo/issues/comments/:id"},
-		{"GET", "/repos/:owner/:repo/issues/:number/events"},
+		{"/repos/:owner/:repo/issues/:number/events"},
 		//{"GET", "/repos/:owner/:repo/issues/events"},
 		//{"GET", "/repos/:owner/:repo/issues/events/:id"},
-		{"GET", "/repos/:owner/:repo/labels"},
-		{"GET", "/repos/:owner/:repo/labels/:name"},
+		{"/repos/:owner/:repo/labels"},
+		{"/repos/:owner/:repo/labels/:name"},
 		//{"POST", "/repos/:owner/:repo/labels"},
 		//{"PATCH", "/repos/:owner/:repo/labels/:name"},
 		//{"DELETE", "/repos/:owner/:repo/labels/:name"},
-		{"GET", "/repos/:owner/:repo/issues/:number/labels"},
+		{"/repos/:owner/:repo/issues/:number/labels"},
 		//{"POST", "/repos/:owner/:repo/issues/:number/labels"},
 		//{"DELETE", "/repos/:owner/:repo/issues/:number/labels/:name"},
 		//{"PUT", "/repos/:owner/:repo/issues/:number/labels"},
 		//{"DELETE", "/repos/:owner/:repo/issues/:number/labels"},
-		{"GET", "/repos/:owner/:repo/milestones/:number/labels"},
-		{"GET", "/repos/:owner/:repo/milestones"},
-		{"GET", "/repos/:owner/:repo/milestones/:number"},
+		{"/repos/:owner/:repo/milestones/:number/labels"},
+		{"/repos/:owner/:repo/milestones"},
+		{"/repos/:owner/:repo/milestones/:number"},
 		//{"POST", "/repos/:owner/:repo/milestones"},
 		//{"PATCH", "/repos/:owner/:repo/milestones/:number"},
 		//{"DELETE", "/repos/:owner/:repo/milestones/:number"},
 
 		// Miscellaneous
-		{"GET", "/emojis"},
-		{"GET", "/gitignore/templates"},
-		{"GET", "/gitignore/templates/:name"},
-		{"POST", "/markdown"},
-		{"POST", "/markdown/raw"},
-		{"GET", "/meta"},
-		{"GET", "/rate_limit"},
+		{"/emojis"},
+		{"/gitignore/templates"},
+		{"/gitignore/templates/:name"},
+		{"/markdown"},
+		{"/markdown/raw"},
+		{"/meta"},
+		{"/rate_limit"},
 
 		// Organizations
-		{"GET", "/users/:user/orgs"},
-		{"GET", "/user/orgs"},
-		{"GET", "/orgs/:org"},
+		{"/users/:user/orgs"},
+		{"/user/orgs"},
+		{"/orgs/:org"},
 		//{"PATCH", "/orgs/:org"},
-		{"GET", "/orgs/:org/members"},
-		{"GET", "/orgs/:org/members/:user"},
+		{"/orgs/:org/members"},
+		{"/orgs/:org/members/:user"},
 		//{"DELETE", "/orgs/:org/members/:user"},
-		{"GET", "/orgs/:org/public_members"},
-		{"GET", "/orgs/:org/public_members/:user"},
+		{"/orgs/:org/public_members"},
+		{"/orgs/:org/public_members/:user"},
 		//{"PUT", "/orgs/:org/public_members/:user"},
 		//{"DELETE", "/orgs/:org/public_members/:user"},
-		{"GET", "/orgs/:org/teams"},
-		{"GET", "/teams/:id"},
+		{"/orgs/:org/teams"},
+		{"/teams/:id"},
 		//{"POST", "/orgs/:org/teams"},
 		//{"PATCH", "/teams/:id"},
 		//{"DELETE", "/teams/:id"},
-		{"GET", "/teams/:id/members"},
-		{"GET", "/teams/:id/members/:user"},
+		{"/teams/:id/members"},
+		{"/teams/:id/members/:user"},
 		//{"PUT", "/teams/:id/members/:user"},
 		//{"DELETE", "/teams/:id/members/:user"},
-		{"GET", "/teams/:id/repos"},
-		{"GET", "/teams/:id/repos/:owner/:repo"},
+		{"/teams/:id/repos"},
+		{"/teams/:id/repos/:owner/:repo"},
 		//{"PUT", "/teams/:id/repos/:owner/:repo"},
 		//{"DELETE", "/teams/:id/repos/:owner/:repo"},
-		{"GET", "/user/teams"},
+		{"/user/teams"},
 
 		// Pull Requests
-		{"GET", "/repos/:owner/:repo/pulls"},
-		{"GET", "/repos/:owner/:repo/pulls/:number"},
+		{"/repos/:owner/:repo/pulls"},
+		{"/repos/:owner/:repo/pulls/:number"},
 		//{"POST", "/repos/:owner/:repo/pulls"},
 		//{"PATCH", "/repos/:owner/:repo/pulls/:number"},
-		{"GET", "/repos/:owner/:repo/pulls/:number/commits"},
-		{"GET", "/repos/:owner/:repo/pulls/:number/files"},
-		{"GET", "/repos/:owner/:repo/pulls/:number/merge"},
+		{"/repos/:owner/:repo/pulls/:number/commits"},
+		{"/repos/:owner/:repo/pulls/:number/files"},
+		{"/repos/:owner/:repo/pulls/:number/merge"},
 		//{"PUT", "/repos/:owner/:repo/pulls/:number/merge"},
-		{"GET", "/repos/:owner/:repo/pulls/:number/comments"},
+		{"/repos/:owner/:repo/pulls/:number/comments"},
 		//{"GET", "/repos/:owner/:repo/pulls/comments"},
 		//{"GET", "/repos/:owner/:repo/pulls/comments/:number"},
 		//{"PUT", "/repos/:owner/:repo/pulls/:number/comments"},
@@ -398,98 +397,98 @@ func BenchmarkTree_FindGithub(b *testing.B) {
 		//{"DELETE", "/repos/:owner/:repo/pulls/comments/:number"},
 
 		// Repositories
-		{"GET", "/user/repos"},
-		{"GET", "/users/:user/repos"},
-		{"GET", "/orgs/:org/repos"},
-		{"GET", "/repositories"},
+		{"/user/repos"},
+		{"/users/:user/repos"},
+		{"/orgs/:org/repos"},
+		{"/repositories"},
 		//{"POST", "/user/repos"},
 		//{"POST", "/orgs/:org/repos"},
-		{"GET", "/repos/:owner/:repo"},
+		{"/repos/:owner/:repo"},
 		//{"PATCH", "/repos/:owner/:repo"},
-		{"GET", "/repos/:owner/:repo/contributors"},
-		{"GET", "/repos/:owner/:repo/languages"},
-		{"GET", "/repos/:owner/:repo/teams"},
-		{"GET", "/repos/:owner/:repo/tags"},
-		{"GET", "/repos/:owner/:repo/branches"},
-		{"GET", "/repos/:owner/:repo/branches/:branch"},
+		{"/repos/:owner/:repo/contributors"},
+		{"/repos/:owner/:repo/languages"},
+		{"/repos/:owner/:repo/teams"},
+		{"/repos/:owner/:repo/tags"},
+		{"/repos/:owner/:repo/branches"},
+		{"/repos/:owner/:repo/branches/:branch"},
 		//{"DELETE", "/repos/:owner/:repo"},
-		{"GET", "/repos/:owner/:repo/collaborators"},
-		{"GET", "/repos/:owner/:repo/collaborators/:user"},
+		{"/repos/:owner/:repo/collaborators"},
+		{"/repos/:owner/:repo/collaborators/:user"},
 		//{"PUT", "/repos/:owner/:repo/collaborators/:user"},
 		//{"DELETE", "/repos/:owner/:repo/collaborators/:user"},
-		{"GET", "/repos/:owner/:repo/comments"},
-		{"GET", "/repos/:owner/:repo/commits/:sha/comments"},
+		{"/repos/:owner/:repo/comments"},
+		{"/repos/:owner/:repo/commits/:sha/comments"},
 		//{"POST", "/repos/:owner/:repo/commits/:sha/comments"},
-		{"GET", "/repos/:owner/:repo/comments/:id"},
+		{"/repos/:owner/:repo/comments/:id"},
 		//{"PATCH", "/repos/:owner/:repo/comments/:id"},
 		//{"DELETE", "/repos/:owner/:repo/comments/:id"},
-		{"GET", "/repos/:owner/:repo/commits"},
-		{"GET", "/repos/:owner/:repo/commits/:sha"},
-		{"GET", "/repos/:owner/:repo/readme"},
+		{"/repos/:owner/:repo/commits"},
+		{"/repos/:owner/:repo/commits/:sha"},
+		{"/repos/:owner/:repo/readme"},
 		//{"GET", "/repos/:owner/:repo/contents/*path"},
 		//{"PUT", "/repos/:owner/:repo/contents/*path"},
 		//{"DELETE", "/repos/:owner/:repo/contents/*path"},
 		//{"GET", "/repos/:owner/:repo/:archive_format/:ref"},
-		{"GET", "/repos/:owner/:repo/keys"},
-		{"GET", "/repos/:owner/:repo/keys/:id"},
+		{"/repos/:owner/:repo/keys"},
+		{"/repos/:owner/:repo/keys/:id"},
 		//{"POST", "/repos/:owner/:repo/keys"},
 		//{"PATCH", "/repos/:owner/:repo/keys/:id"},
 		//{"DELETE", "/repos/:owner/:repo/keys/:id"},
-		{"GET", "/repos/:owner/:repo/downloads"},
-		{"GET", "/repos/:owner/:repo/downloads/:id"},
+		{"/repos/:owner/:repo/downloads"},
+		{"/repos/:owner/:repo/downloads/:id"},
 		//{"DELETE", "/repos/:owner/:repo/downloads/:id"},
-		{"GET", "/repos/:owner/:repo/forks"},
+		{"/repos/:owner/:repo/forks"},
 		//{"POST", "/repos/:owner/:repo/forks"},
-		{"GET", "/repos/:owner/:repo/hooks"},
-		{"GET", "/repos/:owner/:repo/hooks/:id"},
+		{"/repos/:owner/:repo/hooks"},
+		{"/repos/:owner/:repo/hooks/:id"},
 		//{"POST", "/repos/:owner/:repo/hooks"},
 		//{"PATCH", "/repos/:owner/:repo/hooks/:id"},
 		//{"POST", "/repos/:owner/:repo/hooks/:id/tests"},
 		//{"DELETE", "/repos/:owner/:repo/hooks/:id"},
 		//{"POST", "/repos/:owner/:repo/merges"},
-		{"GET", "/repos/:owner/:repo/releases"},
-		{"GET", "/repos/:owner/:repo/releases/:id"},
+		{"/repos/:owner/:repo/releases"},
+		{"/repos/:owner/:repo/releases/:id"},
 		//{"POST", "/repos/:owner/:repo/releases"},
 		//{"PATCH", "/repos/:owner/:repo/releases/:id"},
 		//{"DELETE", "/repos/:owner/:repo/releases/:id"},
-		{"GET", "/repos/:owner/:repo/releases/:id/assets"},
-		{"GET", "/repos/:owner/:repo/stats/contributors"},
-		{"GET", "/repos/:owner/:repo/stats/commit_activity"},
-		{"GET", "/repos/:owner/:repo/stats/code_frequency"},
-		{"GET", "/repos/:owner/:repo/stats/participation"},
-		{"GET", "/repos/:owner/:repo/stats/punch_card"},
-		{"GET", "/repos/:owner/:repo/statuses/:ref"},
+		{"/repos/:owner/:repo/releases/:id/assets"},
+		{"/repos/:owner/:repo/stats/contributors"},
+		{"/repos/:owner/:repo/stats/commit_activity"},
+		{"/repos/:owner/:repo/stats/code_frequency"},
+		{"/repos/:owner/:repo/stats/participation"},
+		{"/repos/:owner/:repo/stats/punch_card"},
+		{"/repos/:owner/:repo/statuses/:ref"},
 		//{"POST", "/repos/:owner/:repo/statuses/:ref"},
 
 		// Search
-		{"GET", "/search/repositories"},
-		{"GET", "/search/code"},
-		{"GET", "/search/issues"},
-		{"GET", "/search/users"},
-		{"GET", "/legacy/issues/search/:owner/:repository/:state/:keyword"},
-		{"GET", "/legacy/repos/search/:keyword"},
-		{"GET", "/legacy/user/search/:keyword"},
-		{"GET", "/legacy/user/email/:email"},
+		{"/search/repositories"},
+		{"/search/code"},
+		{"/search/issues"},
+		{"/search/users"},
+		{"/legacy/issues/search/:owner/:repository/:state/:keyword"},
+		{"/legacy/repos/search/:keyword"},
+		{"/legacy/user/search/:keyword"},
+		{"/legacy/user/email/:email"},
 
 		// Users
-		{"GET", "/users/:user"},
-		{"GET", "/user"},
+		{"/users/:user"},
+		{"/user"},
 		//{"PATCH", "/user"},
-		{"GET", "/users"},
-		{"GET", "/user/emails"},
+		{"/users"},
+		{"/user/emails"},
 		//{"POST", "/user/emails"},
 		//{"DELETE", "/user/emails"},
-		{"GET", "/users/:user/followers"},
-		{"GET", "/user/followers"},
-		{"GET", "/users/:user/following"},
-		{"GET", "/user/following"},
-		{"GET", "/user/following/:user"},
-		{"GET", "/users/:user/following/:target_user"},
+		{"/users/:user/followers"},
+		{"/user/followers"},
+		{"/users/:user/following"},
+		{"/user/following"},
+		{"/user/following/:user"},
+		{"/users/:user/following/:target_user"},
 		//{"PUT", "/user/following/:user"},
 		//{"DELETE", "/user/following/:user"},
-		{"GET", "/users/:user/keys"},
-		{"GET", "/user/keys"},
-		{"GET", "/user/keys/:id"},
+		{"/users/:user/keys"},
+		{"/user/keys"},
+		{"/user/keys/:id"},
 		//{"POST", "/user/keys"},
 		//{"PATCH", "/user/keys/:id"},
 		//{"DELETE", "/user/keys/:id"},