Repository: cloudwego/hertz
Pull request #554 (2023-01-18 08:47:46)

chore: release v0.5.1


Patch:
diff --git a/cmd/hz/app/app.go b/cmd/hz/app/app.go
index a66f624..8625de4 100644
--- a/cmd/hz/app/app.go
+++ b/cmd/hz/app/app.go
@@ -141,6 +141,7 @@ func Init() *cli.App {
 	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}
+	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}
 
 	optPkgFlag := cli.StringSliceFlag{Name: "option_package", Aliases: []string{"P"}, Usage: "Specify the package path. ({include_path}={import_path})"}
@@ -262,6 +263,7 @@ func Init() *cli.App {
 			Flags: []cli.Flag{
 				&idlFlag,
 				&moduleFlag,
+				&baseDomainFlag,
 				&modelDirFlag,
 				&clientDirFlag,
 
diff --git a/cmd/hz/config/argument.go b/cmd/hz/config/argument.go
index d4250ed..bf15fdf 100644
--- a/cmd/hz/config/argument.go
+++ b/cmd/hz/config/argument.go
@@ -37,6 +37,7 @@ type Argument struct {
 	ModelDir   string // model path
 	RouterDir  string // router path
 	ClientDir  string // client path
+	BaseDomain string // request domain
 
 	IdlType   string   // idl type
 	IdlPaths  []string // master idl path
diff --git a/cmd/hz/generator/client.go b/cmd/hz/generator/client.go
index 37b2cac..bbc3b5c 100644
--- a/cmd/hz/generator/client.go
+++ b/cmd/hz/generator/client.go
@@ -35,6 +35,7 @@ type ClientMethod struct {
 
 type ClientFile struct {
 	FilePath      string
+	PackageName   string
 	ServiceName   string
 	BaseDomain    string
 	Imports       map[string]*model.Model
@@ -49,17 +50,22 @@ func (pkgGen *HttpPackageGenerator) genClient(pkg *HttpPackage, clientDir string
 		if err != nil {
 			return err
 		}
-		if !isExist {
-			err := pkgGen.TemplateGenerator.Generate(nil, hertzClientTplName, hertzClientPath, false)
-			if err != nil {
-				return err
-			}
+		baseDomain := s.BaseDomain
+		if len(pkgGen.BaseDomain) != 0 {
+			baseDomain = pkgGen.BaseDomain
 		}
 		client := ClientFile{
 			FilePath:      filepath.Join(cliDir, util.ToSnakeCase(s.Name)+".go"),
+			PackageName:   util.ToSnakeCase(s.Name),
 			ServiceName:   util.ToCamelCase(s.Name),
 			ClientMethods: s.ClientMethods,
-			BaseDomain:    s.BaseDomain,
+			BaseDomain:    baseDomain,
+		}
+		if !isExist {
+			err := pkgGen.TemplateGenerator.Generate(client, hertzClientTplName, hertzClientPath, false)
+			if err != nil {
+				return err
+			}
 		}
 		client.Imports = make(map[string]*model.Model, len(client.ClientMethods))
 		for _, m := range client.ClientMethods {
@@ -72,7 +78,7 @@ func (pkgGen *HttpPackageGenerator) genClient(pkg *HttpPackage, clientDir string
 				client.Imports[mm.PackageName] = mm
 			}
 		}
-		err = pkgGen.TemplateGenerator.Generate(client, serviceClientName, client.FilePath, false)
+		err = pkgGen.TemplateGenerator.Generate(client, idlClientName, client.FilePath, false)
 		if err != nil {
 			return err
 		}
diff --git a/cmd/hz/generator/custom_files.go b/cmd/hz/generator/custom_files.go
index 685ddb3..38851a3 100644
--- a/cmd/hz/generator/custom_files.go
+++ b/cmd/hz/generator/custom_files.go
@@ -19,120 +19,565 @@ package generator
 import (
 	"bytes"
 	"fmt"
+	"io/ioutil"
+	"path/filepath"
+	"strings"
 	"text/template"
 
-	"github.com/cloudwego/hertz/cmd/hz/generator/model"
 	"github.com/cloudwego/hertz/cmd/hz/util"
+	"github.com/cloudwego/hertz/cmd/hz/util/logs"
 )
 
-type CustomTplRenderInfo struct {
+type FilePathRenderInfo struct {
+	MasterIDLName  string // master IDL name
+	GenPackage     string // master IDL generate code package
+	HandlerDir     string // handler generate dir
+	ModelDir       string // model generate dir
+	RouterDir      string // router generate dir
+	ProjectDir     string // projectDir
+	GoModule       string // go module
+	ServiceName    string // service name, changed as services are traversed
+	MethodName     string // method name, changed as methods are traversed
+	HandlerGenPath string // "api.gen_path" value
+}
+
+type IDLPackageRenderInfo struct {
+	FilePathRenderInfo
+	ServiceInfos *HttpPackage
+}
+
+type CustomizedFileForMethod struct {
+	*HttpMethod
+	FilePath    string
+	FilePackage string
+	ServiceInfo *Service // service info for that method
+}
+
+type CustomizedFileForService struct {
+	*Service
+	FilePath       string
+	FilePackage    string
+	IDLPackageInfo *IDLPackageRenderInfo // IDL info for that service
+}
+
+type CustomizedFileForIDL struct {
+	*IDLPackageRenderInfo
 	FilePath    string
-	Service     string
-	Module      string
-	PackagePath string
-	PackageName string
-	Methods     []*HttpMethod
-	Imports     map[string]*model.Model
-	TPL         *template.Template
+	FilePackage string
+}
+
+// todo: 1. how to import other file, if the other file name is a template
+
+// genCustomizedFile generate customized file template
+func (pkgGen *HttpPackageGenerator) genCustomizedFile(pkg *HttpPackage) error {
+	filePathRenderInfo := FilePathRenderInfo{
+		MasterIDLName: pkg.IdlName,
+		GenPackage:    pkg.Package,
+		HandlerDir:    pkgGen.HandlerDir,
+		ModelDir:      pkgGen.ModelDir,
+		RouterDir:     pkgGen.RouterDir,
+		ProjectDir:    pkgGen.OutputDir,
+		GoModule:      pkgGen.ProjPackage,
+		// methodName & serviceName will change as traverse
+	}
+
+	idlPackageRenderInfo := IDLPackageRenderInfo{
+		FilePathRenderInfo: filePathRenderInfo,
+		ServiceInfos:       pkg,
+	}
+
+	for _, tplInfo := range pkgGen.tplsInfo {
+		// the default template has been automatically generated by the tool, so skip
+		if tplInfo.Default {
+			continue
+		}
+
+		// loop generate file
+		if tplInfo.LoopService || tplInfo.LoopMethod {
+			loopMethod := tplInfo.LoopMethod
+			loopService := tplInfo.LoopService
+			if loopService && !loopMethod { // only loop service
+				for _, service := range idlPackageRenderInfo.ServiceInfos.Services {
+					filePathRenderInfo.ServiceName = service.Name
+					err := pkgGen.genLoopService(tplInfo, filePathRenderInfo, service, &idlPackageRenderInfo)
+					if err != nil {
+						return err
+					}
+				}
+			} else { // loop service & method, because if loop method, the service must be looped
+				for _, service := range idlPackageRenderInfo.ServiceInfos.Services {
+					for _, method := range service.Methods {
+						filePathRenderInfo.ServiceName = service.Name
+						filePathRenderInfo.MethodName = method.Name
+						filePathRenderInfo.HandlerGenPath = method.OutputDir
+						err := pkgGen.genLoopMethod(tplInfo, filePathRenderInfo, method, service)
+						if err != nil {
+							return err
+						}
+					}
+				}
+			}
+		} else { // generate customized file single
+			err := pkgGen.genSingleCustomizedFile(tplInfo, filePathRenderInfo, idlPackageRenderInfo)
+			if err != nil {
+				return err
+			}
+		}
+	}
+	return nil
+}
+
+// renderFilePath used to render file path template to get real file path
+func renderFilePath(tplInfo *Template, filePathRenderInfo FilePathRenderInfo) (string, error) {
+	tpl, err := template.New(tplInfo.Path).Funcs(funcMap).Parse(tplInfo.Path)
+	if err != nil {
+		return "", fmt.Errorf("parse file path template(%s) failed, err: %v", tplInfo.Path, err)
+	}
+	filePath := bytes.NewBuffer(nil)
+	err = tpl.Execute(filePath, filePathRenderInfo)
+	if err != nil {
+		return "", fmt.Errorf("render file path template(%s) failed, err: %v", tplInfo.Path, err)
+	}
+
+	return filePath.String(), nil
+}
+
+func renderInsertKey(tplInfo *Template, data interface{}) (string, error) {
+	tpl, err := template.New(tplInfo.UpdateBehavior.InsertKey).Funcs(funcMap).Parse(tplInfo.UpdateBehavior.InsertKey)
+	if err != nil {
+		return "", fmt.Errorf("parse insert key template(%s) failed, err: %v", tplInfo.UpdateBehavior.InsertKey, err)
+	}
+	insertKey := bytes.NewBuffer(nil)
+	err = tpl.Execute(insertKey, data)
+	if err != nil {
+		return "", fmt.Errorf("render insert key template(%s) failed, err: %v", tplInfo.UpdateBehavior.InsertKey, err)
+	}
+
+	return insertKey.String(), nil
+}
+
+// renderImportTpl will render import template
+// it will return the []string, like blow:
+// ["import", alias "import", import]
+// other format will be error
+func renderImportTpl(tplInfo *Template, data interface{}) ([]string, error) {
+	var importList []string
+	for _, impt := range tplInfo.UpdateBehavior.ImportTpl {
+		tpl, err := template.New(impt).Funcs(funcMap).Parse(impt)
+		if err != nil {
+			return nil, fmt.Errorf("parse import template(%s) failed, err: %v", impt, err)
+		}
+		imptContent := bytes.NewBuffer(nil)
+		err = tpl.Execute(imptContent, data)
+		if err != nil {
+			return nil, fmt.Errorf("render import template(%s) failed, err: %v", impt, err)
+		}
+		importList = append(importList, imptContent.String())
+	}
+	var ret []string
+	for _, impts := range importList {
+		// 'import render result' may have multiple imports
+		if strings.Contains(impts, "\n") {
+			for _, impt := range strings.Split(impts, "\n") {
+				ret = append(ret, impt)
+			}
+		} else {
+			ret = append(ret, impts)
+		}
+	}
+
+	return ret, nil
+}
+
+// renderAppendContent used to render append content for 'update' command
+func renderAppendContent(tplInfo *Template, renderInfo interface{}) (string, error) {
+	tpl, err := template.New(tplInfo.Path).Parse(tplInfo.UpdateBehavior.AppendTpl)
+	if err != nil {
+		return "", fmt.Errorf("parse append content template(%s) failed, err: %v", tplInfo.Path, err)
+	}
+	appendContent := bytes.NewBuffer(nil)
+	err = tpl.Execute(appendContent, renderInfo)
+	if err != nil {
+		return "", fmt.Errorf("render append content template(%s) failed, err: %v", tplInfo.Path, err)
+	}
+
+	return appendContent.String(), nil
 }
 
-func (pkgGen *HttpPackageGenerator) generateCustomTemplate(pkg *HttpPackage) error {
-	customTpls := pkgGen.customTpls
-	divideMethods := pkgGen.divideMethods
-	if len(customTpls) == 0 {
-		return nil
+// appendUpdateFile used to append content to file for 'update' command
+func appendUpdateFile(tplInfo *Template, renderInfo interface{}, fileContent []byte) ([]byte, error) {
+	// render insert content
+	appendContent, err := renderAppendContent(tplInfo, renderInfo)
+	if err != nil {
+		return []byte(""), err
+	}
+	buf := bytes.NewBuffer(nil)
+	_, err = buf.Write(fileContent)
+	if err != nil {
+		return []byte(""), fmt.Errorf("write file(%s) failed, err: %v", tplInfo.Path, err)
+	}
+	_, err = buf.WriteString("\n" + appendContent + "\n")
+	if err != nil {
+		return []byte(""), fmt.Errorf("append file(%s) failed, err: %v", tplInfo.Path, err)
+	}
+
+	return buf.Bytes(), nil
+}
+
+func getInsertImportContent(tplInfo *Template, renderInfo interface{}, fileContent []byte) ([][2]string, error) {
+	importContent, err := renderImportTpl(tplInfo, renderInfo)
+	if err != nil {
+		return nil, err
+	}
+	var imptSlice [][2]string
+	for _, impt := range importContent {
+		// import has to format
+		// 1. alias "import"
+		// 2. "import"
+		// 3. import (can not contain '"')
+		impt = strings.TrimSpace(impt)
+		if !strings.Contains(impt, "\"") { // 3. import (can not contain '"')
+			if bytes.Contains(fileContent, []byte(impt)) {
+				continue
+			}
+			imptSlice = append(imptSlice, [2]string{"", impt})
+		} else {
+			if !strings.HasSuffix(impt, "\"") {
+				return nil, fmt.Errorf("import can not has suffix \"\"\", for file: %s", tplInfo.Path)
+			}
+			if strings.HasPrefix(impt, "\"") { // 2. "import"
+				if bytes.Contains(fileContent, []byte(impt[1:len(impt)-1])) {
+					continue
+				}
+				imptSlice = append(imptSlice, [2]string{"", impt[1 : len(impt)-1]})
+			} else { // 3. alias "import"
+				idx := strings.Index(impt, "\n")
+				if idx == -1 {
+					return nil, fmt.Errorf("error import format for file: %s", tplInfo.Path)
+				}
+				if bytes.Contains(fileContent, []byte(impt[idx+1:len(impt)-1])) {
+					continue
+				}
+				imptSlice = append(imptSlice, [2]string{impt[:idx], impt[idx+1 : len(impt)-1]})
+			}
+		}
 	}
-	for tplPath, customTpl := range customTpls {
-		for _, service := range pkg.Services {
-			if dm := divideMethods[tplPath]; dm {
+
+	return imptSlice, nil
+}
+
+// genLoopService used to generate files by 'service'
+func (pkgGen *HttpPackageGenerator) genLoopService(tplInfo *Template, filePathRenderInfo FilePathRenderInfo, service *Service, idlPackageRenderInfo *IDLPackageRenderInfo) error {
+	filePath, err := renderFilePath(tplInfo, filePathRenderInfo)
+	if err != nil {
+		return err
+	}
+	// determine if a custom file exists
+	exist, err := util.PathExist(filePath)
+	if err != nil {
+		return fmt.Errorf("judge file(%s) exists failed, err: %v", filePath, err)
+	}
+	if !exist { // create file
+		data := CustomizedFileForService{
+			Service:        service,
+			FilePath:       filePath,
+			FilePackage:    util.SplitPackage(filepath.Dir(filePath), ""),
+			IDLPackageInfo: idlPackageRenderInfo,
+		}
+		err = pkgGen.TemplateGenerator.Generate(data, tplInfo.Path, filePath, false)
+		if err != nil {
+			return err
+		}
+	} else {
+		switch tplInfo.UpdateBehavior.Type {
+		case Skip:
+			// do nothing
+			logs.Infof("do not update file '%s', because the update behavior is 'Unchanged'", filePath)
+		case Cover:
+			// re-generate
+			logs.Infof("re-generate file '%s', because the update behavior is 'Regenerate'", filePath)
+			data := CustomizedFileForService{
+				Service:        service,
+				FilePath:       filePath,
+				FilePackage:    util.SplitPackage(filepath.Dir(filePath), ""),
+				IDLPackageInfo: idlPackageRenderInfo,
+			}
+			err := pkgGen.TemplateGenerator.Generate(data, tplInfo.Path, filePath, false)
+			if err != nil {
+				return err
+			}
+		case Append: // todo: append logic need to be optimized for method
+			fileContent, err := ioutil.ReadFile(filePath)
+			if err != nil {
+				return err
+			}
+			var appendContent []byte
+			// get insert content
+			if tplInfo.UpdateBehavior.AppendKey == "method" {
 				for _, method := range service.Methods {
-					genFilePath, err := pkgGen.generateCustomFileName(tplPath, util.ToSnakeCase(service.Name), pkg.Package, util.ToSnakeCase(method.Name))
+					data := CustomizedFileForMethod{
+						HttpMethod:  method,
+						FilePath:    filePath,
+						FilePackage: util.SplitPackage(filepath.Dir(filePath), ""),
+						ServiceInfo: service,
+					}
+					insertKey, err := renderInsertKey(tplInfo, data)
+					if err != nil {
+						return err
+					}
+					if bytes.Contains(fileContent, []byte(insertKey)) {
+						continue
+					}
+					imptSlice, err := getInsertImportContent(tplInfo, data, fileContent)
 					if err != nil {
 						return err
 					}
-					customPackagePath := util.SubPackage(pkgGen.ProjPackage, util.SubPackageDir(genFilePath))
-					customInfo := &CustomTplRenderInfo{
-						FilePath:    genFilePath,
-						Service:     service.Name,
-						Module:      pkg.Package,
-						PackagePath: customPackagePath,
-						PackageName: util.SplitPackage(customPackagePath, ""),
-						Methods:     []*HttpMethod{method},
-						TPL:         customTpl,
-						Imports:     getImports([]*HttpMethod{method}),
+					// insert new import to the fileContent
+					for _, impt := range imptSlice {
+						if bytes.Contains(fileContent, []byte(impt[1])) {
+							continue
+						}
+						fileContent, err = util.AddImportForContent(fileContent, impt[0], impt[1])
+						// insert import error do not influence the generated file
+						if err != nil {
+							logs.Warnf("can not add import(%s) for file(%s), err: %v\n", impt[1], filePath, err)
+						}
 					}
-					if err = pkgGen.generateCustomTemplateFile(customInfo); err != nil {
+					appendContent, err = appendUpdateFile(tplInfo, data, appendContent)
+					if err != nil {
 						return err
 					}
 				}
-			} else {
-				genFilePath, err := pkgGen.generateCustomFileName(tplPath, util.ToSnakeCase(service.Name), pkg.Package, "")
+				buf := bytes.NewBuffer(nil)
+				_, err = buf.Write(fileContent)
 				if err != nil {
-					return err
+					return fmt.Errorf("write file(%s) failed, err: %v", tplInfo.Path, err)
 				}
-				customPackagePath := util.SubPackage(pkgGen.ProjPackage, util.SubPackageDir(genFilePath))
-				customInfo := &CustomTplRenderInfo{
-					FilePath:    genFilePath,
-					Service:     service.Name,
-					Module:      pkg.Package,
-					PackagePath: customPackagePath,
-					PackageName: util.SplitPackage(customPackagePath, ""),
-					Methods:     service.Methods,
-					TPL:         customTpl,
-					Imports:     getImports(service.Methods),
-				}
-				if err = pkgGen.generateCustomTemplateFile(customInfo); err != nil {
-					return err
+				_, err = buf.Write(appendContent)
+				if err != nil {
+					return fmt.Errorf("append file(%s) failed, err: %v", tplInfo.Path, err)
 				}
+				logs.Infof("append content for file '%s', because the update behavior is 'Append' and appendKey is 'method'", filePath)
+				pkgGen.files = append(pkgGen.files, File{filePath, buf.String(), false, ""})
+			} else {
+				logs.Warnf("Loop 'service' field for '%s' only append content by appendKey for 'method', so cannot append content", filePath)
 			}
+		default:
+			// do nothing
+			logs.Warnf("unknown update behavior, do nothing")
 		}
 	}
 	return nil
 }
 
-func (pkgGen *HttpPackageGenerator) generateCustomFileName(path, serviceName, packagePath, method string) (string, error) {
-	pathTpl, ok := pkgGen.pathNameTpls[path]
-	if !ok {
-		return "", fmt.Errorf("path is not exist,path = %s", path)
+// genLoopMethod used to generate files by 'method'
+func (pkgGen *HttpPackageGenerator) genLoopMethod(tplInfo *Template, filePathRenderInfo FilePathRenderInfo, method *HttpMethod, service *Service) error {
+	filePath, err := renderFilePath(tplInfo, filePathRenderInfo)
+	if err != nil {
+		return err
 	}
-	data := make(map[string]interface{})
-	data["Method"] = method
-	data["PackagePath"] = packagePath
-	data["ServiceName"] = serviceName
-	fileName := bytes.NewBuffer(nil)
-	if err := pathTpl.Execute(fileName, data); err != nil {
-		return "", err
+	// determine if a custom file exists
+	exist, err := util.PathExist(filePath)
+	if err != nil {
+		return fmt.Errorf("judge file(%s) exists failed, err: %v", filePath, err)
 	}
-	return fileName.String(), nil
+
+	if !exist { // create file
+		data := CustomizedFileForMethod{
+			HttpMethod:  method,
+			FilePath:    filePath,
+			FilePackage: util.SplitPackage(filepath.Dir(filePath), ""),
+			ServiceInfo: service,
+		}
+		err := pkgGen.TemplateGenerator.Generate(data, tplInfo.Path, filePath, false)
+		if err != nil {
+			return err
+		}
+	} else {
+		switch tplInfo.UpdateBehavior.Type {
+		case Skip:
+			// do nothing
+			logs.Infof("do not update file '%s', because the update behavior is 'Unchanged'", filePath)
+		case Cover:
+			// re-generate
+			logs.Infof("re-generate file '%s', because the update behavior is 'Regenerate'", filePath)
+			data := CustomizedFileForMethod{
+				HttpMethod:  method,
+				FilePath:    filePath,
+				FilePackage: util.SplitPackage(filepath.Dir(filePath), ""),
+				ServiceInfo: service,
+			}
+			err := pkgGen.TemplateGenerator.Generate(data, tplInfo.Path, filePath, false)
+			if err != nil {
+				return err
+			}
+		case Append:
+			// for loop method, no need to append something; so do nothing
+			logs.Warnf("do not append content for file '%s', because the update behavior is 'Append' and loop 'method' have no need to append content", filePath)
+		default:
+			// do nothing
+			logs.Warnf("unknown update behavior, do nothing")
+		}
+	}
+
+	return nil
 }
 
-func (pkgGen *HttpPackageGenerator) generateCustomTemplateFile(customRenderInfo *CustomTplRenderInfo) error {
-	isExist, err := util.PathExist(customRenderInfo.FilePath)
+// genSingleCustomizedFile used to generate file by 'master IDL'
+func (pkgGen *HttpPackageGenerator) genSingleCustomizedFile(tplInfo *Template, filePathRenderInfo FilePathRenderInfo, idlPackageRenderInfo IDLPackageRenderInfo) error {
+	// generate file single
+	filePath, err := renderFilePath(tplInfo, filePathRenderInfo)
 	if err != nil {
 		return err
 	}
-	if isExist {
-		return nil
-	}
-	file := bytes.NewBuffer(nil)
-	if err = customRenderInfo.TPL.Execute(file, customRenderInfo); err != nil {
-		return err
+	// determine if a custom file exists
+	exist, err := util.PathExist(filePath)
+	if err != nil {
+		return fmt.Errorf("judge file(%s) exists failed, err: %v", filePath, err)
 	}
-	pkgGen.files = append(pkgGen.files, File{customRenderInfo.FilePath, file.String(), false, ""})
-	return nil
-}
 
-func getImports(Methods []*HttpMethod) map[string]*model.Model {
-	ims := make(map[string]*model.Model, len(Methods))
-	for _, m := range Methods {
-		// Iterate over the request and return parameters of the method to get import path.
-		for key, mm := range m.Models {
-			if v, ok := ims[mm.PackageName]; ok && v.Package != mm.Package {
-				ims[key] = mm
-				continue
+	if !exist { // create file
+		data := CustomizedFileForIDL{
+			IDLPackageRenderInfo: &idlPackageRenderInfo,
+			FilePath:             filePath,
+			FilePackage:          util.SplitPackage(filepath.Dir(filePath), ""),
+		}
+		err := pkgGen.TemplateGenerator.Generate(data, tplInfo.Path, filePath, false)
+		if err != nil {
+			return err
+		}
+	} else {
+		switch tplInfo.UpdateBehavior.Type {
+		case Skip:
+			// do nothing
+			logs.Infof("do not update file '%s', because the update behavior is 'Unchanged'", filePath)
+		case Cover:
+			// re-generate
+			logs.Infof("re-generate file '%s', because the update behavior is 'Regenerate'", filePath)
+			data := CustomizedFileForIDL{
+				IDLPackageRenderInfo: &idlPackageRenderInfo,
+				FilePath:             filePath,
+				FilePackage:          util.SplitPackage(filepath.Dir(filePath), ""),
+			}
+			err := pkgGen.TemplateGenerator.Generate(data, tplInfo.Path, filePath, false)
+			if err != nil {
+				return err
+			}
+		case Append: // todo: append logic need to be optimized for single file
+			fileContent, err := ioutil.ReadFile(filePath)
+			if err != nil {
+				return err
+			}
+			if tplInfo.UpdateBehavior.AppendKey == "method" {
+				var appendContent []byte
+				for _, service := range idlPackageRenderInfo.ServiceInfos.Services {
+					for _, method := range service.Methods {
+						data := CustomizedFileForMethod{
+							HttpMethod:  method,
+							FilePath:    filePath,
+							FilePackage: util.SplitPackage(filepath.Dir(filePath), ""),
+							ServiceInfo: service,
+						}
+						insertKey, err := renderInsertKey(tplInfo, data)
+						if err != nil {
+							return err
+						}
+						if bytes.Contains(fileContent, []byte(insertKey)) {
+							continue
+						}
+						imptSlice, err := getInsertImportContent(tplInfo, data, fileContent)
+						if err != nil {
+							return err
+						}
+						for _, impt := range imptSlice {
+							if bytes.Contains(fileContent, []byte(impt[1])) {
+								continue
+							}
+							fileContent, err = util.AddImportForContent(fileContent, impt[0], impt[1])
+							if err != nil {
+								logs.Warnf("can not add import(%s) for file(%s)\n", impt[1], filePath)
+							}
+						}
+
+						appendContent, err = appendUpdateFile(tplInfo, data, appendContent)
+						if err != nil {
+							return err
+						}
+					}
+				}
+				buf := bytes.NewBuffer(nil)
+				_, err = buf.Write(fileContent)
+				if err != nil {
+					return fmt.Errorf("write file(%s) failed, err: %v", tplInfo.Path, err)
+				}
+				_, err = buf.Write(appendContent)
+				if err != nil {
+					return fmt.Errorf("append file(%s) failed, err: %v", tplInfo.Path, err)
+				}
+				logs.Infof("append content for file '%s', because the update behavior is 'Append' and appendKey is 'method'", filePath)
+				pkgGen.files = append(pkgGen.files, File{filePath, buf.String(), false, ""})
+			} else if tplInfo.UpdateBehavior.AppendKey == "service" {
+				var appendContent []byte
+				for _, service := range idlPackageRenderInfo.ServiceInfos.Services {
+					data := CustomizedFileForService{
+						Service:        service,
+						FilePath:       filePath,
+						FilePackage:    util.SplitPackage(filepath.Dir(filePath), ""),
+						IDLPackageInfo: &idlPackageRenderInfo,
+					}
+					insertKey, err := renderInsertKey(tplInfo, data)
+					if err != nil {
+						return err
+					}
+					if bytes.Contains(fileContent, []byte(insertKey)) {
+						continue
+					}
+					imptSlice, err := getInsertImportContent(tplInfo, data, fileContent)
+					if err != nil {
+						return err
+					}
+					for _, impt := range imptSlice {
+						if bytes.Contains(fileContent, []byte(impt[1])) {
+							continue
+						}
+						fileContent, err = util.AddImportForContent(fileContent, impt[0], impt[1])
+						if err != nil {
+							logs.Warnf("can not add import(%s) for file(%s)\n", impt[1], filePath)
+						}
+					}
+					appendContent, err = appendUpdateFile(tplInfo, data, appendContent)
+					if err != nil {
+						return err
+					}
+				}
+				buf := bytes.NewBuffer(nil)
+				_, err = buf.Write(fileContent)
+				if err != nil {
+					return fmt.Errorf("write file(%s) failed, err: %v", tplInfo.Path, err)
+				}
+				_, err = buf.Write(appendContent)
+				if err != nil {
+					return fmt.Errorf("append file(%s) failed, err: %v", tplInfo.Path, err)
+				}
+				logs.Infof("append content for file '%s', because the update behavior is 'Append' and appendKey is 'service'", filePath)
+				pkgGen.files = append(pkgGen.files, File{filePath, buf.String(), false, ""})
+			} else { // add append content to the file directly
+				data := CustomizedFileForIDL{
+					IDLPackageRenderInfo: &idlPackageRenderInfo,
+					FilePath:             filePath,
+					FilePackage:          util.SplitPackage(filepath.Dir(filePath), ""),
+				}
+				file, err := appendUpdateFile(tplInfo, data, fileContent)
+				if err != nil {
+					return err
+				}
+				pkgGen.files = append(pkgGen.files, File{filePath, string(file), false, ""})
 			}
-			ims[mm.PackageName] = mm
+		default:
+			// do nothing
+			logs.Warnf("unknown update behavior, do nothing")
 		}
 	}
-	return ims
+
+	return nil
 }
diff --git a/cmd/hz/generator/handler.go b/cmd/hz/generator/handler.go
index 6d0fb7b..c05793e 100644
--- a/cmd/hz/generator/handler.go
+++ b/cmd/hz/generator/handler.go
@@ -59,9 +59,10 @@ func (pkgGen *HttpPackageGenerator) genHandler(pkg *HttpPackage, handlerDir, han
 		var handler Handler
 		if pkgGen.HandlerByMethod { // generate handler by method
 			for _, m := range s.Methods {
+				filePath := filepath.Join(handlerDir, m.OutputDir, util.ToSnakeCase(m.Name)+".go")
 				handler = Handler{
-					FilePath:    filepath.Join(handlerDir, m.OutputDir, util.ToSnakeCase(m.Name)+".go"),
-					PackageName: util.SplitPackage(handlerPackage, ""),
+					FilePath:    filePath,
+					PackageName: util.SplitPackage(filepath.Dir(filePath), ""),
 					Methods:     []*HttpMethod{m},
 					ProjPackage: pkgGen.ProjPackage,
 				}
@@ -147,16 +148,33 @@ func (pkgGen *HttpPackageGenerator) updateHandler(handler interface{}, handlerTp
 		return err
 	}
 
-	// insert new imports
+	// insert new model imports
 	for alias, model := range handler.(Handler).Imports {
 		if bytes.Contains(file, []byte(model.Package)) {
 			continue
 		}
-		newFile, err := util.AddImport(filePath, alias, model.Package)
+		file, err = util.AddImportForContent(file, alias, model.Package)
 		if err != nil {
 			return err
 		}
-		file = []byte(newFile)
+	}
+	// insert customized imports
+	if tplInfo, exist := pkgGen.TemplateGenerator.tplsInfo[handlerTpl]; exist {
+		if len(tplInfo.UpdateBehavior.ImportTpl) != 0 {
+			imptSlice, err := getInsertImportContent(tplInfo, handler, file)
+			if err != nil {
+				return err
+			}
+			for _, impt := range imptSlice {
+				if bytes.Contains(file, []byte(impt[1])) {
+					continue
+				}
+				file, err = util.AddImportForContent(file, impt[0], impt[1])
+				if err != nil {
+					logs.Warnf("can not add import(%s) for file(%s), err: %v\n", impt[1], filePath, err)
+				}
+			}
+		}
 	}
 
 	// insert new handler
@@ -176,6 +194,7 @@ func (pkgGen *HttpPackageGenerator) updateHandler(handler interface{}, handlerTp
 		data["RequestTypeName"] = method.RequestTypeName
 		data["ReturnTypeName"] = method.ReturnTypeName
 		data["Serializer"] = method.Serializer
+		data["OutputDir"] = method.OutputDir
 		handlerFunc := bytes.NewBuffer(nil)
 		err = handlerSingleTpl.Execute(handlerFunc, data)
 		if err != nil {
diff --git a/cmd/hz/generator/package.go b/cmd/hz/generator/package.go
index 0476271..54ecd41 100644
--- a/cmd/hz/generator/package.go
+++ b/cmd/hz/generator/package.go
@@ -31,10 +31,11 @@ import (
 )
 
 type HttpPackage struct {
-	IdlName  string
-	Package  string
-	Services []*Service
-	Models   []*model.Model
+	IdlName    string
+	Package    string
+	Services   []*Service
+	Models     []*model.Model
+	RouterInfo *Router
 }
 
 type Service struct {
@@ -58,6 +59,7 @@ type HttpPackageGenerator struct {
 	IdlClientDir    string
 	NeedModel       bool
 	HandlerByMethod bool
+	BaseDomain      string
 
 	loadedBackend   Backend
 	curModel        *model.Model
@@ -67,46 +69,52 @@ type HttpPackageGenerator struct {
 }
 
 func (pkgGen *HttpPackageGenerator) Init() error {
-	config := packageConfig
+	defaultConfig := packageConfig
+	customConfig := TemplateConfig{}
 	// unmarshal from user-defined config file if it exists
 	if pkgGen.ConfigPath != "" {
 		cdata, err := os.ReadFile(pkgGen.ConfigPath)
 		if err != nil {
 			return fmt.Errorf("read layout config from  %s failed, err: %v", pkgGen.ConfigPath, err.Error())
 		}
-		config = TemplateConfig{}
-		if err = yaml.Unmarshal(cdata, &config); err != nil {
+		if err = yaml.Unmarshal(cdata, &customConfig); err != nil {
 			return fmt.Errorf("unmarshal layout config failed, err: %v", err.Error())
 		}
-		if reflect.DeepEqual(config, TemplateConfig{}) {
+		if reflect.DeepEqual(customConfig, TemplateConfig{}) {
 			return errors.New("empty config")
 		}
 	}
 
+	if pkgGen.tpls == nil {
+		pkgGen.tpls = make(map[string]*template.Template, len(defaultConfig.Layouts))
+	}
+	if pkgGen.tplsInfo == nil {
+		pkgGen.tplsInfo = make(map[string]*Template, len(defaultConfig.Layouts))
+	}
+
 	// extract routerTplName/middlewareTplName/handlerTplName/registerTplName/modelTplName/clientTplName directories
-	for _, layout := range config.Layouts {
-		name := filepath.Base(layout.Path)
-		if !IsDefaultTpl(name) {
-			continue
-		}
-		path := name
-		delims := DefaultDelimiters
-		if layout.Delims[0] != "" && layout.Delims[1] != "" {
-			delims = layout.Delims
+	// load default template
+	for _, layout := range defaultConfig.Layouts {
+		// default template use "fileName" as template name
+		path := filepath.Base(layout.Path)
+		err := pkgGen.loadLayout(layout, path, true)
+		if err != nil {
+			return err
 		}
-		tpl := template.New(path)
-		tpl = tpl.Delims(delims[0], delims[1])
-		var err error
-		if tpl, err = tpl.Parse(layout.Body); err != nil {
-			return fmt.Errorf("parse template '%s' failed, err: %v", path, err.Error())
+	}
+
+	// override the default template, other customized file template will be loaded by "TemplateGenerator.Init"
+	for _, layout := range customConfig.Layouts {
+		if !IsDefaultPackageTpl(layout.Path) {
+			continue
 		}
-		if pkgGen.tpls == nil {
-			pkgGen.tpls = make(map[string]*template.Template, len(config.Layouts))
+		err := pkgGen.loadLayout(layout, layout.Path, true)
+		if err != nil {
+			return err
 		}
-		pkgGen.tpls[path] = tpl
 	}
 
-	pkgGen.Config = &config
+	pkgGen.Config = &customConfig
 	// load Model tpl if need
 	if pkgGen.Backend != "" {
 		if err := pkgGen.LoadBackend(pkgGen.Backend); err != nil {
@@ -115,6 +123,7 @@ func (pkgGen *HttpPackageGenerator) Init() error {
 	}
 
 	pkgGen.processedModels = make(map[*model.Model]bool)
+	pkgGen.TemplateGenerator.isPackageTpl = true
 
 	return pkgGen.TemplateGenerator.Init()
 }
@@ -171,5 +180,9 @@ func (pkgGen *HttpPackageGenerator) Generate(pkg *HttpPackage) error {
 		return err
 	}
 
-	return pkgGen.generateCustomTemplate(pkg)
+	if err := pkgGen.genCustomizedFile(pkg); err != nil {
+		return err
+	}
+
+	return nil
 }
diff --git a/cmd/hz/generator/package_tpl.go b/cmd/hz/generator/package_tpl.go
index 3eba61e..e313d91 100644
--- a/cmd/hz/generator/package_tpl.go
+++ b/cmd/hz/generator/package_tpl.go
@@ -26,7 +26,7 @@ var (
 	registerTplName         = "register.go"
 	clientTplName           = "client.go"       // generate a default client for server
 	hertzClientTplName      = "hertz_client.go" // underlying client for client command
-	serviceClientName       = "client.go"       // client of service for quick call
+	idlClientName           = "idl_client.go"   // client of service for quick call
 
 	insertPointNew        = "//INSERT_POINT: DO NOT DELETE THIS LINE!"
 	insertPointPatternNew = `//INSERT_POINT\: DO NOT DELETE THIS LINE\!`
@@ -42,10 +42,10 @@ var templateNameSet = map[string]string{
 	registerTplName:         registerTplName,
 	clientTplName:           clientTplName,
 	hertzClientTplName:      hertzClientTplName,
-	serviceClientName:       serviceClientName,
+	idlClientName:           idlClientName,
 }
 
-func IsDefaultTpl(name string) bool {
+func IsDefaultPackageTpl(name string) bool {
 	if _, exist := templateNameSet[name]; exist {
 		return true
 	}
@@ -253,16 +253,16 @@ func {{.MiddleWare}}Mw() []app.HandlerFunc {
 			Body:   hertzClientTpl,
 		},
 		{
-			Path:   defaultRouterDir + sp + serviceClientName,
+			Path:   defaultRouterDir + sp + idlClientName,
 			Delims: [2]string{"{{", "}}"},
-			Body:   serviceClientTpl,
+			Body:   idlClientTpl,
 		},
 	},
 }
 
 var hertzClientTpl = `// Code generated by hz.
 
-package hertz
+package {{.PackageName}}
 
 import (
 	"context"
@@ -412,6 +412,7 @@ func newClient(opts *Options) (*cli, error) {
 	c := &cli{
 		hostUrl:               opts.hostUrl,
 		doer:                  opts.doer,
+		header:                opts.header,
 		bindRequestBody:       opts.requestBodyBind,
 		responseResultDecider: opts.responseResultDecider,
 		beforeRequest: []beforeRequestFunc{
@@ -850,9 +851,9 @@ func unmarshalContent(ct string, b []byte, d interface{}) (err error) {
 
 `
 
-var serviceClientTpl = `// Code generated by hertz generator.
+var idlClientTpl = `// Code generated by hertz generator.
 
-package hertz
+package {{.PackageName}}
 
 import (
 	"context"
diff --git a/cmd/hz/generator/router.go b/cmd/hz/generator/router.go
index cebbe69..3ea65fc 100644
--- a/cmd/hz/generator/router.go
+++ b/cmd/hz/generator/router.go
@@ -256,11 +256,10 @@ func (pkgGen *HttpPackageGenerator) updateRegister(pkg, rDir, pkgName string) er
 	}
 
 	if !bytes.Contains(file, []byte(register.Pkg)) {
-		newFile, err := util.AddImport(registerPath, register.PkgAlias, register.Pkg)
+		file, err = util.AddImport(registerPath, register.PkgAlias, register.Pkg)
 		if err != nil {
 			return err
 		}
-		file = []byte(newFile)
 
 		insertReg := register.PkgAlias + ".Register(r)\n"
 		if bytes.Contains(file, []byte(insertReg)) {
@@ -309,6 +308,9 @@ func (pkgGen *HttpPackageGenerator) genRouter(pkg *HttpPackage, root *RouterNode
 		router.HandlerPackages = handlerMap
 	}
 
+	// store router info
+	pkg.RouterInfo = &router
+
 	if err := pkgGen.TemplateGenerator.Generate(router, routerTplName, router.FilePath, false); err != nil {
 		return fmt.Errorf("generate router %s failed, err: %v", router.FilePath, err.Error())
 	}
diff --git a/cmd/hz/generator/template.go b/cmd/hz/generator/template.go
index 0918d2e..d5eeca5 100644
--- a/cmd/hz/generator/template.go
+++ b/cmd/hz/generator/template.go
@@ -36,26 +36,40 @@ type TemplateConfig struct {
 	Layouts []Template `yaml:"layouts"`
 }
 
+const (
+	Skip   = "skip"
+	Cover  = "cover"
+	Append = "append"
+)
+
 type Template struct {
-	Path           string    `yaml:"path"`            // The generated path and its filename, such as biz/handler/ping.go
-	Delims         [2]string `yaml:"delims"`          // Template Action Instruction Identifier, default: "{{}}"
-	Body           string    `yaml:"body"`            // Render template, currently only supports go template syntax
-	CustomTemplate bool      `yaml:"custom_template"` // Whether it is a custom template, This configuration does not work in layout templates, default: false
-	DivideMethod   bool      `yaml:"divide_method"`   // When CustomTemplate is equal to true, all methods are traversed and code is generated.
+	Default        bool           // Update command behavior; skip/cover/append
+	Path           string         `yaml:"path"`            // The generated path and its filename, such as biz/handler/ping.go
+	Delims         [2]string      `yaml:"delims"`          // Template Action Instruction Identifier, default: "{{}}"
+	Body           string         `yaml:"body"`            // Render template, currently only supports go template syntax
+	LoopMethod     bool           `yaml:"loop_method"`     // Loop generate files based on "method"
+	LoopService    bool           `yaml:"loop_service"`    // Loop generate files based on "service"
+	UpdateBehavior UpdateBehavior `yaml:"update_behavior"` // Update command behavior; 0:unchanged, 1:regenerate, 2:append
+}
+
+type UpdateBehavior struct {
+	Type string `yaml:"type"` // Update behavior type; skip/cover/append
+	// the following variables are used for append update
+	AppendKey string   `yaml:"append_key"`         // Append content based in key; for example: 'method'/'service'
+	InsertKey string   `yaml:"insert_key"`         // Insert content by "insert_key"
+	AppendTpl string   `yaml:"append_content_tpl"` // Append content if UpdateBehavior is "append"
+	ImportTpl []string `yaml:"import_tpl"`         // Import insert template
 }
 
 // TemplateGenerator contains information about the output template
 type TemplateGenerator struct {
-	OutputDir string
-	Config    *TemplateConfig
-	Excludes  []string
-	tpls      map[string]*template.Template
-	dirs      map[string]bool
-
-	// custom template files and whether divide method
-	customTpls    map[string]*template.Template
-	pathNameTpls  map[string]*template.Template
-	divideMethods map[string]bool
+	OutputDir    string
+	Config       *TemplateConfig
+	Excludes     []string
+	tpls         map[string]*template.Template // "template name" -> "Template", it is used get the "parsed template" directly
+	tplsInfo     map[string]*Template          // "template name" -> "template info", it is used to get the original "template information"
+	dirs         map[string]bool
+	isPackageTpl bool
 
 	files         []File
 	excludedFiles map[string]*File
@@ -66,31 +80,21 @@ func (tg *TemplateGenerator) Init() error {
 		return errors.New("config not set yet")
 	}
 
-	tpls := tg.tpls
-	if tpls == nil {
-		tpls = make(map[string]*template.Template, len(tg.Config.Layouts))
-	}
-	dirs := tg.dirs
-	if dirs == nil {
-		dirs = make(map[string]bool)
+	if tg.tpls == nil {
+		tg.tpls = make(map[string]*template.Template, len(tg.Config.Layouts))
 	}
-
-	customTpls := tg.customTpls
-	if customTpls == nil {
-		customTpls = make(map[string]*template.Template, len(tg.Config.Layouts))
+	if tg.tplsInfo == nil {
+		tg.tplsInfo = make(map[string]*Template, len(tg.Config.Layouts))
 	}
-
-	pathNameTpls := tg.pathNameTpls
-	if pathNameTpls == nil {
-		pathNameTpls = make(map[string]*template.Template, len(tg.Config.Layouts))
-	}
-
-	divideMethods := tg.divideMethods
-	if divideMethods == nil {
-		divideMethods = make(map[string]bool, len(tg.Config.Layouts))
+	if tg.dirs == nil {
+		tg.dirs = make(map[string]bool)
 	}
 
 	for _, l := range tg.Config.Layouts {
+		if tg.isPackageTpl && IsDefaultPackageTpl(l.Path) {
+			continue
+		}
+
 		// check if is a directory
 		var noFile bool
 		if strings.HasSuffix(l.Path, string(filepath.Separator)) {
@@ -106,46 +110,23 @@ func (tg *TemplateGenerator) Init() error {
 			return fmt.Errorf("check directory '%s' failed, err: %v", dir, err.Error())
 		}
 		if isExist {
-			dirs[dir] = true
+			tg.dirs[dir] = true
 		} else {
-			dirs[dir] = false
+			tg.dirs[dir] = false
 		}
 
-		divideMethods[path] = l.DivideMethod
-
 		if noFile {
 			continue
 		}
 
 		// parse templates
-		if _, ok := tpls[path]; ok {
+		if _, ok := tg.tpls[path]; ok {
 			continue
 		}
-		delims := DefaultDelimiters
-		if l.Delims[0] != "" && l.Delims[1] != "" {
-			delims = l.Delims
-		}
-		tpl := template.New(path)
-		tpl = tpl.Delims(delims[0], delims[1])
-		if tpl, err = tpl.Parse(l.Body); err != nil {
-			return fmt.Errorf("parse template '%s' failed, err: %v", path, err.Error())
-		}
-
-		if l.CustomTemplate {
-			customTpls[path] = tpl
-		} else {
-			tpls[path] = tpl
-		}
-
-		if l.CustomTemplate {
-			pathNameTpl := template.New(path)
-			pathNameTpl = pathNameTpl.Delims(delims[0], delims[1])
-			if pathNameTpl, err = pathNameTpl.Parse(path); err != nil {
-				return fmt.Errorf("parse template '%s' failed, err: %v", path, err.Error())
-			}
-			pathNameTpls[path] = pathNameTpl
+		err = tg.loadLayout(l, path, false)
+		if err != nil {
+			return err
 		}
-
 	}
 
 	excludes := make(map[string]*File, len(tg.Excludes))
@@ -153,12 +134,25 @@ func (tg *TemplateGenerator) Init() error {
 		excludes[f] = &File{}
 	}
 
-	tg.tpls = tpls
-	tg.dirs = dirs
 	tg.excludedFiles = excludes
-	tg.customTpls = customTpls
-	tg.divideMethods = divideMethods
-	tg.pathNameTpls = pathNameTpls
+	return nil
+}
+
+func (tg *TemplateGenerator) loadLayout(layout Template, tplName string, isDefaultTpl bool) error {
+	delims := DefaultDelimiters
+	if layout.Delims[0] != "" && layout.Delims[1] != "" {
+		delims = layout.Delims
+	}
+	// insert template funcs
+	tpl := template.New(tplName).Funcs(funcMap)
+	tpl = tpl.Delims(delims[0], delims[1])
+	var err error
+	if tpl, err = tpl.Parse(layout.Body); err != nil {
+		return fmt.Errorf("parse template '%s' failed, err: %v", tplName, err.Error())
+	}
+	layout.Default = isDefaultTpl
+	tg.tpls[tplName] = tpl
+	tg.tplsInfo[tplName] = &layout
 	return nil
 }
 
diff --git a/cmd/hz/generator/template_funcs.go b/cmd/hz/generator/template_funcs.go
new file mode 100644
index 0000000..359723d
--- /dev/null
+++ b/cmd/hz/generator/template_funcs.go
@@ -0,0 +1,41 @@
+/*
+ * 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 generator
+
+import (
+	"text/template"
+
+	"github.com/cloudwego/hertz/cmd/hz/util"
+)
+
+var funcMap = template.FuncMap{
+	"GetUniqueHandlerOutDir": getUniqueHandlerOutDir,
+	"ToSnakeCase":            util.ToSnakeCase,
+}
+
+// getUniqueHandlerOutDir uses to get unique "api.handler_path"
+func getUniqueHandlerOutDir(methods []*HttpMethod) (ret []string) {
+	outDirMap := make(map[string]string)
+	for _, method := range methods {
+		if _, exist := outDirMap[method.OutputDir]; !exist {
+			outDirMap[method.OutputDir] = method.OutputDir
+			ret = append(ret, method.OutputDir)
+		}
+	}
+
+	return ret
+}
diff --git a/cmd/hz/go.mod b/cmd/hz/go.mod
index 2e4d091..b65eeeb 100644
--- a/cmd/hz/go.mod
+++ b/cmd/hz/go.mod
@@ -7,7 +7,7 @@ require (
 	github.com/hashicorp/go-version v1.5.0
 	github.com/jhump/protoreflect v1.12.0
 	github.com/urfave/cli/v2 v2.23.0
-	golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135
+	golang.org/x/tools v0.4.0
 	google.golang.org/protobuf v1.28.0
 	gopkg.in/yaml.v2 v2.4.0
 )
diff --git a/cmd/hz/go.sum b/cmd/hz/go.sum
index 7838c6a..23aa603 100644
--- a/cmd/hz/go.sum
+++ b/cmd/hz/go.sum
@@ -59,39 +59,63 @@ github.com/urfave/cli/v2 v2.23.0 h1:pkly7gKIeYv3olPAeNajNpLjeJrmTPYCoZWaV+2VfvE=
 github.com/urfave/cli/v2 v2.23.0/go.mod h1:1CNUng3PtjQMtRzJO4FMXBQvkGtuYRxxiR9xMa7jMwI=
 github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
 github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
 golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.3.0 h1:VWL6FNY2bEEmsGVKabSlHu5Irp34xmMRoqb/9lF9lxk=
+golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
+golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
-golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
+golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135 h1:5Beo0mZN8dRzgrMMkDp0jc8YXQKx9DiJ2k1dkvGsn5A=
 golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.4.0 h1:7mTAgkunk3fr4GAloyyCasadO6h9zSsQZbwvcaIciV4=
+golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
diff --git a/cmd/hz/meta/const.go b/cmd/hz/meta/const.go
index 0bcb21f..00c0aa8 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.0"
+const Version = "v0.5.1"
 
 // Mode hz run modes
 type Mode int
diff --git a/cmd/hz/protobuf/plugin.go b/cmd/hz/protobuf/plugin.go
index 4d32346..fdddc93 100644
--- a/cmd/hz/protobuf/plugin.go
+++ b/cmd/hz/protobuf/plugin.go
@@ -595,6 +595,7 @@ func (plugin *Plugin) genHttpPackage(ast *descriptorpb.FileDescriptorProto, deps
 		HandlerByMethod: args.HandlerByMethod,
 		CmdType:         args.CmdType,
 		IdlClientDir:    plugin.IdlClientDir,
+		BaseDomain:      args.BaseDomain,
 	}
 
 	if args.ModelBackend != "" {
diff --git a/cmd/hz/protobuf/resolver.go b/cmd/hz/protobuf/resolver.go
index 8246b3c..94fe948 100644
--- a/cmd/hz/protobuf/resolver.go
+++ b/cmd/hz/protobuf/resolver.go
@@ -66,10 +66,18 @@ type PackageReference struct {
 	Referred    bool
 }
 
-func getReferPkgMap(pkgMap map[string]string, incs []*descriptorpb.FileDescriptorProto) (map[string]*PackageReference, error) {
+func getReferPkgMap(pkgMap map[string]string, incs []*descriptorpb.FileDescriptorProto, mainModel *model.Model) (map[string]*PackageReference, error) {
 	var err error
 	out := make(map[string]*PackageReference, len(pkgMap))
 	pkgAliasMap := make(map[string]string, len(incs))
+	// bugfix: add main package to avoid namespace conflict
+	mainPkg := mainModel.Package
+	mainPkgName := mainModel.PackageName
+	mainPkgName, err = util.GetPackageUniqueName(mainPkgName)
+	if err != nil {
+		return nil, err
+	}
+	pkgAliasMap[mainPkg] = mainPkgName
 	for _, inc := range incs {
 		pkg := getGoPackage(inc, pkgMap)
 		path := inc.GetName()
@@ -157,7 +165,7 @@ func NewResolver(ast *descriptorpb.FileDescriptorProto, files FileInfos, model *
 			return nil, fmt.Errorf("%s not found", dep)
 		}
 	}
-	pm, err := getReferPkgMap(pkgMap, incs)
+	pm, err := getReferPkgMap(pkgMap, incs, model)
 	if err != nil {
 		return nil, fmt.Errorf("get package map failed, err: %v", err)
 	}
diff --git a/cmd/hz/thrift/plugin.go b/cmd/hz/thrift/plugin.go
index 33d159b..c0f52cf 100644
--- a/cmd/hz/thrift/plugin.go
+++ b/cmd/hz/thrift/plugin.go
@@ -137,6 +137,7 @@ func (plugin *Plugin) Run() int {
 		HandlerByMethod: args.HandlerByMethod,
 		CmdType:         args.CmdType,
 		IdlClientDir:    util.SubDir(modelDir, pkgInfo.Package),
+		BaseDomain:      args.BaseDomain,
 	}
 	if args.ModelBackend != "" {
 		sg.Backend = meta.Backend(args.ModelBackend)
diff --git a/cmd/hz/thrift/resolver.go b/cmd/hz/thrift/resolver.go
index bc8963a..9b40b5c 100644
--- a/cmd/hz/thrift/resolver.go
+++ b/cmd/hz/thrift/resolver.go
@@ -54,10 +54,18 @@ type PackageReference struct {
 	Referred    bool
 }
 
-func getReferPkgMap(pkgMap map[string]string, incs []*parser.Include) (map[string]*PackageReference, error) {
+func getReferPkgMap(pkgMap map[string]string, incs []*parser.Include, mainModel *model.Model) (map[string]*PackageReference, error) {
 	var err error
 	out := make(map[string]*PackageReference, len(pkgMap))
 	pkgAliasMap := make(map[string]string, len(incs))
+	// bugfix: add main package to avoid namespace conflict
+	mainPkg := mainModel.Package
+	mainPkgName := mainModel.PackageName
+	mainPkgName, err = util.GetPackageUniqueName(mainPkgName)
+	if err != nil {
+		return nil, err
+	}
+	pkgAliasMap[mainPkg] = mainPkgName
 	for _, inc := range incs {
 		pkg := getGoPackage(inc.Reference, pkgMap)
 		impt := inc.GetPath()
@@ -102,7 +110,7 @@ type Resolver struct {
 }
 
 func NewResolver(ast *parser.Thrift, model *model.Model, pkgMap map[string]string) (*Resolver, error) {
-	pm, err := getReferPkgMap(pkgMap, ast.GetIncludes())
+	pm, err := getReferPkgMap(pkgMap, ast.GetIncludes(), model)
 	if err != nil {
 		return nil, fmt.Errorf("get package map failed, err: %v", err)
 	}
diff --git a/cmd/hz/util/ast.go b/cmd/hz/util/ast.go
index 9c2350e..b97d773 100644
--- a/cmd/hz/util/ast.go
+++ b/cmd/hz/util/ast.go
@@ -19,6 +19,7 @@ package util
 import (
 	"bytes"
 	"fmt"
+	"go/ast"
 	"go/format"
 	"go/parser"
 	"go/token"
@@ -27,23 +28,38 @@ import (
 	"golang.org/x/tools/go/ast/astutil"
 )
 
-func AddImport(file, alias, impt string) (string, error) {
+func AddImport(file, alias, impt string) ([]byte, error) {
 	fset := token.NewFileSet()
 	path, _ := filepath.Abs(file)
 	f, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
 	if err != nil {
-		return "", fmt.Errorf("can not parse ast for file: %s", path)
+		return nil, fmt.Errorf("can not parse ast for file: %s, err: %v", path, err)
 	}
+
+	return addImport(fset, f, alias, impt)
+}
+
+func AddImportForContent(fileContent []byte, alias, impt string) ([]byte, error) {
+	fset := token.NewFileSet()
+	f, err := parser.ParseFile(fset, "", fileContent, parser.ParseComments)
+	if err != nil {
+		return nil, fmt.Errorf("can not parse ast for file: %s, err: %v", fileContent, err)
+	}
+
+	return addImport(fset, f, alias, impt)
+}
+
+func addImport(fset *token.FileSet, f *ast.File, alias, impt string) ([]byte, error) {
 	added := astutil.AddNamedImport(fset, f, alias, impt)
 	if !added {
-		return "", fmt.Errorf("can not add import \"%s\" for file: %s", impt, path)
+		return nil, fmt.Errorf("can not add import \"%s\" for file: %s", impt, f.Name.Name)
 	}
 	var output []byte
 	buffer := bytes.NewBuffer(output)
-	err = format.Node(buffer, fset, f)
+	err := format.Node(buffer, fset, f)
 	if err != nil {
-		return "", fmt.Errorf("can add import for file: %s", path)
+		return nil, fmt.Errorf("can not add import for file: %s, err: %v", f.Name.Name, err)
 	}
 
-	return buffer.String(), nil
+	return buffer.Bytes(), nil
 }
diff --git a/pkg/network/standard/connection.go b/pkg/network/standard/connection.go
index b9b2e83..5388c38 100644
--- a/pkg/network/standard/connection.go
+++ b/pkg/network/standard/connection.go
@@ -22,11 +22,11 @@ import (
 	"io"
 	"net"
 	"strconv"
+	"syscall"
 	"time"
 
 	errs "github.com/cloudwego/hertz/pkg/common/errors"
 	"github.com/cloudwego/hertz/pkg/network"
-	"golang.org/x/sys/unix"
 )
 
 const (
@@ -47,7 +47,7 @@ type Conn struct {
 }
 
 func (c *Conn) ToHertzError(err error) error {
-	if errors.Is(err, unix.EPIPE) || errors.Is(err, unix.ENOTCONN) {
+	if errors.Is(err, syscall.EPIPE) || errors.Is(err, syscall.ENOTCONN) {
 		return errs.ErrConnectionClosed
 	}
 	return err
diff --git a/pkg/protocol/header.go b/pkg/protocol/header.go
index b139545..b447cce 100644
--- a/pkg/protocol/header.go
+++ b/pkg/protocol/header.go
@@ -1165,6 +1165,20 @@ func (h *RequestHeader) Cookie(key string) []byte {
 	return peekArgStr(h.cookies, key)
 }
 
+// Cookies returns all the request cookies.
+//
+// It's a good idea to call protocol.ReleaseCookie to reduce GC load after the cookie used.
+func (h *RequestHeader) Cookies() []*Cookie {
+	var cookies []*Cookie
+	h.VisitAllCookie(func(key, value []byte) {
+		cookie := AcquireCookie()
+		cookie.SetKeyBytes(key)
+		cookie.SetValueBytes(value)
+		cookies = append(cookies, cookie)
+	})
+	return cookies
+}
+
 func (h *RequestHeader) PeekRange() []byte {
 	return h.peek(bytestr.StrRange)
 }
diff --git a/pkg/protocol/http1/factory/server.go b/pkg/protocol/http1/factory/server.go
index 99029eb..513303b 100644
--- a/pkg/protocol/http1/factory/server.go
+++ b/pkg/protocol/http1/factory/server.go
@@ -30,10 +30,10 @@ type serverFactory struct {
 
 // New is called by Hertz during engine.Run()
 func (s *serverFactory) New(core suite.Core) (server protocol.Server, err error) {
-	return &http1.Server{
-		Option: *s.option,
-		Core:   core,
-	}, nil
+	serv := http1.NewServer()
+	serv.Option = *s.option
+	serv.Core = core
+	return serv, nil
 }
 
 func NewServerFactory(option *http1.Option) suite.ServerFactory {
diff --git a/pkg/protocol/http1/server.go b/pkg/protocol/http1/server.go
index 6ff8d7d..99d2da7 100644
--- a/pkg/protocol/http1/server.go
+++ b/pkg/protocol/http1/server.go
@@ -22,6 +22,7 @@ import (
 	"errors"
 	"io"
 	"net"
+	"sync"
 	"time"
 
 	"github.com/cloudwego/hertz/internal/bytestr"
@@ -30,6 +31,7 @@ import (
 	"github.com/cloudwego/hertz/pkg/app/server/render"
 	errs "github.com/cloudwego/hertz/pkg/common/errors"
 	"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/consts"
@@ -71,6 +73,8 @@ type Option struct {
 type Server struct {
 	Option
 	Core suite.Core
+
+	eventStackPool *sync.Pool
 }
 
 func (s Server) Serve(c context.Context, conn network.Conn) (err error) {
@@ -93,19 +97,31 @@ func (s Server) Serve(c context.Context, conn network.Conn) (err error) {
 		// 4. Reset and recycle
 		ctx = s.Core.GetCtxPool().Get().(*app.RequestContext)
 
-		traceCtl = s.Core.GetTracer()
+		traceCtl        = s.Core.GetTracer()
+		eventsToTrigger *eventStack
 
 		// Use a new variable to hold the standard context to avoid modify the initial
 		// context.
 		cc = c
 	)
 
+	if s.EnableTrace {
+		eventsToTrigger = s.eventStackPool.Get().(*eventStack)
+	}
+
 	defer func() {
-		// error case
-		if err != nil && s.EnableTrace {
-			if !errors.Is(err, errs.ErrIdleTimeout) && !errors.Is(err, errs.ErrHijacked) {
+		if s.EnableTrace {
+			if err != nil && !errors.Is(err, errs.ErrIdleTimeout) && !errors.Is(err, errs.ErrHijacked) {
 				ctx.GetTraceInfo().Stats().SetError(err)
 			}
+			// in case of error, we need to trigger all events
+			if eventsToTrigger != nil {
+				for last := eventsToTrigger.pop(); last != nil; last = eventsToTrigger.pop() {
+					last(ctx.GetTraceInfo(), err)
+				}
+				s.eventStackPool.Put(eventsToTrigger)
+			}
+
 			traceCtl.DoFinish(cc, ctx, err)
 		}
 
@@ -142,7 +158,7 @@ func (s Server) Serve(c context.Context, conn network.Conn) (err error) {
 			ctx.GetConn().SetReadTimeout(s.IdleTimeout) //nolint:errcheck
 
 			_, err = zr.Peek(4)
-			// This is not the first request and we haven't read a single byte
+			// This is not the first request, and we haven't read a single byte
 			// of a new request yet. This means it's just a keep-alive connection
 			// closing down either because the remote closed it or because
 			// or a read timeout on our side. Either way just close the connection
@@ -159,12 +175,21 @@ func (s Server) Serve(c context.Context, conn network.Conn) (err error) {
 		if s.EnableTrace {
 			cc = traceCtl.DoStart(c, ctx)
 			internalStats.Record(ctx.GetTraceInfo(), stats.ReadHeaderStart, err)
+			eventsToTrigger.push(func(ti traceinfo.TraceInfo, err error) {
+				internalStats.Record(ti, stats.ReadHeaderFinish, err)
+			})
 		}
 		// Read Headers
 		if err = req.ReadHeader(&ctx.Request.Header, zr); err == nil {
 			if s.EnableTrace {
-				internalStats.Record(ctx.GetTraceInfo(), stats.ReadHeaderFinish, err)
+				// read header finished
+				if last := eventsToTrigger.pop(); last != nil {
+					last(ctx.GetTraceInfo(), err)
+				}
 				internalStats.Record(ctx.GetTraceInfo(), stats.ReadBodyStart, err)
+				eventsToTrigger.push(func(ti traceinfo.TraceInfo, err error) {
+					internalStats.Record(ti, stats.ReadBodyFinish, err)
+				})
 			}
 			// Read body
 			if s.StreamRequestBody {
@@ -180,7 +205,10 @@ func (s Server) Serve(c context.Context, conn network.Conn) (err error) {
 			} else {
 				ctx.GetTraceInfo().Stats().SetRecvSize(0)
 			}
-			internalStats.Record(ctx.GetTraceInfo(), stats.ReadBodyFinish, err)
+			// read body finished
+			if last := eventsToTrigger.pop(); last != nil {
+				last(ctx.GetTraceInfo(), err)
+			}
 		}
 
 		if err != nil {
@@ -241,6 +269,9 @@ func (s Server) Serve(c context.Context, conn network.Conn) (err error) {
 		}
 		if s.EnableTrace {
 			internalStats.Record(ctx.GetTraceInfo(), stats.ServerHandleStart, err)
+			eventsToTrigger.push(func(ti traceinfo.TraceInfo, err error) {
+				internalStats.Record(ti, stats.ServerHandleFinish, err)
+			})
 		}
 		// Handle the request
 		//
@@ -248,7 +279,10 @@ func (s Server) Serve(c context.Context, conn network.Conn) (err error) {
 		// and the route has been matched.
 		s.Core.ServeHTTP(cc, ctx)
 		if s.EnableTrace {
-			internalStats.Record(ctx.GetTraceInfo(), stats.ServerHandleFinish, err)
+			// application layer handle finished
+			if last := eventsToTrigger.pop(); last != nil {
+				last(ctx.GetTraceInfo(), err)
+			}
 		}
 
 		// exit check
@@ -275,6 +309,9 @@ func (s Server) Serve(c context.Context, conn network.Conn) (err error) {
 		}
 		if s.EnableTrace {
 			internalStats.Record(ctx.GetTraceInfo(), stats.WriteStart, err)
+			eventsToTrigger.push(func(ti traceinfo.TraceInfo, err error) {
+				internalStats.Record(ti, stats.WriteFinish, err)
+			})
 		}
 		if err = writeResponse(ctx, zw); err != nil {
 			return
@@ -298,7 +335,10 @@ func (s Server) Serve(c context.Context, conn network.Conn) (err error) {
 			return
 		}
 		if s.EnableTrace {
-			internalStats.Record(ctx.GetTraceInfo(), stats.WriteFinish, err)
+			// write finished
+			if last := eventsToTrigger.pop(); last != nil {
+				last(ctx.GetTraceInfo(), err)
+			}
 		}
 
 		// Release request body stream
@@ -345,6 +385,16 @@ func (s Server) Serve(c context.Context, conn network.Conn) (err error) {
 	}
 }
 
+func NewServer() *Server {
+	return &Server{
+		eventStackPool: &sync.Pool{
+			New: func() interface{} {
+				return &eventStack{}
+			},
+		},
+	}
+}
+
 func writeErrorResponse(zw network.Writer, ctx *app.RequestContext, serverName []byte, err error) network.Writer {
 	errorHandler := defaultErrorHandler
 
@@ -380,3 +430,22 @@ func defaultErrorHandler(ctx *app.RequestContext, err error) {
 		ctx.AbortWithMsg("Error when parsing request", consts.StatusBadRequest)
 	}
 }
+
+type eventStack []func(ti traceinfo.TraceInfo, err error)
+
+func (e *eventStack) isEmpty() bool {
+	return len(*e) == 0
+}
+
+func (e *eventStack) push(f func(ti traceinfo.TraceInfo, err error)) {
+	*e = append(*e, f)
+}
+
+func (e *eventStack) pop() func(ti traceinfo.TraceInfo, err error) {
+	if e.isEmpty() {
+		return nil
+	}
+	last := (*e)[len(*e)-1]
+	*e = (*e)[:len(*e)-1]
+	return last
+}
diff --git a/pkg/protocol/request.go b/pkg/protocol/request.go
index 52f28d9..8b86067 100644
--- a/pkg/protocol/request.go
+++ b/pkg/protocol/request.go
@@ -347,7 +347,7 @@ func (req *Request) SwapBody(body []byte) []byte {
 func (req *Request) CopyTo(dst *Request) {
 	req.CopyToSkipBody(dst)
 	if req.bodyRaw != nil {
-		dst.bodyRaw = req.bodyRaw
+		dst.bodyRaw = append(dst.bodyRaw[:0], req.bodyRaw...)
 		if dst.body != nil {
 			dst.body.Reset()
 		}
diff --git a/pkg/protocol/response.go b/pkg/protocol/response.go
index f46395b..04440f3 100644
--- a/pkg/protocol/response.go
+++ b/pkg/protocol/response.go
@@ -240,7 +240,7 @@ func (resp *Response) BodyWriteTo(w io.Writer) error {
 func (resp *Response) CopyTo(dst *Response) {
 	resp.CopyToSkipBody(dst)
 	if resp.bodyRaw != nil {
-		dst.bodyRaw = resp.bodyRaw
+		dst.bodyRaw = append(dst.bodyRaw[:0], resp.bodyRaw...)
 		if dst.body != nil {
 			dst.body.Reset()
 		}
diff --git a/version.go b/version.go
index 80702a5..b57f3fe 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.0"
+	Version = "v0.5.1"
 )


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 4fae434..0e42b00 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -31,4 +31,34 @@ jobs:
         run: go test -race -covermode=atomic -coverprofile=coverage.txt ./...
 
       - name: Codecov
-        run: bash <(curl -s https://codecov.io/bash)
\ No newline at end of file
+        run: bash <(curl -s https://codecov.io/bash)
+
+  lint-and-ut-windows:
+    strategy:
+      matrix:
+        version: [ 1.18, 1.19 ]
+    runs-on: windows-latest
+    steps:
+      - uses: actions/checkout@v3
+
+      - name: Set up Go
+        uses: actions/setup-go@v3
+        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: Lint
+        run: |
+          go vet -stdmethods=false $(go list ./...)
+          go install mvdan.cc/gofumpt@v0.2.0
+          test -z "$(gofumpt -l -extra .)"
+      - name: Unit Test
+        run: go test -race -covermode=atomic -coverprofile=coverage.txt ./...
+#
+#      - name: Codecov
+#        run: bash <(curl -s https://codecov.io/bash)
diff --git a/pkg/protocol/header_test.go b/pkg/protocol/header_test.go
index 14446d5..0a7baf2 100644
--- a/pkg/protocol/header_test.go
+++ b/pkg/protocol/header_test.go
@@ -189,6 +189,18 @@ func TestRequestHeaderVisitAll(t *testing.T) {
 		})
 }
 
+func TestRequestHeaderCookies(t *testing.T) {
+	var h RequestHeader
+	h.SetCookie("foo", "bar")
+	h.SetCookie("привет", "мир")
+	cookies := h.Cookies()
+	assert.DeepEqual(t, 2, len(cookies))
+	assert.DeepEqual(t, []byte("foo"), cookies[0].Key())
+	assert.DeepEqual(t, []byte("bar"), cookies[0].Value())
+	assert.DeepEqual(t, []byte("привет"), cookies[1].Key())
+	assert.DeepEqual(t, []byte("мир"), cookies[1].Value())
+}
+
 func TestRequestHeaderDel(t *testing.T) {
 	t.Parallel()
 
diff --git a/pkg/protocol/http1/server_test.go b/pkg/protocol/http1/server_test.go
new file mode 100644
index 0000000..4a2a15a
--- /dev/null
+++ b/pkg/protocol/http1/server_test.go
@@ -0,0 +1,223 @@
+/*
+ * 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 http1
+
+import (
+	"context"
+	"errors"
+	"sync"
+	"testing"
+
+	inStats "github.com/cloudwego/hertz/internal/stats"
+	"github.com/cloudwego/hertz/pkg/app"
+	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/tracer"
+	"github.com/cloudwego/hertz/pkg/common/tracer/stats"
+	"github.com/cloudwego/hertz/pkg/common/tracer/traceinfo"
+	"github.com/cloudwego/hertz/pkg/network"
+)
+
+var pool = &sync.Pool{New: func() interface{} {
+	return &eventStack{}
+}}
+
+func TestTraceEventCompleted(t *testing.T) {
+	server := &Server{}
+	server.eventStackPool = pool
+	server.EnableTrace = true
+	reqCtx := &app.RequestContext{}
+	server.Core = &mockCore{
+		ctxPool: &sync.Pool{New: func() interface{} {
+			ti := traceinfo.NewTraceInfo()
+			ti.Stats().SetLevel(2)
+			reqCtx.SetTraceInfo(&mockTraceInfo{ti})
+			return reqCtx
+		}},
+		controller: &inStats.Controller{},
+	}
+	err := server.Serve(context.TODO(), mock.NewConn("GET /aaa HTTP/1.1\nHost: foobar.com\n\n"))
+	assert.True(t, errors.Is(err, errs.ErrShortConnection))
+	traceInfo := reqCtx.GetTraceInfo()
+	assert.False(t, traceInfo.Stats().GetEvent(stats.HTTPStart).IsNil())
+	assert.False(t, traceInfo.Stats().GetEvent(stats.ReadHeaderStart).IsNil())
+	assert.False(t, traceInfo.Stats().GetEvent(stats.ReadHeaderFinish).IsNil())
+	assert.False(t, traceInfo.Stats().GetEvent(stats.ReadBodyStart).IsNil())
+	assert.False(t, traceInfo.Stats().GetEvent(stats.ReadBodyFinish).IsNil())
+	assert.False(t, traceInfo.Stats().GetEvent(stats.ServerHandleStart).IsNil())
+	assert.False(t, traceInfo.Stats().GetEvent(stats.ServerHandleFinish).IsNil())
+	assert.False(t, traceInfo.Stats().GetEvent(stats.WriteStart).IsNil())
+	assert.False(t, traceInfo.Stats().GetEvent(stats.WriteFinish).IsNil())
+	assert.False(t, traceInfo.Stats().GetEvent(stats.HTTPFinish).IsNil())
+}
+
+func TestTraceEventReadHeaderError(t *testing.T) {
+	server := &Server{}
+	server.eventStackPool = pool
+	server.EnableTrace = true
+	reqCtx := &app.RequestContext{}
+	server.Core = &mockCore{
+		ctxPool: &sync.Pool{New: func() interface{} {
+			ti := traceinfo.NewTraceInfo()
+			ti.Stats().SetLevel(2)
+			reqCtx.SetTraceInfo(&mockTraceInfo{ti})
+			return reqCtx
+		}},
+		controller: &inStats.Controller{},
+	}
+	err := server.Serve(context.TODO(), mock.NewConn("ErrorFirstLine\r\n\r\n"))
+	assert.NotNil(t, err)
+	traceInfo := reqCtx.GetTraceInfo()
+	assert.False(t, traceInfo.Stats().GetEvent(stats.HTTPStart).IsNil())
+	assert.False(t, traceInfo.Stats().GetEvent(stats.ReadHeaderStart).IsNil())
+	assert.False(t, traceInfo.Stats().GetEvent(stats.ReadHeaderFinish).IsNil())
+	assert.Nil(t, traceInfo.Stats().GetEvent(stats.ReadBodyStart))
+	assert.Nil(t, traceInfo.Stats().GetEvent(stats.ReadBodyFinish))
+	assert.Nil(t, traceInfo.Stats().GetEvent(stats.ServerHandleStart))
+	assert.Nil(t, traceInfo.Stats().GetEvent(stats.ServerHandleFinish))
+	assert.Nil(t, traceInfo.Stats().GetEvent(stats.WriteStart))
+	assert.Nil(t, traceInfo.Stats().GetEvent(stats.WriteFinish))
+	assert.False(t, traceInfo.Stats().GetEvent(stats.HTTPFinish).IsNil())
+}
+
+func TestTraceEventReadBodyError(t *testing.T) {
+	server := &Server{}
+	server.eventStackPool = pool
+	server.EnableTrace = true
+	server.GetOnly = true
+	reqCtx := &app.RequestContext{}
+	server.Core = &mockCore{
+		ctxPool: &sync.Pool{New: func() interface{} {
+			ti := traceinfo.NewTraceInfo()
+			ti.Stats().SetLevel(2)
+			reqCtx.SetTraceInfo(&mockTraceInfo{ti})
+			return reqCtx
+		}},
+		controller: &inStats.Controller{},
+	}
+	err := server.Serve(context.TODO(), mock.NewConn("POST /aaa HTTP/1.1\nHost: foobar.com\nContent-Length: 5\nContent-Type: foo/bar\n\n12346\n\n"))
+	assert.NotNil(t, err)
+
+	traceInfo := reqCtx.GetTraceInfo()
+	assert.False(t, traceInfo.Stats().GetEvent(stats.HTTPStart).IsNil())
+	assert.False(t, traceInfo.Stats().GetEvent(stats.ReadHeaderStart).IsNil())
+	assert.False(t, traceInfo.Stats().GetEvent(stats.ReadHeaderFinish).IsNil())
+	assert.False(t, traceInfo.Stats().GetEvent(stats.ReadBodyStart).IsNil())
+	assert.False(t, traceInfo.Stats().GetEvent(stats.ReadBodyFinish).IsNil())
+	assert.Nil(t, traceInfo.Stats().GetEvent(stats.ServerHandleStart))
+	assert.Nil(t, traceInfo.Stats().GetEvent(stats.ServerHandleFinish))
+	assert.Nil(t, traceInfo.Stats().GetEvent(stats.WriteStart))
+	assert.Nil(t, traceInfo.Stats().GetEvent(stats.WriteFinish))
+	assert.False(t, traceInfo.Stats().GetEvent(stats.HTTPFinish).IsNil())
+}
+
+func TestTraceEventWriteError(t *testing.T) {
+	server := &Server{}
+	server.eventStackPool = pool
+	server.EnableTrace = true
+	reqCtx := &app.RequestContext{}
+	server.Core = &mockCore{
+		ctxPool: &sync.Pool{New: func() interface{} {
+			ti := traceinfo.NewTraceInfo()
+			ti.Stats().SetLevel(2)
+			reqCtx.SetTraceInfo(&mockTraceInfo{ti})
+			return reqCtx
+		}},
+		controller: &inStats.Controller{},
+	}
+	err := server.Serve(
+		context.TODO(),
+		&mockErrorWriter{
+			mock.NewConn("POST /aaa HTTP/1.1\nHost: foobar.com\nContent-Length: 5\nContent-Type: foo/bar\n\n12346\n\n"),
+		},
+	)
+	assert.NotNil(t, err)
+	traceInfo := reqCtx.GetTraceInfo()
+	assert.False(t, traceInfo.Stats().GetEvent(stats.HTTPStart).IsNil())
+	assert.False(t, traceInfo.Stats().GetEvent(stats.ReadHeaderStart).IsNil())
+	assert.False(t, traceInfo.Stats().GetEvent(stats.ReadHeaderFinish).IsNil())
+	assert.False(t, traceInfo.Stats().GetEvent(stats.ReadBodyStart).IsNil())
+	assert.False(t, traceInfo.Stats().GetEvent(stats.ReadBodyFinish).IsNil())
+	assert.False(t, traceInfo.Stats().GetEvent(stats.ServerHandleStart).IsNil())
+	assert.False(t, traceInfo.Stats().GetEvent(stats.ServerHandleFinish).IsNil())
+	assert.False(t, traceInfo.Stats().GetEvent(stats.WriteStart).IsNil())
+	assert.False(t, traceInfo.Stats().GetEvent(stats.WriteFinish).IsNil())
+	assert.False(t, traceInfo.Stats().GetEvent(stats.HTTPFinish).IsNil())
+}
+
+func TestEventStack(t *testing.T) {
+	// Create a stack.
+	s := &eventStack{}
+	assert.True(t, s.isEmpty())
+
+	count := 0
+
+	// Push 10 events.
+	for i := 0; i < 10; i++ {
+		s.push(func(ti traceinfo.TraceInfo, err error) {
+			count += 1
+		})
+	}
+
+	assert.False(t, s.isEmpty())
+	// Pop 10 events and process them.
+	for last := s.pop(); last != nil; last = s.pop() {
+		last(nil, nil)
+	}
+
+	assert.DeepEqual(t, 10, count)
+
+	// Pop an empty stack.
+	e := s.pop()
+	if e != nil {
+		t.Fatalf("should be nil")
+	}
+}
+
+type mockCore struct {
+	ctxPool    *sync.Pool
+	controller tracer.Controller
+}
+
+func (m *mockCore) IsRunning() bool {
+	return false
+}
+
+func (m *mockCore) GetCtxPool() *sync.Pool {
+	return m.ctxPool
+}
+
+func (m *mockCore) ServeHTTP(c context.Context, ctx *app.RequestContext) {}
+
+func (m *mockCore) GetTracer() tracer.Controller {
+	return m.controller
+}
+
+type mockTraceInfo struct {
+	traceinfo.TraceInfo
+}
+
+func (m *mockTraceInfo) Reset() {}
+
+type mockErrorWriter struct {
+	network.Conn
+}
+
+func (errorWriter *mockErrorWriter) Flush() error {
+	return errors.New("error")
+}
diff --git a/pkg/protocol/http1/server_timing_test.go b/pkg/protocol/http1/server_timing_test.go
new file mode 100644
index 0000000..144bb3e
--- /dev/null
+++ b/pkg/protocol/http1/server_timing_test.go
@@ -0,0 +1,56 @@
+/*
+ * 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 http1
+
+import (
+	"context"
+	"fmt"
+	"sync"
+	"testing"
+
+	inStats "github.com/cloudwego/hertz/internal/stats"
+	"github.com/cloudwego/hertz/pkg/app"
+	"github.com/cloudwego/hertz/pkg/common/test/mock"
+	"github.com/cloudwego/hertz/pkg/common/tracer/traceinfo"
+)
+
+func BenchmarkServer_Serve(b *testing.B) {
+	server := &Server{}
+	server.eventStackPool = &sync.Pool{
+		New: func() interface{} {
+			return &eventStack{}
+		},
+	}
+	server.EnableTrace = true
+	reqCtx := &app.RequestContext{}
+	server.Core = &mockCore{
+		ctxPool: &sync.Pool{New: func() interface{} {
+			ti := traceinfo.NewTraceInfo()
+			ti.Stats().SetLevel(2)
+			reqCtx.SetTraceInfo(&mockTraceInfo{ti})
+			return reqCtx
+		}},
+		controller: &inStats.Controller{},
+	}
+	err := server.Serve(context.TODO(), mock.NewConn("GET /aaa HTTP/1.1\nHost: foobar.com\n\n"))
+	if err != nil {
+		fmt.Println(err.Error())
+	}
+	for i := 0; i < b.N; i++ {
+		server.Serve(context.TODO(), mock.NewConn("GET /aaa HTTP/1.1\nHost: foobar.com\n\n"))
+	}
+}
diff --git a/pkg/protocol/request_test.go b/pkg/protocol/request_test.go
index 71fa8fb..850b73d 100644
--- a/pkg/protocol/request_test.go
+++ b/pkg/protocol/request_test.go
@@ -748,3 +748,18 @@ func testBodyWriteTo(t *testing.T, bw bodyWriterTo, expectedS string, isRetained
 		}
 	}
 }
+
+func TestReqSafeCopy(t *testing.T) {
+	req := AcquireRequest()
+	req.bodyRaw = make([]byte, 1)
+	reqs := make([]*Request, 10)
+	for i := 0; i < 10; i++ {
+		req.bodyRaw[0] = byte(i)
+		tmpReq := AcquireRequest()
+		req.CopyTo(tmpReq)
+		reqs[i] = tmpReq
+	}
+	for i := 0; i < 10; i++ {
+		assert.DeepEqual(t, []byte{byte(i)}, reqs[i].Body())
+	}
+}
diff --git a/pkg/protocol/response_test.go b/pkg/protocol/response_test.go
index 7d88e0b..1ecb806 100644
--- a/pkg/protocol/response_test.go
+++ b/pkg/protocol/response_test.go
@@ -257,3 +257,18 @@ func TestSetBodyStreamNoReset(t *testing.T) {
 	resp.SetBodyStream(bsC, 1)
 	assert.DeepEqual(t, bsA.String(), "")
 }
+
+func TestRespSafeCopy(t *testing.T) {
+	resp := AcquireResponse()
+	resp.bodyRaw = make([]byte, 1)
+	resps := make([]*Response, 10)
+	for i := 0; i < 10; i++ {
+		resp.bodyRaw[0] = byte(i)
+		tmpResq := AcquireResponse()
+		resp.CopyTo(tmpResq)
+		resps[i] = tmpResq
+	}
+	for i := 0; i < 10; i++ {
+		assert.DeepEqual(t, []byte{byte(i)}, resps[i].Body())
+	}
+}