Repository: cloudwego/hertz
Pull request #791 (2023-05-25 08:12:07)

chore: release v0.6.4


Patch:
diff --git a/.github/workflows/invalid_question.yml b/.github/workflows/invalid_question.yml
index 4be99f9..9589f42 100644
--- a/.github/workflows/invalid_question.yml
+++ b/.github/workflows/invalid_question.yml
@@ -18,9 +18,9 @@ jobs:
         uses: actions/stale@v6
         with:
           repo-token: ${{ secrets.GITHUB_TOKEN }}
-          stale-issue-message: "This issue has been marked as invalid question, please give more information by following the `issue` template. The issue will be closed in 3 days if no further activity occurs."
+          stale-issue-message: "This issue has been marked as invalid question, please give more information by following the `issue` template. The issue will be closed in 1 days if no further activity occurs."
           stale-issue-label: "stale"
           days-before-stale: 0
-          days-before-close: 3
+          days-before-close: 1
           remove-stale-when-updated: true
           only-labels: "invalid issue"
diff --git a/cmd/hz/app/app.go b/cmd/hz/app/app.go
index 3f40e91..4196709 100644
--- a/cmd/hz/app/app.go
+++ b/cmd/hz/app/app.go
@@ -178,6 +178,7 @@ func Init() *cli.App {
 	protoPluginsFlag := cli.StringSliceFlag{Name: "protoc-plugins", Usage: "Specify plugins for the protoc. ({plugin_name}:{options}:{out_dir})"}
 	noRecurseFlag := cli.BoolFlag{Name: "no_recurse", Usage: "Generate master model only.", Destination: &globalArgs.NoRecurse}
 	forceNewFlag := cli.BoolFlag{Name: "force", Aliases: []string{"f"}, Usage: "Force new a project, which will overwrite the generated files", Destination: &globalArgs.ForceNew}
+	enableExtendsFlag := cli.BoolFlag{Name: "enable_extends", Usage: "Parse 'extends' for thrift IDL", Destination: &globalArgs.EnableExtends}
 
 	jsonEnumStrFlag := cli.BoolFlag{Name: "json_enumstr", Usage: "Use string instead of num for json enums when idl is thrift.", Destination: &globalArgs.JSONEnumStr}
 	unsetOmitemptyFlag := cli.BoolFlag{Name: "unset_omitempty", Usage: "Remove 'omitempty' tag for generated struct.", Destination: &globalArgs.UnsetOmitempty}
@@ -223,6 +224,7 @@ func Init() *cli.App {
 				&optPkgFlag,
 				&noRecurseFlag,
 				&forceNewFlag,
+				&enableExtendsFlag,
 
 				&jsonEnumStrFlag,
 				&unsetOmitemptyFlag,
@@ -255,6 +257,7 @@ func Init() *cli.App {
 				&protoOptionsFlag,
 				&optPkgFlag,
 				&noRecurseFlag,
+				&enableExtendsFlag,
 
 				&jsonEnumStrFlag,
 				&unsetOmitemptyFlag,
@@ -306,12 +309,16 @@ func Init() *cli.App {
 				&thriftOptionsFlag,
 				&protoOptionsFlag,
 				&noRecurseFlag,
+				&enableExtendsFlag,
 
 				&jsonEnumStrFlag,
 				&unsetOmitemptyFlag,
 				&protoCamelJSONTag,
 				&snakeNameFlag,
 				&excludeFilesFlag,
+				&customLayout,
+				&customLayoutData,
+				&customPackage,
 				&protoPluginsFlag,
 				&thriftPluginsFlag,
 			},
diff --git a/cmd/hz/config/argument.go b/cmd/hz/config/argument.go
index 27e376c..8eef041 100644
--- a/cmd/hz/config/argument.go
+++ b/cmd/hz/config/argument.go
@@ -69,6 +69,7 @@ type Argument struct {
 	HandlerByMethod      bool
 	ForceNew             bool
 	SnakeStyleMiddleware bool
+	EnableExtends        bool
 
 	CustomizeLayout     string
 	CustomizeLayoutData string
diff --git a/cmd/hz/generator/custom_files.go b/cmd/hz/generator/custom_files.go
index 38851a3..390a40d 100644
--- a/cmd/hz/generator/custom_files.go
+++ b/cmd/hz/generator/custom_files.go
@@ -216,7 +216,14 @@ func appendUpdateFile(tplInfo *Template, renderInfo interface{}, fileContent []b
 	if err != nil {
 		return []byte(""), fmt.Errorf("write file(%s) failed, err: %v", tplInfo.Path, err)
 	}
-	_, err = buf.WriteString("\n" + appendContent + "\n")
+	// "\r\n" && "\n" has the same suffix
+	if !bytes.HasSuffix(buf.Bytes(), []byte("\n")) {
+		_, err = buf.WriteString("\n")
+		if err != nil {
+			return []byte(""), fmt.Errorf("write file(%s) line break failed, err: %v", tplInfo.Path, err)
+		}
+	}
+	_, err = buf.WriteString(appendContent)
 	if err != nil {
 		return []byte(""), fmt.Errorf("append file(%s) failed, err: %v", tplInfo.Path, err)
 	}
@@ -348,17 +355,34 @@ func (pkgGen *HttpPackageGenerator) genLoopService(tplInfo *Template, filePathRe
 						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)
+				if len(tplInfo.UpdateBehavior.AppendLocation) == 0 { // default, append to end of file
+					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 { // 'append location', append new content after 'append location'
+					part := bytes.Split(fileContent, []byte(tplInfo.UpdateBehavior.AppendLocation))
+					if len(part) == 0 {
+						return fmt.Errorf("can not find append location '%s' for file '%s'\n", tplInfo.UpdateBehavior.AppendLocation, filePath)
+					}
+					if len(part) != 2 {
+						return fmt.Errorf("do not support multiple append location '%s' for file '%s'\n", tplInfo.UpdateBehavior.AppendLocation, filePath)
+					}
+					buf := bytes.NewBuffer(nil)
+					err = writeBytes(buf, part[0], []byte(tplInfo.UpdateBehavior.AppendLocation), appendContent, part[1])
+					if err != nil {
+						return fmt.Errorf("write 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, ""})
 				}
-				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)
 			}
@@ -505,17 +529,34 @@ func (pkgGen *HttpPackageGenerator) genSingleCustomizedFile(tplInfo *Template, f
 						}
 					}
 				}
-				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)
+				if len(tplInfo.UpdateBehavior.AppendLocation) == 0 { // default, append to end of file
+					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 { // 'append location', append new content after 'append location'
+					part := bytes.Split(fileContent, []byte(tplInfo.UpdateBehavior.AppendLocation))
+					if len(part) == 0 {
+						return fmt.Errorf("can not find append location '%s' for file '%s'\n", tplInfo.UpdateBehavior.AppendLocation, filePath)
+					}
+					if len(part) != 2 {
+						return fmt.Errorf("do not support multiple append location '%s' for file '%s'\n", tplInfo.UpdateBehavior.AppendLocation, filePath)
+					}
+					buf := bytes.NewBuffer(nil)
+					err = writeBytes(buf, part[0], []byte(tplInfo.UpdateBehavior.AppendLocation), appendContent, part[1])
+					if err != nil {
+						return fmt.Errorf("write 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, ""})
 				}
-				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 {
@@ -550,17 +591,34 @@ func (pkgGen *HttpPackageGenerator) genSingleCustomizedFile(tplInfo *Template, f
 						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)
+				if len(tplInfo.UpdateBehavior.AppendLocation) == 0 { // default, append to end of file
+					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 { // 'append location', append new content after 'append location'
+					part := bytes.Split(fileContent, []byte(tplInfo.UpdateBehavior.AppendLocation))
+					if len(part) == 0 {
+						return fmt.Errorf("can not find append location '%s' for file '%s'\n", tplInfo.UpdateBehavior.AppendLocation, filePath)
+					}
+					if len(part) != 2 {
+						return fmt.Errorf("do not support multiple append location '%s' for file '%s'\n", tplInfo.UpdateBehavior.AppendLocation, filePath)
+					}
+					buf := bytes.NewBuffer(nil)
+					err = writeBytes(buf, part[0], []byte(tplInfo.UpdateBehavior.AppendLocation), appendContent, part[1])
+					if err != nil {
+						return fmt.Errorf("write 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, ""})
 				}
-				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,
@@ -581,3 +639,14 @@ func (pkgGen *HttpPackageGenerator) genSingleCustomizedFile(tplInfo *Template, f
 
 	return nil
 }
+
+func writeBytes(buf *bytes.Buffer, bytes ...[]byte) error {
+	for _, b := range bytes {
+		_, err := buf.Write(b)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
diff --git a/cmd/hz/generator/handler.go b/cmd/hz/generator/handler.go
index dca3282..cecb0d0 100644
--- a/cmd/hz/generator/handler.go
+++ b/cmd/hz/generator/handler.go
@@ -37,6 +37,9 @@ type HttpMethod struct {
 	Path            string
 	Serializer      string
 	OutputDir       string
+	RefPackage      string
+	RefPackageAlias string
+	ModelPackage    map[string]string
 	// Annotations     map[string]string
 	Models map[string]*model.Model
 }
@@ -76,13 +79,24 @@ func (pkgGen *HttpPackageGenerator) genHandler(pkg *HttpPackage, handlerDir, han
 				}
 			}
 		} else { // generate handler service
+			tmpHandlerDir := handlerDir
+			tmpHandlerPackage := handlerPackage
+			if len(s.ServiceGenDir) != 0 {
+				tmpHandlerDir = s.ServiceGenDir
+				tmpHandlerPackage = util.SubPackage(pkgGen.ProjPackage, tmpHandlerDir)
+			}
 			handler = Handler{
-				FilePath:    filepath.Join(handlerDir, util.ToSnakeCase(s.Name)+".go"),
-				PackageName: util.SplitPackage(handlerPackage, ""),
+				FilePath:    filepath.Join(tmpHandlerDir, util.ToSnakeCase(s.Name)+".go"),
+				PackageName: util.SplitPackage(tmpHandlerPackage, ""),
 				Methods:     s.Methods,
 				ProjPackage: pkgGen.ProjPackage,
 			}
 
+			for _, m := range s.Methods {
+				m.RefPackage = tmpHandlerPackage
+				m.RefPackageAlias = util.BaseName(tmpHandlerPackage, "")
+			}
+
 			if err := pkgGen.processHandler(&handler, root, "", "", false); err != nil {
 				return fmt.Errorf("generate handler %s failed, err: %v", handler.FilePath, err.Error())
 			}
@@ -145,6 +159,9 @@ func (pkgGen *HttpPackageGenerator) processHandler(handler *Handler, root *Route
 }
 
 func (pkgGen *HttpPackageGenerator) updateHandler(handler interface{}, handlerTpl, filePath string, noRepeat bool) error {
+	if pkgGen.tplsInfo[handlerTpl].Disable {
+		return nil
+	}
 	isExist, err := util.PathExist(filePath)
 	if err != nil {
 		return err
diff --git a/cmd/hz/generator/package.go b/cmd/hz/generator/package.go
index af8e950..9eb00ad 100644
--- a/cmd/hz/generator/package.go
+++ b/cmd/hz/generator/package.go
@@ -44,6 +44,8 @@ type Service struct {
 	ClientMethods []*ClientMethod
 	Models        []*model.Model // all dependency models
 	BaseDomain    string         // base domain for client code
+	ServiceGroup  string         // service level router group
+	ServiceGenDir string         // handler_dir for handler_by_service
 }
 
 // HttpPackageGenerator is used to record the configuration related to generating hertz http code.
@@ -61,6 +63,7 @@ type HttpPackageGenerator struct {
 	IdlClientDir   string // client dir for "client" command
 	ForceClientDir string // client dir without namespace for "client" command
 	BaseDomain     string // request domain for "client" command
+	ServiceGenDir  string
 
 	NeedModel            bool
 	HandlerByMethod      bool // generate handler files with method dimension
diff --git a/cmd/hz/generator/package_tpl.go b/cmd/hz/generator/package_tpl.go
index 1b9bcdc..b6f6055 100644
--- a/cmd/hz/generator/package_tpl.go
+++ b/cmd/hz/generator/package_tpl.go
@@ -829,7 +829,7 @@ func parseResponseBody(c *cli, res *response) (err error) {
 			}
 		} else {
 			jsonByte, jsonErr := json.Marshal(map[string]interface{}{
-				"status_code": res.rawResponse.StatusCode,
+				"status_code": res.rawResponse.StatusCode(),
 				"body":        string(res.bodyByte),
 			})
 			if jsonErr != nil {
diff --git a/cmd/hz/generator/router.go b/cmd/hz/generator/router.go
index 16378a8..54c6d21 100644
--- a/cmd/hz/generator/router.go
+++ b/cmd/hz/generator/router.go
@@ -218,6 +218,8 @@ func (routerNode *RouterNode) Insert(name string, method *HttpMethod, handlerTyp
 				c.HandlerPackageAlias = pkgAlias
 				c.Handler = pkgAlias + "." + method.Name
 				c.HandlerPackage = handlerPkg
+				method.RefPackage = c.HandlerPackage
+				method.RefPackageAlias = c.HandlerPackageAlias
 			} else { // generate handler by service
 				c.Handler = handlerType + "." + method.Name
 			}
@@ -290,6 +292,9 @@ var (
 )
 
 func (pkgGen *HttpPackageGenerator) updateRegister(pkg, rDir, pkgName string) error {
+	if pkgGen.tplsInfo[registerTplName].Disable {
+		return nil
+	}
 	register := RegisterInfo{
 		PackageName: filepath.Base(rDir),
 		DepPkgAlias: strings.ReplaceAll(pkgName, "/", "_"),
@@ -365,8 +370,10 @@ func (pkgGen *HttpPackageGenerator) genRouter(pkg *HttpPackage, root *RouterNode
 	// 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())
+	if !pkgGen.tplsInfo[routerTplName].Disable {
+		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())
+		}
 	}
 	if err := pkgGen.updateMiddlewareReg(router, middlewareTplName, filepath.Join(routerDir, "middleware.go")); err != nil {
 		return fmt.Errorf("generate middleware %s failed, err: %v", filepath.Join(routerDir, "middleware.go"), err.Error())
@@ -379,6 +386,9 @@ func (pkgGen *HttpPackageGenerator) genRouter(pkg *HttpPackage, root *RouterNode
 }
 
 func (pkgGen *HttpPackageGenerator) updateMiddlewareReg(router interface{}, middlewareTpl, filePath string) error {
+	if pkgGen.tplsInfo[middlewareTpl].Disable {
+		return nil
+	}
 	isExist, err := util.PathExist(filePath)
 	if err != nil {
 		return err
@@ -405,7 +415,11 @@ func (pkgGen *HttpPackageGenerator) updateMiddlewareReg(router interface{}, midd
 	}
 
 	for _, mw := range middlewareList {
-		if bytes.Contains(file, []byte(mw)) {
+		mwName := mw + "Mw"
+		if pkgGen.SnakeStyleMiddleware {
+			mwName = mw + "_mw"
+		}
+		if bytes.Contains(file, []byte(mwName)) {
 			continue
 		}
 		middlewareSingleTpl := pkgGen.tpls[middlewareSingleTplName]
diff --git a/cmd/hz/generator/template.go b/cmd/hz/generator/template.go
index d5eeca5..b339fbb 100644
--- a/cmd/hz/generator/template.go
+++ b/cmd/hz/generator/template.go
@@ -43,22 +43,24 @@ const (
 )
 
 type Template struct {
-	Default        bool           // Update command behavior; skip/cover/append
+	Default        bool           // Is it the default template
 	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
+	Disable        bool           `yaml:"disable"`         // Disable generating file, used to disable default package template
 	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
+	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
+	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
+	AppendLocation string   `yaml:"append_location"`    // AppendLocation specifies the location of append,  the default is the end of the file
 }
 
 // TemplateGenerator contains information about the output template
diff --git a/cmd/hz/meta/const.go b/cmd/hz/meta/const.go
index 0836c79..75cacca 100644
--- a/cmd/hz/meta/const.go
+++ b/cmd/hz/meta/const.go
@@ -19,7 +19,7 @@ package meta
 import "runtime"
 
 // Version hz version
-const Version = "v0.6.3"
+const Version = "v0.6.4"
 
 // Mode hz run modes
 type Mode int
diff --git a/cmd/hz/protobuf/ast.go b/cmd/hz/protobuf/ast.go
index c86d21c..2abe833 100644
--- a/cmd/hz/protobuf/ast.go
+++ b/cmd/hz/protobuf/ast.go
@@ -235,7 +235,7 @@ func astToService(ast *descriptorpb.FileDescriptorProto, resolver *Resolver, cmd
 	return out, nil
 }
 
-func getCompatibleAnnotation(options proto.Message, anno *protoimpl.ExtensionInfo, compatibleAnno *protoimpl.ExtensionInfo) interface{} {
+func getCompatibleAnnotation(options proto.Message, anno, compatibleAnno *protoimpl.ExtensionInfo) interface{} {
 	if proto.HasExtension(options, anno) {
 		return checkFirstOption(anno, options)
 	} else if proto.HasExtension(options, compatibleAnno) {
diff --git a/cmd/hz/thrift/ast.go b/cmd/hz/thrift/ast.go
index bbee88d..298bad0 100644
--- a/cmd/hz/thrift/ast.go
+++ b/cmd/hz/thrift/ast.go
@@ -20,6 +20,7 @@ import (
 	"fmt"
 	"strings"
 
+	"github.com/cloudwego/hertz/cmd/hz/config"
 	"github.com/cloudwego/hertz/cmd/hz/generator"
 	"github.com/cloudwego/hertz/cmd/hz/generator/model"
 	"github.com/cloudwego/hertz/cmd/hz/meta"
@@ -28,6 +29,7 @@ import (
 	"github.com/cloudwego/thriftgo/generator/golang"
 	"github.com/cloudwego/thriftgo/generator/golang/styles"
 	"github.com/cloudwego/thriftgo/parser"
+	"github.com/cloudwego/thriftgo/semantic"
 )
 
 /*---------------------------Import-----------------------------*/
@@ -48,12 +50,18 @@ func getGoPackage(ast *parser.Thrift, pkgMap map[string]string) string {
 
 /*---------------------------Service-----------------------------*/
 
-func astToService(ast *parser.Thrift, resolver *Resolver, cmdType string) ([]*generator.Service, error) {
+func astToService(ast *parser.Thrift, resolver *Resolver, args *config.Argument) ([]*generator.Service, error) {
 	ss := ast.GetServices()
 	out := make([]*generator.Service, 0, len(ss))
 	var models model.Models
-
+	extendServices := getExtendServices(ast)
 	for _, s := range ss {
+		// if the service is extended, it is not processed
+		if extendServices.exist(s.Name) && args.EnableExtends {
+			logs.Debugf("%s is extended, so skip it\n", s.Name)
+			continue
+		}
+
 		resolver.ExportReferred(true, false)
 		service := &generator.Service{
 			Name: s.GetName(),
@@ -61,12 +69,33 @@ func astToService(ast *parser.Thrift, resolver *Resolver, cmdType string) ([]*ge
 		service.BaseDomain = ""
 		domainAnno := getAnnotation(s.Annotations, ApiBaseDomain)
 		if len(domainAnno) == 1 {
-			if cmdType == meta.CmdClient {
+			if args.CmdType == meta.CmdClient {
 				service.BaseDomain = domainAnno[0]
 			}
 		}
-
+		service.ServiceGroup = ""
+		groupAnno := getAnnotation(s.Annotations, ApiServiceGroup)
+		if len(groupAnno) == 1 {
+			if args.CmdType != meta.CmdClient {
+				service.ServiceGroup = groupAnno[0]
+			}
+		}
+		service.ServiceGenDir = ""
+		serviceGenDirAnno := getAnnotation(s.Annotations, ApiServiceGenDir)
+		if len(serviceGenDirAnno) == 1 {
+			if args.CmdType != meta.CmdClient {
+				service.ServiceGenDir = serviceGenDirAnno[0]
+			}
+		}
 		ms := s.GetFunctions()
+		if len(s.Extends) != 0 && args.EnableExtends {
+			// all the services that are extended to the current service
+			extendsFuncs, err := getAllExtendFunction(s, ast, resolver, args)
+			if err != nil {
+				return nil, fmt.Errorf("parser extend function failed, err=%v", err)
+			}
+			ms = append(ms, extendsFuncs...)
+		}
 		methods := make([]*generator.HttpMethod, 0, len(ms))
 		clientMethods := make([]*generator.ClientMethod, 0, len(ms))
 		for _, m := range ms {
@@ -134,7 +163,7 @@ func astToService(ast *parser.Thrift, resolver *Resolver, cmdType string) ([]*ge
 			}
 			models.MergeMap(method.Models)
 			methods = append(methods, method)
-			if cmdType == meta.CmdClient {
+			if args.CmdType == meta.CmdClient {
 				clientMethod := &generator.ClientMethod{}
 				clientMethod.HttpMethod = method
 				rt, err := resolver.ResolveIdentifier(m.Arguments[0].GetType().GetName())
@@ -250,6 +279,166 @@ func parseAnnotationToClient(clientMethod *generator.ClientMethod, p *parser.Typ
 	return nil
 }
 
+type extendServiceList []string
+
+func (svr extendServiceList) exist(serviceName string) bool {
+	for _, s := range svr {
+		if s == serviceName {
+			return true
+		}
+	}
+	return false
+}
+
+func getExtendServices(ast *parser.Thrift) (res extendServiceList) {
+	for a := range ast.DepthFirstSearch() {
+		for _, svc := range a.Services {
+			if len(svc.Extends) > 0 {
+				res = append(res, svc.Extends)
+			}
+		}
+	}
+	return
+}
+
+func getAllExtendFunction(svc *parser.Service, ast *parser.Thrift, resolver *Resolver, args *config.Argument) (res []*parser.Function, err error) {
+	if len(svc.Extends) == 0 {
+		return
+	}
+	parts := semantic.SplitType(svc.Extends)
+	switch len(parts) {
+	case 1:
+		if resolver.mainPkg.Ast.Filename == ast.Filename { // extended current service for master IDL
+			extendSvc, found := ast.GetService(parts[0])
+			if found {
+				funcs := extendSvc.GetFunctions()
+				// determine if it still has extends
+				extendFuncs, err := getAllExtendFunction(extendSvc, ast, resolver, args)
+				if err != nil {
+					return nil, err
+				}
+				res = append(res, append(funcs, extendFuncs...)...)
+			}
+			return res, nil
+		} else { // extended current service for other IDL
+			extendSvc, found := ast.GetService(parts[0])
+			if found {
+				base, err := addResolverDependency(resolver, ast, args)
+				if err != nil {
+					return nil, err
+				}
+				funcs := extendSvc.GetFunctions()
+				for _, f := range funcs {
+					// the method of other file is extended, and the package of req/resp needs to be changed
+					// ex. base.thrift -> Resp Method(Req){}
+					//					  base.Resp Method(base.Req){}
+					// todo: support container for Struct
+					if len(f.Arguments) > 0 {
+						if !strings.Contains(f.Arguments[0].Type.Name, ".") && f.Arguments[0].Type.Category.IsStruct() {
+							f.Arguments[0].Type.Name = base + "." + f.Arguments[0].Type.Name
+						}
+					}
+					if !strings.Contains(f.FunctionType.Name, ".") && f.FunctionType.Category.IsStruct() {
+						f.FunctionType.Name = base + "." + f.FunctionType.Name
+					}
+				}
+				extendFuncs, err := getAllExtendFunction(extendSvc, ast, resolver, args)
+				if err != nil {
+					return nil, err
+				}
+				res = append(res, append(funcs, extendFuncs...)...)
+			}
+			return res, nil
+		}
+	case 2:
+		refAst, found := ast.GetReference(parts[0])
+		base, err := addResolverDependency(resolver, refAst, args)
+		if err != nil {
+			return nil, err
+		}
+		// ff the service extends from other files, it has to resolve the dependencies of other files as well
+		for _, dep := range refAst.Includes {
+			_, err := addResolverDependency(resolver, dep.Reference, args)
+			if err != nil {
+				return nil, err
+			}
+		}
+		if found {
+			extendSvc, found := refAst.GetService(parts[1])
+			if found {
+				funcs := extendSvc.GetFunctions()
+				for _, f := range funcs {
+					// the method of other file is extended, and the package of req/resp needs to be changed
+					// ex. base.thrift -> Resp Method(Req){}
+					//					  base.Resp Method(base.Req){}
+					// todo: support container for Struct
+					if len(f.Arguments) > 0 {
+						if !strings.Contains(f.Arguments[0].Type.Name, ".") && f.Arguments[0].Type.Category.IsStruct() {
+							f.Arguments[0].Type.Name = base + "." + f.Arguments[0].Type.Name
+						}
+					}
+					if !strings.Contains(f.FunctionType.Name, ".") && f.FunctionType.Category.IsStruct() {
+						f.FunctionType.Name = base + "." + f.FunctionType.Name
+					}
+				}
+				extendFuncs, err := getAllExtendFunction(extendSvc, refAst, resolver, args)
+				if err != nil {
+					return nil, err
+				}
+				res = append(res, append(funcs, extendFuncs...)...)
+			}
+		}
+		return res, nil
+	}
+
+	return res, nil
+}
+
+func getUniqueResolveDependentName(name string, resolver *Resolver) string {
+	rawName := name
+	for i := 0; i < 10000; i++ {
+		if _, exist := resolver.deps[name]; !exist {
+			return name
+		}
+		name = rawName + fmt.Sprint(i)
+	}
+
+	return name
+}
+
+func addResolverDependency(resolver *Resolver, ast *parser.Thrift, args *config.Argument) (string, error) {
+	namespace, err := resolver.LoadOne(ast)
+	if err != nil {
+		return "", err
+	}
+	baseName := util.BaseName(ast.Filename, ".thrift")
+	if refPkg, exist := resolver.refPkgs[baseName]; !exist {
+		resolver.deps[baseName] = namespace
+	} else {
+		if ast.Filename != refPkg.Ast.Filename {
+			baseName = getUniqueResolveDependentName(baseName, resolver)
+			resolver.deps[baseName] = namespace
+		}
+	}
+	pkg := getGoPackage(ast, args.OptPkgMap)
+	impt := ast.Filename
+	pkgName := util.SplitPackageName(pkg, "")
+	pkgName, err = util.GetPackageUniqueName(pkgName)
+	if err != nil {
+		return "", err
+	}
+	ref := &PackageReference{baseName, impt, &model.Model{
+		FilePath:    ast.Filename,
+		Package:     pkg,
+		PackageName: pkgName,
+	}, ast, false}
+	if _, exist := resolver.refPkgs[baseName]; !exist {
+		resolver.refPkgs[baseName] = ref
+	}
+
+	return baseName, nil
+}
+
 /*---------------------------Model-----------------------------*/
 
 var BaseThrift = parser.Thrift{}
diff --git a/cmd/hz/thrift/plugin.go b/cmd/hz/thrift/plugin.go
index 99d80b4..23c5c75 100644
--- a/cmd/hz/thrift/plugin.go
+++ b/cmd/hz/thrift/plugin.go
@@ -293,7 +293,7 @@ func (plugin *Plugin) getPackageInfo() (*generator.HttpPackage, error) {
 		return nil, fmt.Errorf("go package for '%s' is not defined", ast.GetFilename())
 	}
 
-	services, err := astToService(ast, rs, args.CmdType)
+	services, err := astToService(ast, rs, args)
 	if err != nil {
 		return nil, err
 	}
diff --git a/cmd/hz/thrift/tags.go b/cmd/hz/thrift/tags.go
index 16a0696..5b2e8b6 100644
--- a/cmd/hz/thrift/tags.go
+++ b/cmd/hz/thrift/tags.go
@@ -61,7 +61,9 @@ const (
 )
 
 const (
-	ApiBaseDomain = "api.base_domain"
+	ApiBaseDomain    = "api.base_domain"
+	ApiServiceGroup  = "api.service_group"
+	ApiServiceGenDir = "api.service_gen_dir"
 )
 
 var (
diff --git a/pkg/app/server/hertz.go b/pkg/app/server/hertz.go
index 72a19c8..46baef1 100644
--- a/pkg/app/server/hertz.go
+++ b/pkg/app/server/hertz.go
@@ -18,7 +18,6 @@ package server
 
 import (
 	"context"
-	"errors"
 	"os"
 	"os/signal"
 	"syscall"
@@ -26,6 +25,7 @@ import (
 
 	"github.com/cloudwego/hertz/pkg/app/middlewares/server/recovery"
 	"github.com/cloudwego/hertz/pkg/common/config"
+	"github.com/cloudwego/hertz/pkg/common/errors"
 	"github.com/cloudwego/hertz/pkg/common/hlog"
 	"github.com/cloudwego/hertz/pkg/route"
 )
@@ -108,7 +108,7 @@ func waitSignal(errCh chan error) error {
 		switch sig {
 		case syscall.SIGTERM:
 			// force exit
-			return errors.New(sig.String()) // nolint
+			return errors.NewPublic(sig.String()) // nolint
 		case syscall.SIGHUP, syscall.SIGINT:
 			hlog.SystemLogger().Infof("Received signal: %s\n", sig)
 			// graceful shutdown
diff --git a/pkg/common/errors/errors.go b/pkg/common/errors/errors.go
index b8d23ff..fdaf2d5 100644
--- a/pkg/common/errors/errors.go
+++ b/pkg/common/errors/errors.go
@@ -63,6 +63,7 @@ var (
 	ErrNoFreeConns        = errors.New("no free connections available to host")
 	ErrConnectionClosed   = errors.New("connection closed")
 	ErrNotSupportProtocol = errors.New("not support protocol")
+	ErrNoMultipartForm    = errors.New("request has no multipart/form-data Content-Type")
 )
 
 // ErrorType is an unsigned 64-bit error code as defined in the hertz spec.
diff --git a/pkg/common/stackless/writer.go b/pkg/common/stackless/writer.go
index 3ebc3b1..c59b8d3 100644
--- a/pkg/common/stackless/writer.go
+++ b/pkg/common/stackless/writer.go
@@ -42,11 +42,11 @@
 package stackless
 
 import (
-	"errors"
 	"fmt"
 	"io"
 
 	"github.com/cloudwego/hertz/pkg/common/bytebufferpool"
+	"github.com/cloudwego/hertz/pkg/common/errors"
 )
 
 // Writer is an interface stackless writer must conform to.
@@ -137,7 +137,7 @@ func (w *writer) do(op op) error {
 	return err
 }
 
-var errHighLoad = errors.New("cannot compress data due to high load")
+var errHighLoad = errors.NewPublic("cannot compress data due to high load")
 
 var stacklessWriterFunc = NewFunc(writerFunc)
 
diff --git a/pkg/common/tracer/stats/event.go b/pkg/common/tracer/stats/event.go
index 6c5fbdc..29a5b32 100644
--- a/pkg/common/tracer/stats/event.go
+++ b/pkg/common/tracer/stats/event.go
@@ -17,9 +17,10 @@
 package stats
 
 import (
-	"errors"
 	"sync"
 	"sync/atomic"
+
+	"github.com/cloudwego/hertz/pkg/common/errors"
 )
 
 // EventIndex indicates a unique event.
@@ -88,8 +89,8 @@ var (
 
 // errors
 var (
-	ErrNotAllowed = errors.New("event definition is not allowed after initialization")
-	ErrDuplicated = errors.New("event name is already defined")
+	ErrNotAllowed = errors.NewPublic("event definition is not allowed after initialization")
+	ErrDuplicated = errors.NewPublic("event name is already defined")
 )
 
 var (
diff --git a/pkg/protocol/consts/headers.go b/pkg/protocol/consts/headers.go
index d01af5a..3c2b82e 100644
--- a/pkg/protocol/consts/headers.go
+++ b/pkg/protocol/consts/headers.go
@@ -85,4 +85,79 @@ const (
 	HTTP11 = "HTTP/1.1"
 	HTTP10 = "HTTP/1.0"
 	HTTP20 = "HTTP/2.0"
+
+	// MIME text
+	MIMETextPlain             = "text/plain"
+	MIMETextPlainUTF8         = "text/plain; charset=utf-8"
+	MIMETextPlainISO88591     = "text/plain; charset=iso-8859-1"
+	MIMETextPlainFormatFlowed = "text/plain; format=flowed"
+	MIMETextPlainDelSpaceYes  = "text/plain; delsp=yes"
+	MiMETextPlainDelSpaceNo   = "text/plain; delsp=no"
+	MIMETextHtml              = "text/html"
+	MIMETextCss               = "text/css"
+	MIMETextJavascript        = "text/javascript"
+
+	// MIME application
+	MIMEApplicationOctetStream  = "application/octet-stream"
+	MIMEApplicationFlash        = "application/x-shockwave-flash"
+	MIMEApplicationHTMLForm     = "application/x-www-form-urlencoded"
+	MIMEApplicationHTMLFormUTF8 = "application/x-www-form-urlencoded; charset=UTF-8"
+	MIMEApplicationTar          = "application/x-tar"
+	MIMEApplicationGZip         = "application/gzip"
+	MIMEApplicationXGZip        = "application/x-gzip"
+	MIMEApplicationBZip2        = "application/bzip2"
+	MIMEApplicationXBZip2       = "application/x-bzip2"
+	MIMEApplicationShell        = "application/x-sh"
+	MIMEApplicationDownload     = "application/x-msdownload"
+	MIMEApplicationJSON         = "application/json"
+	MIMEApplicationJSONUTF8     = "application/json; charset=utf-8"
+	MIMEApplicationXML          = "application/xml"
+	MIMEApplicationXMLUTF8      = "application/xml; charset=utf-8"
+	MIMEApplicationZip          = "application/zip"
+	MIMEApplicationPdf          = "application/pdf"
+	MIMEApplicationWord         = "application/msword"
+	MIMEApplicationExcel        = "application/vnd.ms-excel"
+	MIMEApplicationPPT          = "application/vnd.ms-powerpoint"
+	MIMEApplicationOpenXMLWord  = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
+	MIMEApplicationOpenXMLExcel = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
+	MIMEApplicationOpenXMLPPT   = "application/vnd.openxmlformats-officedocument.presentationml.presentation"
+
+	// MIME image
+	MIMEImageJPEG         = "image/jpeg"
+	MIMEImagePNG          = "image/png"
+	MIMEImageGIF          = "image/gif"
+	MIMEImageBitmap       = "image/bmp"
+	MIMEImageWebP         = "image/webp"
+	MIMEImageIco          = "image/x-icon"
+	MIMEImageMicrosoftICO = "image/vnd.microsoft.icon"
+	MIMEImageTIFF         = "image/tiff"
+	MIMEImageSVG          = "image/svg+xml"
+	MIMEImagePhotoshop    = "image/vnd.adobe.photoshop"
+
+	// MIME audio
+	MIMEAudioBasic     = "audio/basic"
+	MIMEAudioL24       = "audio/L24"
+	MIMEAudioMP3       = "audio/mp3"
+	MIMEAudioMP4       = "audio/mp4"
+	MIMEAudioMPEG      = "audio/mpeg"
+	MIMEAudioOggVorbis = "audio/ogg"
+	MIMEAudioWAVE      = "audio/vnd.wave"
+	MIMEAudioWebM      = "audio/webm"
+	MIMEAudioAAC       = "audio/x-aac"
+	MIMEAudioAIFF      = "audio/x-aiff"
+	MIMEAudioMIDI      = "audio/x-midi"
+	MIMEAudioM3U       = "audio/x-mpegurl"
+	MIMEAudioRealAudio = "audio/x-pn-realaudio"
+
+	// MIME video
+	MIMEVideoMPEG          = "video/mpeg"
+	MIMEVideoOgg           = "video/ogg"
+	MIMEVideoMP4           = "video/mp4"
+	MIMEVideoQuickTime     = "video/quicktime"
+	MIMEVideoWinMediaVideo = "video/x-ms-wmv"
+	MIMEVideWebM           = "video/webm"
+	MIMEVideoFlashVideo    = "video/x-flv"
+	MIMEVideo3GPP          = "video/3gpp"
+	MIMEVideoAVI           = "video/x-msvideo"
+	MIMEVideoMatroska      = "video/x-matroska"
 )
diff --git a/pkg/protocol/http1/proxy/proxy.go b/pkg/protocol/http1/proxy/proxy.go
index faf6803..f8bae76 100644
--- a/pkg/protocol/http1/proxy/proxy.go
+++ b/pkg/protocol/http1/proxy/proxy.go
@@ -28,11 +28,11 @@ import (
 	"context"
 	"crypto/tls"
 	"encoding/base64"
-	"errors"
 	"time"
 
 	"github.com/cloudwego/hertz/internal/bytesconv"
 	"github.com/cloudwego/hertz/internal/bytestr"
+	"github.com/cloudwego/hertz/pkg/common/errors"
 	"github.com/cloudwego/hertz/pkg/network"
 	"github.com/cloudwego/hertz/pkg/protocol"
 	"github.com/cloudwego/hertz/pkg/protocol/consts"
@@ -111,7 +111,7 @@ func SetupProxy(conn network.Conn, addr string, proxyURI *protocol.URI, tlsConfi
 		if connectResp.StatusCode() != consts.StatusOK {
 			conn.Close()
 
-			return nil, errors.New(consts.StatusMessage(connectResp.StatusCode()))
+			return nil, errors.NewPublic(consts.StatusMessage(connectResp.StatusCode()))
 		}
 	}
 
diff --git a/pkg/protocol/request.go b/pkg/protocol/request.go
index d10edd2..af29db1 100644
--- a/pkg/protocol/request.go
+++ b/pkg/protocol/request.go
@@ -65,8 +65,7 @@ import (
 )
 
 var (
-	errMissingFile     = errors.NewPublic("http: no such file")
-	errNoMultipartForm = errors.NewPublic("request has no multipart/form-data Content-Type")
+	errMissingFile = errors.NewPublic("http: no such file")
 
 	responseBodyPool bytebufferpool.Pool
 	requestBodyPool  bytebufferpool.Pool
@@ -217,7 +216,7 @@ func (req *Request) PostArgString() []byte {
 
 // MultipartForm returns request's multipart form.
 //
-// Returns errNoMultipartForm if request's Content-Type
+// Returns errors.ErrNoMultipartForm if request's Content-Type
 // isn't 'multipart/form-data'.
 //
 // RemoveMultipartFormFiles must be called after returned multipart form
@@ -228,7 +227,7 @@ func (req *Request) MultipartForm() (*multipart.Form, error) {
 	}
 	req.multipartFormBoundary = string(req.Header.MultipartFormBoundary())
 	if len(req.multipartFormBoundary) == 0 {
-		return nil, errNoMultipartForm
+		return nil, errors.ErrNoMultipartForm
 	}
 
 	ce := req.Header.peek(bytestr.StrContentEncoding)
diff --git a/version.go b/version.go
index 5428b83..bf3cf79 100644
--- a/version.go
+++ b/version.go
@@ -19,5 +19,5 @@ package hertz
 // Name and Version info of this framework, used for statistics and debug
 const (
 	Name    = "Hertz"
-	Version = "v0.6.3"
+	Version = "v0.6.4"
 )


Test patch:
diff --git a/pkg/app/client/client_test.go b/pkg/app/client/client_test.go
index 8862d84..84c6a91 100644
--- a/pkg/app/client/client_test.go
+++ b/pkg/app/client/client_test.go
@@ -975,7 +975,7 @@ func TestClientFollowRedirects(t *testing.T) {
 			u.Update("/bar")
 			ctx.Redirect(consts.StatusFound, u.FullURI())
 		default:
-			ctx.SetContentType("text/plain")
+			ctx.SetContentType(consts.MIMETextPlain)
 			ctx.Response.SetBody(ctx.Path())
 		}
 	}
@@ -1440,13 +1440,13 @@ func TestSetMultipartFields(t *testing.T) {
 		{
 			Param:       "file_1",
 			FileName:    files[0],
-			ContentType: "application/json",
+			ContentType: consts.MIMEApplicationJSON,
 			Reader:      strings.NewReader(jsonStr1),
 		},
 		{
 			Param:       "file_2",
 			FileName:    files[1],
-			ContentType: "application/json",
+			ContentType: consts.MIMEApplicationJSON,
 			Reader:      strings.NewReader(jsonStr2),
 		},
 	}
diff --git a/pkg/app/context_test.go b/pkg/app/context_test.go
index 31d6da0..7853d2a 100644
--- a/pkg/app/context_test.go
+++ b/pkg/app/context_test.go
@@ -431,7 +431,7 @@ tailfoobar`
 			t.Fatalf("unexpected filename %q. Expecting %q", v.Filename, "TODO")
 		}
 		ct := v.Header.Get("Content-Type")
-		if ct != "application/octet-stream" {
+		if ct != consts.MIMEApplicationOctetStream {
 			t.Fatalf("unexpected content-type %q. Expecting %q", ct, "application/octet-stream")
 		}
 	}
@@ -472,7 +472,7 @@ func TestContextRenderFileFromFS(t *testing.T) {
 
 	assert.DeepEqual(t, consts.StatusOK, ctx.Response.StatusCode())
 	assert.True(t, strings.Contains(resp.GetHTTP1Response(&ctx.Response).String(), "func (fs *FS) initRequestHandler() {"))
-	assert.DeepEqual(t, "text/plain; charset=utf-8", string(ctx.Response.Header.Peek("Content-Type")))
+	assert.DeepEqual(t, consts.MIMETextPlainUTF8, string(ctx.Response.Header.Peek("Content-Type")))
 	assert.DeepEqual(t, "/some/path", string(ctx.Request.URI().Path()))
 }
 
@@ -489,7 +489,7 @@ func TestContextRenderFile(t *testing.T) {
 
 	assert.DeepEqual(t, consts.StatusOK, ctx.Response.StatusCode())
 	assert.True(t, strings.Contains(resp.GetHTTP1Response(&ctx.Response).String(), "func (fs *FS) initRequestHandler() {"))
-	assert.DeepEqual(t, "text/plain; charset=utf-8", string(ctx.Response.Header.Peek("Content-Type")))
+	assert.DeepEqual(t, consts.MIMETextPlainUTF8, string(ctx.Response.Header.Peek("Content-Type")))
 }
 
 func TestContextRenderAttachment(t *testing.T) {
@@ -625,7 +625,7 @@ func TestRender(t *testing.T) {
 	c := NewContext(0)
 
 	c.Render(consts.StatusOK, &render.Data{
-		ContentType: "application/json; charset=utf-8",
+		ContentType: consts.MIMEApplicationJSONUTF8,
 		Data:        []byte("{\"test\":1}"),
 	})
 
@@ -666,8 +666,8 @@ func TestContextReset(t *testing.T) {
 
 func TestContextContentType(t *testing.T) {
 	c := NewContext(0)
-	c.Request.Header.Set("Content-Type", "application/json; charset=utf-8")
-	assert.DeepEqual(t, "application/json; charset=utf-8", bytesconv.B2s(c.ContentType()))
+	c.Request.Header.Set("Content-Type", consts.MIMEApplicationJSONUTF8)
+	assert.DeepEqual(t, consts.MIMEApplicationJSONUTF8, bytesconv.B2s(c.ContentType()))
 }
 
 func TestClientIp(t *testing.T) {
@@ -735,7 +735,7 @@ func TestGetQuery(t *testing.T) {
 
 func TestGetPostForm(t *testing.T) {
 	c := NewContext(0)
-	c.Request.Header.SetContentTypeBytes([]byte("application/x-www-form-urlencoded"))
+	c.Request.Header.SetContentTypeBytes([]byte(consts.MIMEApplicationHTMLForm))
 	c.Request.SetBodyString("a=1&b=")
 	v, exists := c.GetPostForm("b")
 	assert.DeepEqual(t, "", v)
@@ -794,7 +794,7 @@ func TestContextAbortWithStatusJSON(t *testing.T) {
 	assert.True(t, c.IsAborted())
 
 	contentType := c.Response.Header.Peek("Content-Type")
-	assert.DeepEqual(t, "application/json; charset=utf-8", string(contentType))
+	assert.DeepEqual(t, consts.MIMEApplicationJSONUTF8, string(contentType))
 
 	jsonStringBody := c.Response.Body()
 	assert.DeepEqual(t, "{\"foo\":\"fooValue\",\"bar\":\"barValue\"}", string(jsonStringBody))
@@ -803,7 +803,7 @@ func TestContextAbortWithStatusJSON(t *testing.T) {
 func TestRequestCtxFormValue(t *testing.T) {
 	ctx := NewContext(0)
 	ctx.Request.SetRequestURI("/foo/bar?baz=123&aaa=bbb")
-	ctx.Request.Header.SetContentTypeBytes([]byte("application/x-www-form-urlencoded"))
+	ctx.Request.Header.SetContentTypeBytes([]byte(consts.MIMEApplicationHTMLForm))
 	ctx.Request.SetBodyString("qqq=port&mmm=sddd")
 
 	v := ctx.FormValue("baz")
@@ -831,7 +831,7 @@ func TestRequestCtxFormValue(t *testing.T) {
 func TestSetCustomFormValueFunc(t *testing.T) {
 	ctx := NewContext(0)
 	ctx.Request.SetRequestURI("/foo/bar?aaa=bbb")
-	ctx.Request.Header.SetContentTypeBytes([]byte("application/x-www-form-urlencoded"))
+	ctx.Request.Header.SetContentTypeBytes([]byte(consts.MIMEApplicationHTMLForm))
 	ctx.Request.SetBodyString("aaa=port")
 
 	ctx.SetFormValueFunc(func(ctx *RequestContext, key string) []byte {
@@ -1154,9 +1154,9 @@ func TestReset(t *testing.T) {
 
 func TestGetHeader(t *testing.T) {
 	ctx := NewContext(0)
-	ctx.Request.Header.SetContentTypeBytes([]byte("text/plain; charset=utf-8"))
+	ctx.Request.Header.SetContentTypeBytes([]byte(consts.MIMETextPlainUTF8))
 	val := ctx.GetHeader("Content-Type")
-	assert.DeepEqual(t, "text/plain; charset=utf-8", string(val))
+	assert.DeepEqual(t, consts.MIMETextPlainUTF8, string(val))
 }
 
 func TestGetRawData(t *testing.T) {
diff --git a/pkg/app/fs_test.go b/pkg/app/fs_test.go
index 389fe9e..1cd545e 100644
--- a/pkg/app/fs_test.go
+++ b/pkg/app/fs_test.go
@@ -655,7 +655,7 @@ func TestServeFileContentType(t *testing.T) {
 		t.Fatalf("unexpected error: %s", err)
 	}
 
-	expected := []byte("image/png")
+	expected := []byte(consts.MIMEImagePNG)
 	if !bytes.Equal(r.Header.ContentType(), expected) {
 		t.Fatalf("Unexpected Content-Type, expected: %q got %q", expected, r.Header.ContentType())
 	}
diff --git a/pkg/app/server/render/render_test.go b/pkg/app/server/render/render_test.go
index bc839e0..338d151 100644
--- a/pkg/app/server/render/render_test.go
+++ b/pkg/app/server/render/render_test.go
@@ -48,6 +48,7 @@ import (
 	"github.com/bytedance/sonic"
 	"github.com/cloudwego/hertz/pkg/common/test/assert"
 	"github.com/cloudwego/hertz/pkg/protocol"
+	"github.com/cloudwego/hertz/pkg/protocol/consts"
 )
 
 type xmlmap map[string]interface{}
@@ -82,13 +83,13 @@ func TestRenderJSON(t *testing.T) {
 	}
 
 	(JSONRender{data}).WriteContentType(resp)
-	assert.DeepEqual(t, []byte("application/json; charset=utf-8"), resp.Header.Peek("Content-Type"))
+	assert.DeepEqual(t, []byte(consts.MIMEApplicationJSONUTF8), resp.Header.Peek("Content-Type"))
 
 	err := (JSONRender{data}).Render(resp)
 
 	assert.Nil(t, err)
 	assert.DeepEqual(t, []byte("{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}"), resp.Body())
-	assert.DeepEqual(t, []byte("application/json; charset=utf-8"), resp.Header.Peek("Content-Type"))
+	assert.DeepEqual(t, []byte(consts.MIMEApplicationJSONUTF8), resp.Header.Peek("Content-Type"))
 }
 
 func TestRenderJSONError(t *testing.T) {
@@ -106,7 +107,7 @@ func TestRenderString(t *testing.T) {
 		Format: "hello %s %d",
 		Data:   []interface{}{},
 	}).WriteContentType(resp)
-	assert.DeepEqual(t, []byte("text/plain; charset=utf-8"), resp.Header.Peek("Content-Type"))
+	assert.DeepEqual(t, []byte(consts.MIMETextPlainUTF8), resp.Header.Peek("Content-Type"))
 
 	err := (String{
 		Format: "hola %s %d",
@@ -115,7 +116,7 @@ func TestRenderString(t *testing.T) {
 
 	assert.Nil(t, err)
 	assert.DeepEqual(t, []byte("hola manu 2"), resp.Body())
-	assert.DeepEqual(t, []byte("text/plain; charset=utf-8"), resp.Header.Peek("Content-Type"))
+	assert.DeepEqual(t, []byte(consts.MIMETextPlainUTF8), resp.Header.Peek("Content-Type"))
 }
 
 func TestRenderStringLenZero(t *testing.T) {
@@ -128,7 +129,7 @@ func TestRenderStringLenZero(t *testing.T) {
 
 	assert.Nil(t, err)
 	assert.DeepEqual(t, []byte("hola %s %d"), resp.Body())
-	assert.DeepEqual(t, []byte("text/plain; charset=utf-8"), resp.Header.Peek("Content-Type"))
+	assert.DeepEqual(t, []byte(consts.MIMETextPlainUTF8), resp.Header.Peek("Content-Type"))
 }
 
 func TestRenderData(t *testing.T) {
@@ -142,7 +143,7 @@ func TestRenderData(t *testing.T) {
 
 	assert.Nil(t, err)
 	assert.DeepEqual(t, []byte("#!PNG some raw data"), resp.Body())
-	assert.DeepEqual(t, []byte("image/png"), resp.Header.Peek("Content-Type"))
+	assert.DeepEqual(t, []byte(consts.MIMEImagePNG), resp.Header.Peek("Content-Type"))
 }
 
 func TestRenderXML(t *testing.T) {
@@ -152,13 +153,13 @@ func TestRenderXML(t *testing.T) {
 	}
 
 	(XML{data}).WriteContentType(resp)
-	assert.DeepEqual(t, []byte("application/xml; charset=utf-8"), resp.Header.Peek("Content-Type"))
+	assert.DeepEqual(t, []byte(consts.MIMEApplicationXMLUTF8), resp.Header.Peek("Content-Type"))
 
 	err := (XML{data}).Render(resp)
 
 	assert.Nil(t, err)
 	assert.DeepEqual(t, []byte("<map><foo>bar</foo></map>"), resp.Body())
-	assert.DeepEqual(t, []byte("application/xml; charset=utf-8"), resp.Header.Peek("Content-Type"))
+	assert.DeepEqual(t, []byte(consts.MIMEApplicationXMLUTF8), resp.Header.Peek("Content-Type"))
 }
 
 func TestRenderIndentedJSON(t *testing.T) {
@@ -169,7 +170,7 @@ func TestRenderIndentedJSON(t *testing.T) {
 	t.Run("TestHeader", func(t *testing.T) {
 		resp := &protocol.Response{}
 		(IndentedJSON{data}).WriteContentType(resp)
-		assert.DeepEqual(t, []byte("application/json; charset=utf-8"), resp.Header.Peek("Content-Type"))
+		assert.DeepEqual(t, []byte(consts.MIMEApplicationJSONUTF8), resp.Header.Peek("Content-Type"))
 	})
 	t.Run("TestBody", func(t *testing.T) {
 		ResetStdJSONMarshal()
@@ -177,7 +178,7 @@ func TestRenderIndentedJSON(t *testing.T) {
 		err := (IndentedJSON{data}).Render(resp)
 		assert.Nil(t, err)
 		assert.DeepEqual(t, []byte("{\n    \"foo\": \"bar\",\n    \"html\": \"h1\"\n}"), resp.Body())
-		assert.DeepEqual(t, []byte("application/json; charset=utf-8"), resp.Header.Peek("Content-Type"))
+		assert.DeepEqual(t, []byte(consts.MIMEApplicationJSONUTF8), resp.Header.Peek("Content-Type"))
 		ResetJSONMarshal(sonic.Marshal)
 	})
 	t.Run("TestError", func(t *testing.T) {
diff --git a/pkg/common/test/mock/network.go b/pkg/common/test/mock/network.go
index 0375982..1038eb7 100644
--- a/pkg/common/test/mock/network.go
+++ b/pkg/common/test/mock/network.go
@@ -18,7 +18,6 @@ package mock
 
 import (
 	"bytes"
-	"errors"
 	"net"
 	"strings"
 	"time"
@@ -258,7 +257,7 @@ func (m *StreamConn) Peek(n int) ([]byte, error) {
 		m.Data = m.Data[:cap(m.Data)]
 		return m.Data[:1], nil
 	}
-	return nil, errors.New("not enough data")
+	return nil, errs.NewPublic("not enough data")
 }
 
 func (m *StreamConn) Skip(n int) error {
@@ -266,7 +265,7 @@ func (m *StreamConn) Skip(n int) error {
 		m.Data = m.Data[n:]
 		return nil
 	}
-	return errors.New("not enough data")
+	return errs.NewPublic("not enough data")
 }
 
 func (m *StreamConn) Release() error {
diff --git a/pkg/protocol/http1/req/request_test.go b/pkg/protocol/http1/req/request_test.go
index a0a29de..e123da4 100644
--- a/pkg/protocol/http1/req/request_test.go
+++ b/pkg/protocol/http1/req/request_test.go
@@ -532,7 +532,7 @@ func TestRequestHostFromHeader(t *testing.T) {
 func TestRequestContentTypeWithCharset(t *testing.T) {
 	t.Parallel()
 
-	expectedContentType := "application/x-www-form-urlencoded; charset=UTF-8"
+	expectedContentType := consts.MIMEApplicationHTMLFormUTF8
 	expectedBody := "0123=56789"
 	s := fmt.Sprintf("POST / HTTP/1.1\r\nContent-Type: %s\r\nContent-Length: %d\r\n\r\n%s",
 		expectedContentType, len(expectedBody), expectedBody)
diff --git a/pkg/protocol/request_test.go b/pkg/protocol/request_test.go
index 0f52686..fda96cd 100644
--- a/pkg/protocol/request_test.go
+++ b/pkg/protocol/request_test.go
@@ -56,6 +56,7 @@ import (
 	"github.com/cloudwego/hertz/pkg/common/compress"
 	"github.com/cloudwego/hertz/pkg/common/config"
 	"github.com/cloudwego/hertz/pkg/common/test/assert"
+	"github.com/cloudwego/hertz/pkg/protocol/consts"
 )
 
 type errorReader struct{}
@@ -369,7 +370,7 @@ func TestRequestSetFormData(t *testing.T) {
 	assert.DeepEqual(t, "username", string(r.postArgs.args[0].key))
 	assert.DeepEqual(t, "admin", string(r.postArgs.args[0].value))
 	assert.DeepEqual(t, true, r.parsedPostArgs)
-	assert.DeepEqual(t, "application/x-www-form-urlencoded", string(r.Header.contentType))
+	assert.DeepEqual(t, consts.MIMEApplicationHTMLForm, string(r.Header.contentType))
 
 	r = &Request{}
 	value := map[string][]string{"item": {"apple", "peach"}}
@@ -573,7 +574,7 @@ func TestRequestPostArgs(t *testing.T) {
 	mr := strings.NewReader(s)
 	r := &Request{}
 	r.SetBodyStream(mr, len(s))
-	r.Header.contentType = []byte("application/x-www-form-urlencoded")
+	r.Header.contentType = []byte(consts.MIMEApplicationHTMLForm)
 	arg := r.PostArgs()
 	assert.DeepEqual(t, "username", string(arg.args[0].key))
 	assert.DeepEqual(t, "admin", string(arg.args[0].value))