Repository: cloudwego/hertz
Pull request #585 (2023-02-02 08:52:58)

fix: skip rest chunked body correctly
#### What type of PR is this?
<!--
Add one of the following kinds:

build: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
ci: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
docs: Documentation only changes
feat: A new feature
optimize: A new optimization
fix: A bug fix
perf: A code change that improves performance
refactor: A code change that neither fixes a bug nor adds a feature
style: Changes that do not affect the meaning of the code (white space, formatting, missing semi-colons, etc)
test: Adding missing tests or correcting existing tests
chore: Changes to the build process or auxiliary tools and libraries such as documentation generation
-->
fix
#### Check the PR title.
<!--
The description of the title will be attached in Release Notes, 
so please describe it from user-oriented, what this PR does / why we need it.
Please check your PR title with the below requirements:
-->
- [x] This PR title match the format: \<type\>(optional scope): \<description\>
- [x] The description of this PR title is user-oriented and clear enough for others to understand.


#### (Optional) Translate the PR title into Chinese.
正确的忽略剩余的chunked body

#### (Optional) More detail description for this PR(en: English/zh: Chinese).
<!--
Provide more detailed info for review. If it is a perf type PR, perf data is suggested to give.
-->
en: When the request's `Transfor-Encoding` is chunked, the server has the WithStreamBody option and the handler returns without processing body, hertz will treat the unprocessed body as a new http request.

zh(optional): 当 request 的 `Transfor-Encoding` 为 chunked，且 server 打开了 `WithStreamBody` 选项，如果 handler 没有处理 body 而直接返回，hertz 会将没有处理的 body 作为新的 http 请求。

#### Which issue(s) this PR fixes:
<!--
Automatically closes linked issue when PR is merged.
Eg: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
-->


codecov[bot]: # [Codecov](https://codecov.io/gh/cloudwego/hertz/pull/585?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego) Report
Base: **69.87**% // Head: **71.22**% // Increases project coverage by **`+1.35%`** :tada:
> Coverage data is based on head [(`8f0d8c2`)](https://codecov.io/gh/cloudwego/hertz/pull/585?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego) compared to base [(`e0c6299`)](https://codecov.io/gh/cloudwego/hertz/commit/e0c6299d98b53d43a5f8c970c348d8e9c09fe4a5?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego).
> Patch coverage: 0.00% of modified lines in pull request are covered.

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


```diff
@@             Coverage Diff             @@
##           develop     #585      +/-   ##
===========================================
+ Coverage    69.87%   71.22%   +1.35%     
===========================================
  Files           93       93              
  Lines         8887     9026     +139     
===========================================
+ Hits          6210     6429     +219     
+ Misses        2307     2207     -100     
- Partials       370      390      +20     
```


| [Impacted Files](https://codecov.io/gh/cloudwego/hertz/pull/585?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego) | Coverage Δ | |
|---|---|---|
| [pkg/protocol/http1/ext/stream.go](https://codecov.io/gh/cloudwego/hertz/pull/585?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego#diff-cGtnL3Byb3RvY29sL2h0dHAxL2V4dC9zdHJlYW0uZ28=) | `0.00% <0.00%> (ø)` | |
| [pkg/app/server/render/html.go](https://codecov.io/gh/cloudwego/hertz/pull/585?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego#diff-cGtnL2FwcC9zZXJ2ZXIvcmVuZGVyL2h0bWwuZ28=) | `34.78% <0.00%> (-2.72%)` | :arrow_down: |
| [pkg/app/client/option.go](https://codecov.io/gh/cloudwego/hertz/pull/585?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego#diff-cGtnL2FwcC9jbGllbnQvb3B0aW9uLmdv) | `96.70% <0.00%> (ø)` | |
| [pkg/network/netpoll/dial.go](https://codecov.io/gh/cloudwego/hertz/pull/585?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego#diff-cGtnL25ldHdvcmsvbmV0cG9sbC9kaWFsLmdv) | `0.00% <0.00%> (ø)` | |
| [pkg/network/netpoll/transport.go](https://codecov.io/gh/cloudwego/hertz/pull/585?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego#diff-cGtnL25ldHdvcmsvbmV0cG9sbC90cmFuc3BvcnQuZ28=) | `0.00% <0.00%> (ø)` | |
| [pkg/network/netpoll/connection.go](https://codecov.io/gh/cloudwego/hertz/pull/585?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego#diff-cGtnL25ldHdvcmsvbmV0cG9sbC9jb25uZWN0aW9uLmdv) | `73.33% <0.00%> (ø)` | |
| [pkg/protocol/args.go](https://codecov.io/gh/cloudwego/hertz/pull/585?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego#diff-cGtnL3Byb3RvY29sL2FyZ3MuZ28=) | `81.86% <0.00%> (+0.54%)` | :arrow_up: |
| [pkg/protocol/header.go](https://codecov.io/gh/cloudwego/hertz/pull/585?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego#diff-cGtnL3Byb3RvY29sL2hlYWRlci5nbw==) | `76.55% <0.00%> (+0.95%)` | :arrow_up: |
| [pkg/protocol/request.go](https://codecov.io/gh/cloudwego/hertz/pull/585?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego#diff-cGtnL3Byb3RvY29sL3JlcXVlc3QuZ28=) | `84.29% <0.00%> (+1.04%)` | :arrow_up: |
| [pkg/protocol/response.go](https://codecov.io/gh/cloudwego/hertz/pull/585?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego#diff-cGtnL3Byb3RvY29sL3Jlc3BvbnNlLmdv) | `84.10% <0.00%> (+1.32%)` | :arrow_up: |
| ... and [3 more](https://codecov.io/gh/cloudwego/hertz/pull/585?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego) | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego)

</details>

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

welkeyever: LGTM, perhaps adding some ut for this part?
wzekin: > LGTM, perhaps adding some ut for this part?

ok~

Patch:
diff --git a/cmd/hz/app/app.go b/cmd/hz/app/app.go
index a3ba38b..2069d55 100644
--- a/cmd/hz/app/app.go
+++ b/cmd/hz/app/app.go
@@ -53,6 +53,13 @@ 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)
+	}
 	return nil
 }
 
@@ -62,25 +69,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,8 +149,9 @@ 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}
 	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}
 
@@ -186,6 +197,7 @@ func Init() *cli.App {
 				&outDirFlag,
 				&handlerDirFlag,
 				&modelDirFlag,
+				&routerDirFlag,
 				&clientDirFlag,
 
 				&includesFlag,
@@ -308,6 +320,9 @@ 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,
 	}
 
 	if args.CustomizeLayout == "" {
diff --git a/cmd/hz/config/argument.go b/cmd/hz/config/argument.go
index bf15fdf..253b2e2 100644
--- a/cmd/hz/config/argument.go
+++ b/cmd/hz/config/argument.go
@@ -24,6 +24,7 @@ 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"
 )
 
@@ -115,6 +116,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,7 +185,7 @@ func (arg *Argument) checkIDL() error {
 	return nil
 }
 
-func (arg Argument) IsUpdate() bool {
+func (arg *Argument) IsUpdate() bool {
 	return arg.CmdType == meta.CmdUpdate
 }
 
@@ -330,3 +346,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/handler.go b/cmd/hz/generator/handler.go
index c05793e..aa76979 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"
 
@@ -143,7 +143,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..2db26b2 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,14 @@ import (
 
 // Layout contains the basic information of idl
 type Layout struct {
+	OutDir          string
 	GoModule        string
 	ServiceName     string
 	UseApacheThrift bool
 	HasIdl          bool
+	ModelDir        string
+	HandlerDir      string
+	RouterDir       string
 }
 
 // LayoutGenerator contains the information generated by generating the layout template
@@ -56,7 +61,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 +101,25 @@ 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
+		}
+	}
+
 	sd, err := serviceToLayoutData(service)
 	if err != nil {
 		return err
@@ -106,7 +130,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{}{
@@ -134,10 +163,18 @@ func serviceToLayoutData(service Layout) (map[string]interface{}, error) {
 
 // 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 072c4c3..6d88bac 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,13 +122,6 @@ _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.
diff --git a/cmd/hz/generator/package.go b/cmd/hz/generator/package.go
index 54ecd41..a0f44d4 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"
@@ -73,7 +73,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/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/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/pkg/app/context.go b/pkg/app/context.go
index 157674c..e86cc4b 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
 	}
+}
 
-	if ip, _, err := net.SplitHostPort(strings.TrimSpace(ctx.RemoteAddr().String())); err == nil {
-		return ip
+// 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]
+}
+
+// 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) {
diff --git a/pkg/app/fs.go b/pkg/app/fs.go
index 9cd6ba4..72aad87 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"
@@ -1076,7 +1077,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
 	}
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/protocol/http1/ext/common.go b/pkg/protocol/http1/ext/common.go
index 7d8498c..94526b7 100644
--- a/pkg/protocol/http1/ext/common.go
+++ b/pkg/protocol/http1/ext/common.go
@@ -387,6 +387,70 @@ 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 {
diff --git a/pkg/protocol/http1/ext/stream.go b/pkg/protocol/http1/ext/stream.go
index 2dd203d..a09e1e3 100644
--- a/pkg/protocol/http1/ext/stream.go
+++ b/pkg/protocol/http1/ext/stream.go
@@ -46,6 +46,7 @@ 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"
@@ -73,6 +74,8 @@ type bodyStream struct {
 	offset          int
 	contentLength   int
 	chunkLeft       int
+	// whether the chunk has reached the EOF
+	chunkEOF bool
 }
 
 func ReadBodyWithStreaming(zr network.Reader, contentLength, maxBodySize int, dst []byte) (b []byte, err error) {
@@ -114,6 +117,7 @@ func AcquireBodyStream(b *bytebufferpool.ByteBuffer, r network.Reader, t *protoc
 	rs.reader = r
 	rs.contentLength = contentLength
 	rs.trailer = t
+	rs.chunkEOF = false
 
 	return rs
 }
@@ -125,6 +129,10 @@ 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 {
@@ -133,6 +141,7 @@ func (rs *bodyStream) Read(p []byte) (int, error) {
 			if chunkSize == 0 {
 				err = ReadTrailer(rs.trailer, rs.reader)
 				if err == nil {
+					rs.chunkEOF = true
 					err = io.EOF
 				}
 				return 0, err
@@ -226,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 {


Test patch:
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 fabc6eb..f5c43d7 100644
--- a/cmd/hz/thrift/plugin_test.go
+++ b/cmd/hz/thrift/plugin_test.go
@@ -17,7 +17,7 @@
 package thrift
 
 import (
-	"os"
+	"io/ioutil"
 	"testing"
 
 	"github.com/cloudwego/hertz/cmd/hz/generator"
@@ -27,7 +27,7 @@ import (
 )
 
 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)
 	}
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/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/protocol/http1/client_test.go b/pkg/protocol/http1/client_test.go
index d8d915c..f61e491 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"
@@ -187,7 +187,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/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 9f6a537..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"
@@ -659,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)
 	}
@@ -1123,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)
 	}
@@ -1204,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))
 	}
@@ -1234,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))
 	}
@@ -1257,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))
 	}
@@ -1329,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)
 	}
diff --git a/pkg/protocol/http1/resp/response_test.go b/pkg/protocol/http1/resp/response_test.go
index d1abfe3..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"
 
@@ -704,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)
 	}
@@ -723,7 +724,7 @@ func testResponseReadBodyStreamBadTrailer(t *testing.T, resp *protocol.Response,
 	}
 	assert.True(t, resp.IsBodyStream())
 
-	_, err = io.ReadAll(resp.BodyStream())
+	_, err = ioutil.ReadAll(resp.BodyStream())
 	if err == nil || err == io.EOF {
 		t.Fatalf("expected error when reading response.")
 	}
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/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)
 	}