Repository: cloudwego/hertz
Pull request #116 (2022-07-05 10:22:19)

feat: update for tempalte
#### What type of PR is this?

<!--
Add one of the following kinds:

build: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
ci: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
docs: Documentation only changes
feat: A new feature
optimize: A new optimization
fix: A bug fix
perf: A code change that improves performance
refactor: A code change that neither fixes a bug nor adds a feature
style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
test: Adding missing tests or correcting existing tests
chore: Changes to the build process or auxiliary tools and libraries such as documentation generation
-->
feat
#### What this PR does / why we need it (English/Chinese):

<!--
The description will be attached in Release Notes, 
so please describe it from user-oriented.
-->
en: update can also use template update handlers and middleware
zh: update也可使用模板更新handler及middleware
#### Which issue(s) this PR fixes:

<!--
*Automatically closes linked issue when PR is merged.
Eg: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
-->



Patch:
diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml
index 2f5c0c0..9d397d0 100644
--- a/.github/workflows/pr-check.yml
+++ b/.github/workflows/pr-check.yml
@@ -1,10 +1,10 @@
 name: Pull Request Check
 
-on: [ pull_request ]
+on: [pull_request]
 
 jobs:
   compliant:
-    runs-on: [ self-hosted, X64 ]
+    runs-on: self-hosted
     steps:
       - uses: actions/checkout@v3
 
@@ -17,7 +17,7 @@ jobs:
         uses: crate-ci/typos@master
 
   staticcheck:
-    runs-on: [ self-hosted, X64 ]
+    runs-on: self-hosted
     steps:
       - uses: actions/checkout@v3
 
diff --git a/README.md b/README.md
index 8ee7496..538005b 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@ English | [中文](README_cn.md)
 ![Forks](https://img.shields.io/github/forks/cloudwego/hertz)
 
 
-Hertz [həːts] is a high-usability, high-performance and high-extensibility Golang HTTP framework that helps developers build microservices. It was designed with reference to other open-source frameworks like [fasthttp](https://github.com/valyala/fasthttp), [gin](https://github.com/gin-gonic/gin), [echo](https://github.com/labstack/echo) and combined with the internal requirements in ByteDance. At present, it has been widely used inside ByteDance. Nowadays, more and more microservices use Golang. If you have requirements for microservice performance and hope that the framework can fully meet the internal customizable requirements, Hertz will be a good choice.
+Hertz [həːts] is a high-usability, high-performance and high-extensibility Golang HTTP framework that helps developers build microservices. It was designed with reference to other open-source frameworks like [fasthttp](https://github.com/valyala/fasthttp)、[gin](https://github.com/gin-gonic/gin)、[echo](https://github.com/labstack/echo) and combined with the internal requirements in ByteDance. At present, it has been widely used inside ByteDance. Nowadays, more and more microservices use Golang. If you have requirements for microservice performance and hope that the framework can fully meet the internal customizable requirements, Hertz will be a good choice.
 ## Basic Features
 - High usability
 
@@ -30,7 +30,7 @@ Hertz [həːts] is a high-usability, high-performance and high-extensibility Gol
   The Hertz framework provides HTTP1.1, ALPN protocol support natively. In addition, due to the layered design, Hertz even supports custom build protocol resolution logic to meet any needs of protocol layer extensions.
 - Network layer switching capability
 
-  Hertz implements the function to switch between Netpoll and Go Net on demand. Users can choose the appropriate network library for different scenarios. And Hertz also supports the extension of network library in the form of plug-ins.
+  Hertz implements the function to switch between Netpoll and Go Net on demand. Users can choose the appropriate network library for different scenarios. And Hertz also supports the extension of network library in the form of plug-ins .
 ## Documentation
 ### [Getting Started](https://www.cloudwego.io/docs/hertz/getting-started/)
 ### Example
@@ -68,7 +68,7 @@ Hertz is distributed under the [Apache License, version 2.0](https://github.com/
 - Lark: Scan the QR code below with [Lark](https://www.larksuite.com/zh_cn/download) to join our CloudWeGo/hertz user group.
 
 ![LarkGroup](images/lark_group.png)
-- WeChat: CloudWeGo community WeChat group.
+- Wechat: CloudWeGo community wechat group.
 
 ![WechatGroup](images/wechat_group_cn.png)
 ## Landscapes
diff --git a/cmd/hz/internal/config/cmd.go b/cmd/hz/internal/config/cmd.go
index 1c16231..96bcab0 100644
--- a/cmd/hz/internal/config/cmd.go
+++ b/cmd/hz/internal/config/cmd.go
@@ -61,12 +61,8 @@ func lookupTool(idlType string) (string, error) {
 	dir := filepath.Dir(path)
 	if tool == meta.TpCompilerProto {
 		pgh, err := exec.LookPath(meta.ProtocPluginName)
-		linkName := filepath.Join(dir, meta.ProtocPluginName)
-		if util.IsWindows() {
-			linkName = linkName + ".exe"
-		}
 		if err != nil {
-			err = link(exe, linkName)
+			err = link(exe, filepath.Join(dir, meta.ProtocPluginName))
 			if err != nil {
 				return "", err
 			}
@@ -80,12 +76,8 @@ func lookupTool(idlType string) (string, error) {
 
 	if tool == meta.TpCompilerThrift {
 		tgh, err := exec.LookPath(meta.ThriftPluginName)
-		linkName := filepath.Join(dir, meta.ThriftPluginName)
-		if util.IsWindows() {
-			linkName = linkName + ".exe"
-		}
 		if err != nil {
-			err = link(exe, linkName)
+			err = link(exe, filepath.Join(dir, meta.ThriftPluginName))
 			if err != nil {
 				return "", err
 			}
diff --git a/cmd/hz/internal/generator/handler.go b/cmd/hz/internal/generator/handler.go
index 9c80c20..b0a2005 100644
--- a/cmd/hz/internal/generator/handler.go
+++ b/cmd/hz/internal/generator/handler.go
@@ -111,7 +111,7 @@ func (pkgGen *HttpPackageGenerator) updateHandler(handler interface{}, handlerTp
 		return err
 	}
 
-	hertzImport := regexp.MustCompile(`"github.com/cloudwego/hertz/pkg/app"\n`)
+	hertzImport := regexp.MustCompile(`import \(\n`)
 	// insert new imports
 	for alias, model := range handler.(Handler).Imports {
 		if bytes.Contains(file, []byte(model.Package)) {
@@ -120,7 +120,7 @@ func (pkgGen *HttpPackageGenerator) updateHandler(handler interface{}, handlerTp
 
 		subIndexImport := hertzImport.FindSubmatchIndex(file)
 		if len(subIndexImport) != 2 || subIndexImport[0] < 1 {
-			return fmt.Errorf("\"github.com/cloudwego/hertz/pkg/app\" not found in %s", string(file))
+			return fmt.Errorf("\"import (\" not found in %s", string(file))
 		}
 
 		buf := bytes.NewBuffer(nil)
@@ -132,35 +132,36 @@ func (pkgGen *HttpPackageGenerator) updateHandler(handler interface{}, handlerTp
 
 	// insert new handler
 	for _, method := range handler.(Handler).Methods {
-		if bytes.Contains(file, []byte(fmt.Sprintf("func %s(ctx context.Context, c *app.RequestContext)", method.Name))) {
+		if bytes.Contains(file, []byte(fmt.Sprintf("func %s(", method.Name))) {
 			continue
 		}
 
-		handlerFunc := fmt.Sprintf("%s\n"+
-			"func %s(ctx context.Context, c *app.RequestContext) { \n"+
-			"\tvar err error\n"+
-			"\tvar req %s\n"+
-			"\terr = c.BindAndValidate(&req)\n"+
-			"\tif err != nil {\n"+
-			"\t\tc.String(400, err.Error())\n"+
-			"\t\treturn\n"+
-			"\t}\n\n"+
-			"\tresp := new(%s)\n\n"+
-			"\tc.%s(200, resp)\n"+
-			"}\n", method.Comment, method.Name, method.RequestTypeName, method.ReturnTypeName, method.Serializer)
-
-		if len(method.Name) == 0 {
-			handlerFunc = fmt.Sprintf("%s\n"+
-				"func %s(ctx context.Context, c *app.RequestContext) { \n"+
-				"\tvar err error\n"+
-				"\tresp := new(%s)\n\n"+
-				"\tc.%s(200, resp)\n"+
-				"}\n", method.Comment, method.Name, method.ReturnTypeName, method.Serializer)
+		// Generate additional handlers using templates
+		handlerSingleTpl := pkgGen.tpls[handlerSingleTplName]
+		if handlerSingleTpl == nil {
+			return fmt.Errorf("tpl %s not found", handlerSingleTplName)
+		}
+		data := make(map[string]string, 5)
+		data["Comment"] = method.Comment
+		data["Name"] = method.Name
+		data["RequestTypeName"] = method.RequestTypeName
+		data["ReturnTypeName"] = method.ReturnTypeName
+		data["Serializer"] = method.Serializer
+		handlerFunc := bytes.NewBuffer(nil)
+		err = handlerSingleTpl.Execute(handlerFunc, data)
+		if err != nil {
+			return fmt.Errorf("execute template \"%s\" failed, %v", handlerSingleTplName, err)
 		}
 
 		buf := bytes.NewBuffer(nil)
-		buf.Write(file)
-		buf.Write([]byte(handlerFunc))
+		_, err = buf.Write(file)
+		if err != nil {
+			return fmt.Errorf("write handler \"%s\" failed, %v", method.Name, err)
+		}
+		_, err = buf.Write(handlerFunc.Bytes())
+		if err != nil {
+			return fmt.Errorf("write handler \"%s\" failed, %v", method.Name, err)
+		}
 		file = buf.Bytes()
 	}
 
diff --git a/cmd/hz/internal/generator/layout_tpl.go b/cmd/hz/internal/generator/layout_tpl.go
index 52a4337..38b6f36 100644
--- a/cmd/hz/internal/generator/layout_tpl.go
+++ b/cmd/hz/internal/generator/layout_tpl.go
@@ -127,7 +127,7 @@ dumped_hertz_remote_config.json
 			Delims: [2]string{"{{", "}}"},
 			Body: `// Code generated by hz. DO NOT EDIT.
 
-hz version: v{{.hzVersion}}`,
+hz version: {{.hzVersion}}`,
 		},
 		{
 			Path: defaultHandlerDir + sp + "ping.go",
diff --git a/cmd/hz/internal/generator/package_tpl.go b/cmd/hz/internal/generator/package_tpl.go
index 690fec5..3e259e3 100644
--- a/cmd/hz/internal/generator/package_tpl.go
+++ b/cmd/hz/internal/generator/package_tpl.go
@@ -17,20 +17,36 @@
 package generator
 
 var (
-	routerTplName     = "router.go"
-	middlewareTplName = "middleware.go"
-	handlerTplName    = "handler.go"
-	modelTplName      = "model.go"
-	registerTplName   = "register.go"
-	clientTplName     = "client.go"
+	routerTplName           = "router.go"
+	middlewareTplName       = "middleware.go"
+	middlewareSingleTplName = "middleware_single.go"
+	handlerTplName          = "handler.go"
+	handlerSingleTplName    = "handler_single.go"
+	modelTplName            = "model.go"
+	registerTplName         = "register.go"
+	clientTplName           = "client.go"
 
 	insertPointNew        = "//INSERT_POINT: DO NOT DELETE THIS LINE!"
 	insertPointPatternNew = `//INSERT_POINT\: DO NOT DELETE THIS LINE\!`
-	insertPointPatternOld = `func register\(r \*(hertz|server)\.Hertz\) \{\n`
 )
 
+var templateNameSet = map[string]string{
+	routerTplName:           routerTplName,
+	middlewareTplName:       middlewareTplName,
+	middlewareSingleTplName: middlewareSingleTplName,
+	handlerTplName:          handlerTplName,
+	handlerSingleTplName:    handlerSingleTplName,
+	modelTplName:            modelTplName,
+	registerTplName:         registerTplName,
+	clientTplName:           clientTplName,
+}
+
 func IsDefaultTpl(name string) bool {
-	return name == routerTplName || name == handlerTplName || name == modelTplName || name == registerTplName || name == middlewareTplName || name == clientTplName
+	if _, exist := templateNameSet[name]; exist {
+		return true
+	}
+
+	return false
 }
 
 var defaultPkgConfig = TemplateConfig{
@@ -193,5 +209,36 @@ func New{{.ServiceName}}Client(opt ...config.ClientOption) (*{{.ServiceName}}Cli
 }
 		`,
 		},
+		{
+			Path:   defaultHandlerDir + sp + handlerSingleTplName,
+			Delims: [2]string{"{{", "}}"},
+			Body: `
+{{.Comment}}
+func {{.Name}}(ctx context.Context, c *app.RequestContext) { 
+	var err error
+	{{if ne .RequestTypeName "" -}}
+	var req {{.RequestTypeName}}
+	err = c.BindAndValidate(&req)
+	if err != nil {
+		c.String(400, err.Error())
+		return
+	}
+	{{end}}
+	resp := new({{.ReturnTypeName}})
+
+	c.{{.Serializer}}(200, resp)
+}
+`,
+		},
+		{
+			Path:   defaultRouterDir + sp + middlewareSingleTplName,
+			Delims: [2]string{"{{", "}}"},
+			Body: `
+func {{.MiddleWare}}Mw() []app.HandlerFunc {
+	// your code...
+	return nil
+}
+`,
+		},
 	},
 }
diff --git a/cmd/hz/internal/generator/router.go b/cmd/hz/internal/generator/router.go
index 6d30abb..d37435d 100644
--- a/cmd/hz/internal/generator/router.go
+++ b/cmd/hz/internal/generator/router.go
@@ -246,7 +246,7 @@ func (pkgGen *HttpPackageGenerator) updateRegister(pkg, rDir, pkgName string) er
 	}
 
 	insertImport := register.PkgAlias + " " + "\"" + register.Pkg + "\"\n"
-	if !bytes.Contains(file, []byte(insertImport)) {
+	if !bytes.Contains(file, []byte(register.Pkg)) {
 
 		subIndexImport := regImport.FindSubmatchIndex(file)
 		if len(subIndexImport) != 2 || subIndexImport[0] < 1 {
@@ -267,7 +267,7 @@ func (pkgGen *HttpPackageGenerator) updateRegister(pkg, rDir, pkgName string) er
 
 		subIndexReg := regRegisterV3.FindSubmatchIndex(file)
 		if len(subIndexReg) != 2 || subIndexReg[0] < 1 {
-			return fmt.Errorf("wrong format %s: insert-point '%s' not found", string(file), insertPointPatternOld)
+			return fmt.Errorf("wrong format %s: insert-point '%s' not found", string(file), insertPointPatternNew)
 		}
 
 		bufReg := bytes.NewBuffer(nil)
@@ -331,9 +331,27 @@ func (pkgGen *HttpPackageGenerator) updateMiddlewareReg(router interface{}, midd
 		if bytes.Contains(file, []byte(mw+"Mw")) {
 			continue
 		}
+		middlewareSingleTpl := pkgGen.tpls[middlewareSingleTplName]
+		if middlewareSingleTpl == nil {
+			return fmt.Errorf("tpl %s not found", middlewareSingleTplName)
+		}
+		data := make(map[string]string, 1)
+		data["MiddleWare"] = mw
+		middlewareFunc := bytes.NewBuffer(nil)
+		err = middlewareSingleTpl.Execute(middlewareFunc, data)
+		if err != nil {
+			return fmt.Errorf("execute template \"%s\" failed, %v", middlewareSingleTplName, err)
+		}
+
 		buf := bytes.NewBuffer(nil)
-		buf.Write(file)
-		buf.Write([]byte("func " + mw + "Mw() []app.HandlerFunc {\n\t// your code...\n\treturn nil\n}\n"))
+		_, err = buf.Write(file)
+		if err != nil {
+			return fmt.Errorf("write middleware \"%s\" failed, %v", mw, err)
+		}
+		_, err = buf.Write(middlewareFunc.Bytes())
+		if err != nil {
+			return fmt.Errorf("write middleware \"%s\" failed, %v", mw, err)
+		}
 		file = buf.Bytes()
 	}
 
diff --git a/cmd/hz/internal/meta/const.go b/cmd/hz/internal/meta/const.go
index ae87730..6e885c3 100644
--- a/cmd/hz/internal/meta/const.go
+++ b/cmd/hz/internal/meta/const.go
@@ -16,19 +16,12 @@
 
 package meta
 
-import "runtime"
-
 // Version hz version
 const Version = "v0.1.0"
 
 // Mode hz run modes
 type Mode int
 
-// SysType is the running program's operating system type
-const SysType = runtime.GOOS
-
-const WindowsOS = "windows"
-
 // hz Commands
 const (
 	CmdUpdate = "update"
diff --git a/cmd/hz/internal/protobuf/plugin.go b/cmd/hz/internal/protobuf/plugin.go
index b434c7c..2e2812a 100644
--- a/cmd/hz/internal/protobuf/plugin.go
+++ b/cmd/hz/internal/protobuf/plugin.go
@@ -287,9 +287,6 @@ func (plugin *Plugin) fixModelPathAndPackage(pkg string) (impt, path string) {
 	}
 	path = util.ImportToPath(impt, "")
 	impt = plugin.Package + "/" + impt
-	if util.IsWindows() {
-		impt = util.PathToImport(impt, "")
-	}
 	return
 }
 
diff --git a/cmd/hz/internal/util/data.go b/cmd/hz/internal/util/data.go
index 8491a3a..3058183 100644
--- a/cmd/hz/internal/util/data.go
+++ b/cmd/hz/internal/util/data.go
@@ -207,18 +207,8 @@ func ToSnakeCase(name string) string {
 	return SnakeString(name)
 }
 
-// unifyPath will convert "\" to "/" in path if the os is windows
-func unifyPath(path string) string {
-	if IsWindows() {
-		path = strings.ReplaceAll(path, "\\", "/")
-	}
-	return path
-}
-
 // BaseName get base name for path. ex: "github.com/p.s.m" => "p.s.m"
 func BaseName(include, subFixToTrim string) string {
-	include = unifyPath(include)
-	subFixToTrim = unifyPath(subFixToTrim)
 	last := include
 	if id := strings.LastIndex(last, "/"); id >= 0 && id < len(last)-1 {
 		last = last[id+1:]
@@ -230,7 +220,6 @@ func BaseName(include, subFixToTrim string) string {
 }
 
 func BaseNameAndTrim(include string) string {
-	include = unifyPath(include)
 	last := include
 	if id := strings.LastIndex(last, "/"); id >= 0 && id < len(last)-1 {
 		last = last[id+1:]
@@ -243,8 +232,6 @@ func BaseNameAndTrim(include string) string {
 }
 
 func SplitPackageName(pkg, subFixToTrim string) string {
-	pkg = unifyPath(pkg)
-	subFixToTrim = unifyPath(subFixToTrim)
 	last := SplitPackage(pkg, subFixToTrim)
 	if id := strings.LastIndex(last, "/"); id >= 0 && id < len(last)-1 {
 		last = last[id+1:]
@@ -253,8 +240,6 @@ func SplitPackageName(pkg, subFixToTrim string) string {
 }
 
 func SplitPackage(pkg, subFixToTrim string) string {
-	pkg = unifyPath(pkg)
-	subFixToTrim = unifyPath(subFixToTrim)
 	last := strings.TrimSuffix(pkg, subFixToTrim)
 	if id := strings.LastIndex(last, "/"); id >= 0 && id < len(last)-1 {
 		last = last[id+1:]
diff --git a/cmd/hz/internal/util/env.go b/cmd/hz/internal/util/env.go
index a6ae0a7..3eb7088 100644
--- a/cmd/hz/internal/util/env.go
+++ b/cmd/hz/internal/util/env.go
@@ -26,8 +26,6 @@ import (
 	"path/filepath"
 	"regexp"
 	"strings"
-
-	"github.com/cloudwego/hertz/cmd/hz/internal/meta"
 )
 
 func GetGOPATH() (gopath string, err error) {
@@ -128,7 +126,3 @@ func InitGoMod(module string) error {
 	}
 	return cmd.Run()
 }
-
-func IsWindows() bool {
-	return meta.SysType == meta.WindowsOS
-}
diff --git a/cmd/hz/main.go b/cmd/hz/main.go
index 1bd9f76..ac041a3 100644
--- a/cmd/hz/main.go
+++ b/cmd/hz/main.go
@@ -19,13 +19,11 @@ package main
 import (
 	"os"
 	"path/filepath"
-	"strings"
 
 	"github.com/cloudwego/hertz/cmd/hz/internal/app"
 	"github.com/cloudwego/hertz/cmd/hz/internal/meta"
 	"github.com/cloudwego/hertz/cmd/hz/internal/protobuf"
 	"github.com/cloudwego/hertz/cmd/hz/internal/thrift"
-	"github.com/cloudwego/hertz/cmd/hz/internal/util"
 	"github.com/cloudwego/hertz/cmd/hz/internal/util/logs"
 )
 
@@ -50,11 +48,7 @@ func Run() {
 }
 
 func pluginMode() {
-	pluginName := filepath.Base(os.Args[0])
-	if util.IsWindows() {
-		pluginName = strings.TrimSuffix(pluginName, ".exe")
-	}
-	switch pluginName {
+	switch filepath.Base(os.Args[0]) {
 	case meta.ThriftPluginName:
 		plugin := new(thrift.Plugin)
 		os.Exit(plugin.Run())
diff --git a/pkg/app/client/client.go b/pkg/app/client/client.go
index ddb7c4b..2af368e 100644
--- a/pkg/app/client/client.go
+++ b/pkg/app/client/client.go
@@ -44,6 +44,7 @@ package client
 import (
 	"bytes"
 	"context"
+	"crypto/tls"
 	"fmt"
 	"sync"
 	"time"
@@ -52,13 +53,9 @@ import (
 	"github.com/cloudwego/hertz/internal/nocopy"
 	"github.com/cloudwego/hertz/pkg/common/config"
 	"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/client"
-	"github.com/cloudwego/hertz/pkg/protocol/consts"
 	"github.com/cloudwego/hertz/pkg/protocol/http1"
-	"github.com/cloudwego/hertz/pkg/protocol/http1/factory"
-	"github.com/cloudwego/hertz/pkg/protocol/suite"
 )
 
 var errorInvalidURI = errors.NewPublic("invalid uri")
@@ -165,7 +162,7 @@ func DoDeadline(ctx context.Context, req *protocol.Request, resp *protocol.Respo
 // It is recommended obtaining req and resp via AcquireRequest
 // and AcquireResponse in performance-critical code.
 func DoRedirects(ctx context.Context, req *protocol.Request, resp *protocol.Response, maxRedirectsCount int) error {
-	_, _, err := client.DoRequestFollowRedirects(ctx, req, resp, req.URI().String(), maxRedirectsCount, defaultClient)
+	_, _, err := client.DoRequestFollowRedirects(ctx, req, resp, req.URI().String(), maxRedirectsCount, &defaultClient)
 	return err
 }
 
@@ -217,7 +214,7 @@ func Post(ctx context.Context, dst []byte, url string, postArgs *protocol.Args)
 	return defaultClient.Post(ctx, dst, url, postArgs)
 }
 
-var defaultClient, _ = NewClient(WithDialTimeout(consts.DefaultDialTimeout))
+var defaultClient = Client{DialTimeout: time.Second}
 
 // Client implements http client.
 //
@@ -227,34 +224,145 @@ var defaultClient, _ = NewClient(WithDialTimeout(consts.DefaultDialTimeout))
 type Client struct {
 	noCopy nocopy.NoCopy //lint:ignore U1000 until noCopy is used
 
-	options *config.ClientOptions
+	// Client name. Used in User-Agent request header.
+	//
+	// Default client name is used if not set.
+	Name string
 
-	// Proxy specifies a function to return a proxy for a given
-	// Request. If the function returns a non-nil error, the
-	// request is aborted with the provided error.
+	// NoDefaultUserAgentHeader when set to true, causes the default
+	// User-Agent header to be excluded from the Request.
+	NoDefaultUserAgentHeader bool
+
+	// Callback for establishing new connections to hosts.
 	//
-	// The proxy type is determined by the URL scheme.
-	// "http" and "https" are supported. If the scheme is empty,
-	// "http" is assumed.
+	// Default Dial is used if not set.
+	Dial client.DialFunc
+
+	// Timeout for establishing new connections to hosts.
 	//
-	// If Proxy is nil or returns a nil *URL, no proxy is used.
-	Proxy protocol.Proxy
+	// Default DialTimeout is used if not set.
+	DialTimeout time.Duration
+
+	// Attempt to connect to both ipv4 and ipv6 addresses if set to true.
+	//
+	// This option is used only if default TCP dialer is used,
+	// i.e. if Dial is blank.
+	//
+	// By default client connects only to ipv4 addresses,
+	// since unfortunately ipv6 remains broken in many networks worldwide :)
+	DialDualStack bool
+
+	// TLS config for https connections.
+	//
+	// Default TLS config is used if not set.
+	TLSConfig *tls.Config
+
+	// Maximum number of connections per each host which may be established.
+	//
+	// DefaultMaxConnsPerHost is used if not set.
+	MaxConnsPerHost int
+
+	// Idle keep-alive connections are closed after this duration.
+	//
+	// By default idle connections are closed
+	// after DefaultMaxIdleConnDuration.
+	MaxIdleConnDuration time.Duration
+
+	// Keep-alive connections are closed after this duration.
+	//
+	// By default connection duration is unlimited.
+	MaxConnDuration time.Duration
+
+	// Maximum number of attempts for idempotent calls
+	//
+	// DefaultMaxIdempotentCallAttempts is used if not set.
+	MaxIdempotentCallAttempts int
+
+	// Per-connection buffer size for responses' reading.
+	// This also limits the maximum header size.
+	//
+	// Default buffer size is used if 0.
+	ReadBufferSize int
+
+	// Per-connection buffer size for requests' writing.
+	//
+	// Default buffer size is used if 0.
+	WriteBufferSize int
+
+	// Maximum duration for full response reading (including body).
+	//
+	// By default response read timeout is unlimited.
+	ReadTimeout time.Duration
+
+	// Maximum duration for full request writing (including body).
+	//
+	// By default request write timeout is unlimited.
+	WriteTimeout time.Duration
+
+	// Maximum response body size.
+	//
+	// The client returns ErrBodyTooLarge if this limit is greater than 0
+	// and response body is greater than the limit.
+	//
+	// By default response body size is unlimited.
+	MaxResponseBodySize int
+
+	// Header names are passed as-is without normalization
+	// if this option is set.
+	//
+	// Disabled header names' normalization may be useful only for proxying
+	// responses to other clients expecting case-sensitive header names.
+	//
+	// By default request and response header names are normalized, i.e.
+	// The first letter and the first letters following dashes
+	// are uppercased, while all the other letters are lowercased.
+	// Examples:
+	//
+	//     * HOST -> Host
+	//     * content-type -> Content-Type
+	//     * cONTENT-lenGTH -> Content-Length
+	DisableHeaderNamesNormalizing bool
+
+	// Path values are sent as-is without normalization
+	//
+	// Disabled path normalization may be useful for proxying incoming requests
+	// to servers that are expecting paths to be forwarded as-is.
+	//
+	// By default path values are normalized, i.e.
+	// extra slashes are removed, special characters are encoded.
+	DisablePathNormalizing bool
+
+	// Maximum duration for waiting for a free connection.
+	//
+	// By default will not waiting, return ErrNoFreeConns immediately
+	MaxConnWaitTimeout time.Duration
 
 	// RetryIf controls whether a retry should be attempted after an error.
 	//
 	// By default will use isIdempotent function
-	RetryIf func(request *protocol.Request) bool
+	RetryIf client.RetryIfFunc
 
-	clientFactory suite.ClientFactory
+	// Connection will close after each request when set this to true.
+	KeepAlive bool
+
+	// ResponseBodyStream enables response body streaming
+	ResponseBodyStream bool
 
 	mLock sync.Mutex
 	m     map[string]client.HostClient
 	ms    map[string]client.HostClient
 	mws   Middleware
-}
 
-func (c *Client) GetOptions() *config.ClientOptions {
-	return c.options
+	// Proxy specifies a function to return a proxy for a given
+	// Request. If the function returns a non-nil error, the
+	// request is aborted with the provided error.
+	//
+	// The proxy type is determined by the URL scheme.
+	// "http" and "https" are supported. If the scheme is empty,
+	// "http" is assumed.
+	//
+	// If Proxy is nil or returns a nil *URL, no proxy is used.
+	Proxy protocol.Proxy
 }
 
 // SetProxy is used to set client proxy.
@@ -265,16 +373,6 @@ func (c *Client) SetProxy(p protocol.Proxy) {
 	c.Proxy = p
 }
 
-// SetRetryIf is used to set RetryIf func.
-func (c *Client) SetRetryIf(fn func(request *protocol.Request) bool) {
-	c.RetryIf = fn
-}
-
-// SetDialFunc is used to set custom dial func.
-func (c *Client) SetDialFunc(dial func(addr string) (network.Conn, error)) {
-	c.options.Dial = dial
-}
-
 // Get returns the status code and body of url.
 //
 // The contents of dst will be replaced by the body and returned, if the dst
@@ -433,7 +531,7 @@ func (c *Client) Do(ctx context.Context, req *protocol.Request, resp *protocol.R
 }
 
 func (c *Client) do(ctx context.Context, req *protocol.Request, resp *protocol.Response) error {
-	if !c.options.KeepAlive {
+	if !c.KeepAlive {
 		req.Header.SetConnectionClose(true)
 	}
 	uri := req.URI()
@@ -466,7 +564,6 @@ func (c *Client) do(ctx context.Context, req *protocol.Request, resp *protocol.R
 	if isTLS {
 		m = c.ms
 	}
-
 	if m == nil {
 		m = make(map[string]client.HostClient)
 		if isTLS {
@@ -475,26 +572,40 @@ func (c *Client) do(ctx context.Context, req *protocol.Request, resp *protocol.R
 			c.m = m
 		}
 	}
-
 	h := string(host)
 	hc := m[h]
 	if hc == nil {
-		if c.clientFactory == nil {
-			// load http1 client by default
-			c.clientFactory = factory.NewClientFactory(newHttp1OptionFromClient(c))
+		hConfig := &client.HostClientConfig{
+			Addr:                          h,
+			Name:                          c.Name,
+			NoDefaultUserAgentHeader:      c.NoDefaultUserAgentHeader,
+			DialDualStack:                 c.DialDualStack,
+			DisableHeaderNamesNormalizing: c.DisableHeaderNamesNormalizing,
+			DisablePathNormalizing:        c.DisablePathNormalizing,
+			TLSConfig:                     c.TLSConfig,
+			IsTLS:                         isTLS,
+			MaxConns:                      c.MaxConnsPerHost,
+			MaxIdempotentCallAttempts:     c.MaxIdempotentCallAttempts,
+			ReadBufferSize:                c.ReadBufferSize,
+			WriteBufferSize:               c.WriteBufferSize,
+			MaxResponseBodySize:           c.MaxResponseBodySize,
+			Dial:                          c.Dial,
+			RetryIf:                       c.RetryIf,
+			DialTimeout:                   c.DialTimeout,
+			MaxIdleConnDuration:           c.MaxIdleConnDuration,
+			MaxConnDuration:               c.MaxConnDuration,
+			ReadTimeout:                   c.ReadTimeout,
+			WriteTimeout:                  c.WriteTimeout,
+			MaxConnWaitTimeout:            c.MaxConnWaitTimeout,
+			ResponseBodyStream:            c.ResponseBodyStream,
+			ProxyURI:                      proxyURI,
 		}
-		hc, _ = c.clientFactory.NewHostClient()
-		hc.SetDynamicConfig(&client.DynamicConfig{
-			Addr:     h,
-			ProxyURI: proxyURI,
-			IsTLS:    isTLS,
-		})
-		m[h] = hc
+		hc = http1.NewHostClient(hConfig)
+		m[string(host)] = hc
 		if len(m) == 1 {
 			startCleaner = true
 		}
 	}
-
 	c.mLock.Unlock()
 
 	if startCleaner {
@@ -540,17 +651,22 @@ func (c *Client) mCleaner() {
 	}
 }
 
-func (c *Client) SetClientFactory(cf suite.ClientFactory) {
-	c.clientFactory = cf
-}
-
 // NewClient return a client with options
 func NewClient(opts ...config.ClientOption) (*Client, error) {
 	opt := config.NewClientOptions(opts)
 	c := &Client{
-		options: opt,
+		DialTimeout:               opt.DialTimeout,
+		MaxConnsPerHost:           opt.MaxConnsPerHost,
+		MaxIdleConnDuration:       opt.MaxIdleConnDuration,
+		MaxConnDuration:           opt.MaxConnDuration,
+		MaxConnWaitTimeout:        opt.MaxConnWaitTimeout,
+		MaxIdempotentCallAttempts: opt.MaxIdempotentCallAttempts,
+		KeepAlive:                 opt.KeepAlive,
+		ReadTimeout:               opt.ReadTimeout,
+		ResponseBodyStream:        opt.ResponseBodyStream,
+		TLSConfig:                 opt.TLSConfig,
+		Dial:                      opt.DialFunc,
 	}
-
 	return c, nil
 }
 
@@ -563,26 +679,3 @@ func (c *Client) Use(mws ...Middleware) {
 	middlewares = append(middlewares, mws...)
 	c.mws = chain(middlewares...)
 }
-
-func newHttp1OptionFromClient(c *Client) *http1.ClientOptions {
-	return &http1.ClientOptions{
-		Name:                          c.options.Name,
-		NoDefaultUserAgentHeader:      c.options.NoDefaultUserAgentHeader,
-		Dial:                          c.options.Dial,
-		DialTimeout:                   c.options.DialTimeout,
-		DialDualStack:                 c.options.DialDualStack,
-		TLSConfig:                     c.options.TLSConfig,
-		MaxConns:                      c.options.MaxConnsPerHost,
-		MaxConnDuration:               c.options.MaxConnDuration,
-		MaxIdleConnDuration:           c.options.MaxIdleConnDuration,
-		MaxIdempotentCallAttempts:     c.options.MaxIdempotentCallAttempts,
-		ReadTimeout:                   c.options.ReadTimeout,
-		WriteTimeout:                  c.options.WriteTimeout,
-		MaxResponseBodySize:           c.options.MaxResponseBodySize,
-		DisableHeaderNamesNormalizing: c.options.DisableHeaderNamesNormalizing,
-		DisablePathNormalizing:        c.options.DisablePathNormalizing,
-		MaxConnWaitTimeout:            c.options.MaxConnWaitTimeout,
-		RetryIf:                       c.RetryIf,
-		ResponseBodyStream:            c.options.ResponseBodyStream,
-	}
-}
diff --git a/pkg/app/client/option.go b/pkg/app/client/option.go
index af149f4..57b3c84 100644
--- a/pkg/app/client/option.go
+++ b/pkg/app/client/option.go
@@ -107,37 +107,9 @@ func WithResponseBodyStream(b bool) config.ClientOption {
 
 // WithDialFunc is used to set dialer function
 //
-// NOTE: By default, hertz client uses dialer.DialConnection as DialFunc.
+// NOTE: By default, hertz client uses dialer.DialConnection as DialFunc
 func WithDialFunc(f client.DialFunc) config.ClientOption {
 	return config.ClientOption{F: func(o *config.ClientOptions) {
-		o.Dial = f
-	}}
-}
-
-// WithDisableHeaderNamesNormalizing is used to set whether disable header names normalizing.
-func WithDisableHeaderNamesNormalizing(disable bool) config.ClientOption {
-	return config.ClientOption{F: func(o *config.ClientOptions) {
-		o.DisableHeaderNamesNormalizing = disable
-	}}
-}
-
-// WithName sets client name which used in User-Agent Header.
-func WithName(name string) config.ClientOption {
-	return config.ClientOption{F: func(o *config.ClientOptions) {
-		o.Name = name
-	}}
-}
-
-// WithNoDefaultUserAgentHeader sets whether no default User-Agent header.
-func WithNoDefaultUserAgentHeader(isNoDefaultUserAgentHeader bool) config.ClientOption {
-	return config.ClientOption{F: func(o *config.ClientOptions) {
-		o.NoDefaultUserAgentHeader = isNoDefaultUserAgentHeader
-	}}
-}
-
-// WithDisablePathNormalizing sets whether disable path normalizing.
-func WithDisablePathNormalizing(isDisablePathNormalizing bool) config.ClientOption {
-	return config.ClientOption{F: func(o *config.ClientOptions) {
-		o.DisablePathNormalizing = isDisablePathNormalizing
+		o.DialFunc = f
 	}}
 }
diff --git a/pkg/app/fs.go b/pkg/app/fs.go
index 84fbaa4..ec2fde2 100644
--- a/pkg/app/fs.go
+++ b/pkg/app/fs.go
@@ -733,11 +733,11 @@ func (h *fsHandler) openIndexFile(ctx *RequestContext, dirPath string, mustCompr
 
 func (ff *fsFile) decReadersCount() {
 	ff.h.cacheLock.Lock()
-	defer ff.h.cacheLock.Unlock()
 	ff.readersCount--
 	if ff.readersCount < 0 {
 		panic("BUG: negative fsFile.readersCount!")
 	}
+	ff.h.cacheLock.Unlock()
 }
 
 func (ff *fsFile) bigFileReader() (io.Reader, error) {
diff --git a/pkg/common/compress/compress.go b/pkg/common/compress/compress.go
index 2586129..fad3bfe 100644
--- a/pkg/common/compress/compress.go
+++ b/pkg/common/compress/compress.go
@@ -97,7 +97,7 @@ func (w *byteSliceWriter) Write(p []byte) (int, error) {
 	return len(p), nil
 }
 
-// WriteGunzip writes gunzipped p to w and returns the number of uncompressed
+// WriteGunzip writes ungzipped p to w and returns the number of uncompressed
 // bytes written to w.
 func WriteGunzip(w io.Writer, p []byte) (int, error) {
 	r := &byteSliceReader{p}
diff --git a/pkg/common/config/client_option.go b/pkg/common/config/client_option.go
index 4ecc757..9f4deaa 100644
--- a/pkg/common/config/client_option.go
+++ b/pkg/common/config/client_option.go
@@ -43,67 +43,7 @@ type ClientOptions struct {
 	ReadTimeout               time.Duration
 	TLSConfig                 *tls.Config
 	ResponseBodyStream        bool
-
-	// Client name. Used in User-Agent request header.
-	//
-	// Default client name is used if not set.
-	Name string
-
-	// NoDefaultUserAgentHeader when set to true, causes the default
-	// User-Agent header to be excluded from the Request.
-	NoDefaultUserAgentHeader bool
-
-	// Callback for establishing new connections to hosts.
-	//
-	// Default Dial is used if not set.
-	Dial func(addr string) (network.Conn, error)
-
-	// Attempt to connect to both ipv4 and ipv6 addresses if set to true.
-	//
-	// This option is used only if default TCP dialer is used,
-	// i.e. if Dial is blank.
-	//
-	// By default client connects only to ipv4 addresses,
-	// since unfortunately ipv6 remains broken in many networks worldwide :)
-	DialDualStack bool
-
-	// Maximum duration for full request writing (including body).
-	//
-	// By default request write timeout is unlimited.
-	WriteTimeout time.Duration
-
-	// Maximum response body size.
-	//
-	// The client returns ErrBodyTooLarge if this limit is greater than 0
-	// and response body is greater than the limit.
-	//
-	// By default response body size is unlimited.
-	MaxResponseBodySize int
-
-	// Header names are passed as-is without normalization
-	// if this option is set.
-	//
-	// Disabled header names' normalization may be useful only for proxying
-	// responses to other clients expecting case-sensitive header names.
-	//
-	// By default request and response header names are normalized, i.e.
-	// The first letter and the first letters following dashes
-	// are uppercased, while all the other letters are lowercased.
-	// Examples:
-	//
-	//     * HOST -> Host
-	//     * content-type -> Content-Type
-	//     * cONTENT-lenGTH -> Content-Length
-	DisableHeaderNamesNormalizing bool
-
-	// Path values are sent as-is without normalization
-	//
-	// Disabled path normalization may be useful for proxying incoming requests
-	// to servers that are expecting paths to be forwarded as-is.
-	//
-	// By default path values are normalized, i.e.
-	// extra slashes are removed, special characters are encoded.
-	DisablePathNormalizing bool
+	DialFunc                  func(addr string) (network.Conn, error)
 }
 
 func NewClientOptions(opts []ClientOption) *ClientOptions {
@@ -115,7 +55,6 @@ func NewClientOptions(opts []ClientOption) *ClientOptions {
 		KeepAlive:                 true,
 	}
 	options.Apply(opts)
-
 	return options
 }
 
diff --git a/pkg/protocol/client/client.go b/pkg/protocol/client/client.go
index 33cec15..ee6fb53 100644
--- a/pkg/protocol/client/client.go
+++ b/pkg/protocol/client/client.go
@@ -70,7 +70,6 @@ var (
 
 type HostClient interface {
 	Doer
-	SetDynamicConfig(dc *DynamicConfig)
 	CloseIdleConnections()
 	ShouldRemove() bool
 	ConnectionCount() int
@@ -81,8 +80,7 @@ type Doer interface {
 }
 
 type HostClientConfig struct {
-	DynamicConfig
-
+	Addr string
 	Name string
 
 	NoDefaultUserAgentHeader      bool
@@ -95,6 +93,8 @@ type HostClientConfig struct {
 
 	MaxConns                  int
 	MaxIdempotentCallAttempts int
+	ReadBufferSize            int
+	WriteBufferSize           int
 	MaxResponseBodySize       int
 
 	Dial               DialFunc
@@ -107,13 +107,8 @@ type HostClientConfig struct {
 	ReadTimeout         time.Duration
 	WriteTimeout        time.Duration
 	MaxConnWaitTimeout  time.Duration
-}
 
-// DynamicConfig is config set which will be confirmed when starts a request.
-type DynamicConfig struct {
-	Addr     string
 	ProxyURI *protocol.URI
-	IsTLS    bool
 }
 
 // DialFunc must establish connection to addr.
diff --git a/pkg/protocol/consts/default.go b/pkg/protocol/consts/default.go
index 82b5b28..ba6cf3d 100644
--- a/pkg/protocol/consts/default.go
+++ b/pkg/protocol/consts/default.go
@@ -66,5 +66,5 @@ const (
 	DefaultMaxIdleConnDuration = 10 * time.Second
 
 	// DefaultMaxIdempotentCallAttempts is the default idempotent calls attempts count.
-	DefaultMaxIdempotentCallAttempts = 1
+	DefaultMaxIdempotentCallAttempts = 5
 )
diff --git a/pkg/protocol/cookie.go b/pkg/protocol/cookie.go
index b26e810..257ced6 100644
--- a/pkg/protocol/cookie.go
+++ b/pkg/protocol/cookie.go
@@ -192,7 +192,10 @@ func (c *Cookie) AppendBytes(dst []byte) []byte {
 		dst = bytesconv.AppendUint(dst, c.maxAge)
 	} else if !c.expire.IsZero() {
 		c.bufKV.value = bytesconv.AppendHTTPDate(c.bufKV.value[:0], c.expire)
-		dst = appendCookiePart(dst, bytestr.StrCookieExpires, c.bufKV.value)
+		dst = append(dst, ';', ' ')
+		dst = append(dst, bytestr.StrCookieExpires...)
+		dst = append(dst, '=')
+		dst = append(dst, c.bufKV.value...)
 	}
 	if len(c.domain) > 0 {
 		dst = appendCookiePart(dst, bytestr.StrCookieDomain, c.domain)
@@ -213,11 +216,20 @@ func (c *Cookie) AppendBytes(dst []byte) []byte {
 		dst = append(dst, ';', ' ')
 		dst = append(dst, bytestr.StrCookieSameSite...)
 	case CookieSameSiteLaxMode:
-		dst = appendCookiePart(dst, bytestr.StrCookieSameSite, bytestr.StrCookieSameSiteLax)
+		dst = append(dst, ';', ' ')
+		dst = append(dst, bytestr.StrCookieSameSite...)
+		dst = append(dst, '=')
+		dst = append(dst, bytestr.StrCookieSameSiteLax...)
 	case CookieSameSiteStrictMode:
-		dst = appendCookiePart(dst, bytestr.StrCookieSameSite, bytestr.StrCookieSameSiteStrict)
+		dst = append(dst, ';', ' ')
+		dst = append(dst, bytestr.StrCookieSameSite...)
+		dst = append(dst, '=')
+		dst = append(dst, bytestr.StrCookieSameSiteStrict...)
 	case CookieSameSiteNoneMode:
-		dst = appendCookiePart(dst, bytestr.StrCookieSameSite, bytestr.StrCookieSameSiteNone)
+		dst = append(dst, ';', ' ')
+		dst = append(dst, bytestr.StrCookieSameSite...)
+		dst = append(dst, '=')
+		dst = append(dst, bytestr.StrCookieSameSiteNone...)
 	}
 	return dst
 }
diff --git a/pkg/protocol/http1/client.go b/pkg/protocol/http1/client.go
index f52ccda..ba5ea41 100644
--- a/pkg/protocol/http1/client.go
+++ b/pkg/protocol/http1/client.go
@@ -58,6 +58,7 @@ import (
 	"github.com/cloudwego/hertz/internal/nocopy"
 	errs "github.com/cloudwego/hertz/pkg/common/errors"
 	"github.com/cloudwego/hertz/pkg/common/timer"
+	"github.com/cloudwego/hertz/pkg/common/utils"
 	"github.com/cloudwego/hertz/pkg/network"
 	"github.com/cloudwego/hertz/pkg/network/dialer"
 	"github.com/cloudwego/hertz/pkg/protocol"
@@ -87,7 +88,141 @@ var (
 type HostClient struct {
 	noCopy nocopy.NoCopy //lint:ignore U1000 until noCopy is used
 
-	*ClientOptions
+	// Comma-separated list of upstream HTTP server host addresses,
+	// which are passed to Dial in a round-robin manner.
+	//
+	// Each address may contain port if default dialer is used.
+	// For example,
+	//
+	//    - foobar.com:80
+	//    - foobar.com:443
+	//    - foobar.com:8080
+	Addr string
+
+	// Client name. Used in User-Agent request header.
+	Name string
+
+	// NoDefaultUserAgentHeader when set to true, causes the default
+	// User-Agent header to be excluded from the Request.
+	NoDefaultUserAgentHeader bool
+
+	// Callback for establishing new connection to the host.
+	//
+	// Default Dial is used if not set.
+	Dial client.DialFunc
+
+	// Timeout for establishing new connections to hosts.
+	//
+	// Default DialTimeout is used if not set.
+	DialTimeout time.Duration
+
+	// Attempt to connect to both ipv4 and ipv6 host addresses
+	// if set to true.
+	//
+	// This option is used only if default TCP dialer is used,
+	// i.e. if Dial is blank.
+	//
+	// By default client connects only to ipv4 addresses,
+	// since unfortunately ipv6 remains broken in many networks worldwide :)
+	DialDualStack bool
+
+	// Whether to use TLS (aka SSL or HTTPS) for host connections.
+	// Optional TLS config.
+	TLSConfig *tls.Config
+
+	IsTLS bool
+
+	// Maximum number of connections which may be established to all hosts
+	// listed in Addr.
+	//
+	// You can change this value while the HostClient is being used
+	// using HostClient.SetMaxConns(value)
+	//
+	// DefaultMaxConnsPerHost is used if not set.
+	MaxConns int
+
+	// Keep-alive connections are closed after this duration.
+	//
+	// By default connection duration is unlimited.
+	MaxConnDuration time.Duration
+
+	// Idle keep-alive connections are closed after this duration.
+	//
+	// By default idle connections are closed
+	// after DefaultMaxIdleConnDuration.
+	MaxIdleConnDuration time.Duration
+
+	// Maximum number of attempts for idempotent calls
+	//
+	// DefaultMaxIdempotentCallAttempts is used if not set.
+	MaxIdempotentCallAttempts int
+
+	// Per-connection buffer size for responses' reading.
+	// This also limits the maximum header size.
+	//
+	// Default buffer size is used if 0.
+	ReadBufferSize int
+
+	// Per-connection buffer size for requests' writing.
+	//
+	// Default buffer size is used if 0.
+	WriteBufferSize int
+
+	// Maximum duration for full response reading (including body).
+	//
+	// By default response read timeout is unlimited.
+	ReadTimeout time.Duration
+
+	// Maximum duration for full request writing (including body).
+	//
+	// By default request write timeout is unlimited.
+	WriteTimeout time.Duration
+
+	// Maximum response body size.
+	//
+	// The client returns errBodyTooLarge if this limit is greater than 0
+	// and response body is greater than the limit.
+	//
+	// By default response body size is unlimited.
+	MaxResponseBodySize int
+
+	// Header names are passed as-is without normalization
+	// if this option is set.
+	//
+	// Disabled header names' normalization may be useful only for proxying
+	// responses to other clients expecting case-sensitive header names.
+	//
+	// By default request and response header names are normalized, i.e.
+	// The first letter and the first letters following dashes
+	// are uppercased, while all the other letters are lowercased.
+	// Examples:
+	//
+	//     * HOST -> Host
+	//     * content-type -> Content-Type
+	//     * cONTENT-lenGTH -> Content-Length
+	DisableHeaderNamesNormalizing bool
+
+	// Path values are sent as-is without normalization
+	//
+	// Disabled path normalization may be useful for proxying incoming requests
+	// to servers that are expecting paths to be forwarded as-is.
+	//
+	// By default path values are normalized, i.e.
+	// extra slashes are removed, special characters are encoded.
+	DisablePathNormalizing bool
+
+	// Maximum duration for waiting for a free connection.
+	//
+	// By default will not waiting, return errNoFreeConns immediately
+	MaxConnWaitTimeout time.Duration
+
+	// RetryIf controls whether a retry should be attempted after an error.
+	//
+	// By default will use isIdempotent function
+	RetryIf client.RetryIfFunc
+
+	// ResponseBodyStream enables response body streaming
+	ResponseBodyStream bool
 
 	clientName  atomic.Value
 	lastUseTime uint32
@@ -107,12 +242,8 @@ type HostClient struct {
 	pendingRequests int32
 
 	connsCleanerRun bool
-}
 
-func (c *HostClient) SetDynamicConfig(dc *client.DynamicConfig) {
-	c.Addr = dc.Addr
-	c.ProxyURI = dc.ProxyURI
-	c.IsTLS = dc.IsTLS
+	proxyURI *protocol.URI
 }
 
 type clientConn struct {
@@ -313,13 +444,16 @@ func (c *HostClient) DoRedirects(ctx context.Context, req *protocol.Request, res
 func (c *HostClient) Do(ctx context.Context, req *protocol.Request, resp *protocol.Response) error {
 	var err error
 	var retry bool
-
 	maxAttempts := c.MaxIdempotentCallAttempts
+	if maxAttempts <= 0 {
+		maxAttempts = consts.DefaultMaxIdempotentCallAttempts
+	}
 	isRequestRetryable := isIdempotent
 	if c.RetryIf != nil {
 		isRequestRetryable = c.RetryIf
 	}
 	attempts := 0
+	hasBodyStream := req.IsBodyStream()
 
 	atomic.AddInt32(&c.pendingRequests, 1)
 	for {
@@ -328,15 +462,9 @@ func (c *HostClient) Do(ctx context.Context, req *protocol.Request, resp *protoc
 			break
 		}
 
-		attempts++
-		if attempts >= maxAttempts {
+		if hasBodyStream {
 			break
 		}
-
-		if req.IsBodyStream() {
-			break
-		}
-
 		if !isRequestRetryable(req) {
 			// Retry non-idempotent requests if the server closes
 			// the connection before sending the response.
@@ -349,7 +477,10 @@ func (c *HostClient) Do(ctx context.Context, req *protocol.Request, resp *protoc
 				break
 			}
 		}
-
+		attempts++
+		if attempts >= maxAttempts {
+			break
+		}
 	}
 	atomic.AddInt32(&c.pendingRequests, -1)
 
@@ -417,9 +548,9 @@ func (c *HostClient) doNonNilReqResp(req *protocol.Request, resp *protocol.Respo
 
 	usingProxy := false
 
-	if c.ProxyURI != nil && bytes.Equal(req.Scheme(), bytestr.StrHTTP) {
+	if c.proxyURI != nil && bytes.Equal(req.Scheme(), bytestr.StrHTTP) {
 		usingProxy = true
-		proxy.SetProxyAuthHeader(&req.Header, c.ProxyURI)
+		proxy.SetProxyAuthHeader(&req.Header, c.proxyURI)
 	}
 
 	resp.ParseNetAddr(conn)
@@ -851,7 +982,7 @@ func (c *HostClient) dialHostHard() (conn network.Conn, err error) {
 	for n > 0 {
 		addr := c.nextAddr()
 		tlsConfig := c.cachedTLSConfig(addr)
-		conn, err = dialAddr(addr, c.Dial, c.DialDualStack, tlsConfig, timeout, c.ProxyURI, c.IsTLS)
+		conn, err = dialAddr(addr, c.Dial, c.DialDualStack, tlsConfig, timeout, c.proxyURI, c.IsTLS)
 		if err == nil {
 			return conn, nil
 		}
@@ -865,8 +996,8 @@ func (c *HostClient) dialHostHard() (conn network.Conn, err error) {
 
 func (c *HostClient) cachedTLSConfig(addr string) *tls.Config {
 	var cfgAddr string
-	if c.ProxyURI != nil && bytes.Equal(c.ProxyURI.Scheme(), bytestr.StrHTTPS) {
-		cfgAddr = bytesconv.B2s(c.ProxyURI.Host())
+	if c.proxyURI != nil && bytes.Equal(c.proxyURI.Scheme(), bytestr.StrHTTPS) {
+		cfgAddr = bytesconv.B2s(c.proxyURI.Host())
 	}
 
 	if c.IsTLS && cfgAddr == "" {
@@ -1033,137 +1164,30 @@ func (q *wantConnQueue) clearFront() (cleaned bool) {
 	}
 }
 
-func NewHostClient(c *ClientOptions) client.HostClient {
+func NewHostClient(c *client.HostClientConfig) client.HostClient {
 	return &HostClient{
-		ClientOptions: c,
+		Addr:                          utils.AddMissingPort(c.Addr, c.IsTLS),
+		Name:                          c.Name,
+		NoDefaultUserAgentHeader:      c.NoDefaultUserAgentHeader,
+		Dial:                          c.Dial,
+		DialDualStack:                 c.DialDualStack,
+		DialTimeout:                   c.DialTimeout,
+		TLSConfig:                     c.TLSConfig,
+		IsTLS:                         c.IsTLS,
+		MaxConns:                      c.MaxConns,
+		MaxIdleConnDuration:           c.MaxIdleConnDuration,
+		MaxConnDuration:               c.MaxConnDuration,
+		MaxIdempotentCallAttempts:     c.MaxIdempotentCallAttempts,
+		ReadBufferSize:                c.ReadBufferSize,
+		WriteBufferSize:               c.WriteBufferSize,
+		ReadTimeout:                   c.ReadTimeout,
+		WriteTimeout:                  c.WriteTimeout,
+		MaxResponseBodySize:           c.MaxResponseBodySize,
+		DisableHeaderNamesNormalizing: c.DisableHeaderNamesNormalizing,
+		DisablePathNormalizing:        c.DisablePathNormalizing,
+		MaxConnWaitTimeout:            c.MaxConnWaitTimeout,
+		RetryIf:                       c.RetryIf,
+		ResponseBodyStream:            c.ResponseBodyStream,
+		proxyURI:                      c.ProxyURI,
 	}
 }
-
-type ClientOptions struct {
-	// Comma-separated list of upstream HTTP server host addresses,
-	// which are passed to Dial in a round-robin manner.
-	//
-	// Each address may contain port if default dialer is used.
-	// For example,
-	//
-	//    - foobar.com:80
-	//    - foobar.com:443
-	//    - foobar.com:8080
-	Addr string
-
-	// Client name. Used in User-Agent request header.
-	Name string
-
-	// NoDefaultUserAgentHeader when set to true, causes the default
-	// User-Agent header to be excluded from the Request.
-	NoDefaultUserAgentHeader bool
-
-	// Callback for establishing new connection to the host.
-	//
-	// Default Dial is used if not set.
-	Dial client.DialFunc
-
-	// Timeout for establishing new connections to hosts.
-	//
-	// Default DialTimeout is used if not set.
-	DialTimeout time.Duration
-
-	// Attempt to connect to both ipv4 and ipv6 host addresses
-	// if set to true.
-	//
-	// This option is used only if default TCP dialer is used,
-	// i.e. if Dial is blank.
-	//
-	// By default client connects only to ipv4 addresses,
-	// since unfortunately ipv6 remains broken in many networks worldwide :)
-	DialDualStack bool
-
-	// Whether to use TLS (aka SSL or HTTPS) for host connections.
-	// Optional TLS config.
-	TLSConfig *tls.Config
-
-	IsTLS bool
-
-	// Maximum number of connections which may be established to all hosts
-	// listed in Addr.
-	//
-	// You can change this value while the HostClient is being used
-	// using HostClient.SetMaxConns(value)
-	//
-	// DefaultMaxConnsPerHost is used if not set.
-	MaxConns int
-
-	// Keep-alive connections are closed after this duration.
-	//
-	// By default connection duration is unlimited.
-	MaxConnDuration time.Duration
-
-	// Idle keep-alive connections are closed after this duration.
-	//
-	// By default idle connections are closed
-	// after DefaultMaxIdleConnDuration.
-	MaxIdleConnDuration time.Duration
-
-	// Maximum number of attempts for idempotent calls
-	//
-	// DefaultMaxIdempotentCallAttempts is used if not set.
-	MaxIdempotentCallAttempts int
-
-	// Maximum duration for full response reading (including body).
-	//
-	// By default response read timeout is unlimited.
-	ReadTimeout time.Duration
-
-	// Maximum duration for full request writing (including body).
-	//
-	// By default request write timeout is unlimited.
-	WriteTimeout time.Duration
-
-	// Maximum response body size.
-	//
-	// The client returns errBodyTooLarge if this limit is greater than 0
-	// and response body is greater than the limit.
-	//
-	// By default response body size is unlimited.
-	MaxResponseBodySize int
-
-	// Header names are passed as-is without normalization
-	// if this option is set.
-	//
-	// Disabled header names' normalization may be useful only for proxying
-	// responses to other clients expecting case-sensitive header names.
-	//
-	// By default request and response header names are normalized, i.e.
-	// The first letter and the first letters following dashes
-	// are uppercased, while all the other letters are lowercased.
-	// Examples:
-	//
-	//     * HOST -> Host
-	//     * content-type -> Content-Type
-	//     * cONTENT-lenGTH -> Content-Length
-	DisableHeaderNamesNormalizing bool
-
-	// Path values are sent as-is without normalization
-	//
-	// Disabled path normalization may be useful for proxying incoming requests
-	// to servers that are expecting paths to be forwarded as-is.
-	//
-	// By default path values are normalized, i.e.
-	// extra slashes are removed, special characters are encoded.
-	DisablePathNormalizing bool
-
-	// Maximum duration for waiting for a free connection.
-	//
-	// By default will not waiting, return errNoFreeConns immediately
-	MaxConnWaitTimeout time.Duration
-
-	// RetryIf controls whether a retry should be attempted after an error.
-	//
-	// By default will use isIdempotent function
-	RetryIf client.RetryIfFunc
-
-	// ResponseBodyStream enables response body streaming
-	ResponseBodyStream bool
-
-	ProxyURI *protocol.URI
-}
diff --git a/pkg/protocol/http1/factory/client.go b/pkg/protocol/http1/factory/client.go
deleted file mode 100644
index ad181aa..0000000
--- a/pkg/protocol/http1/factory/client.go
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * 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 factory
-
-import (
-	"github.com/cloudwego/hertz/pkg/protocol/client"
-	"github.com/cloudwego/hertz/pkg/protocol/http1"
-	"github.com/cloudwego/hertz/pkg/protocol/suite"
-)
-
-var _ suite.ClientFactory = &clientFactory{}
-
-type clientFactory struct {
-	option *http1.ClientOptions
-}
-
-func (s *clientFactory) NewHostClient() (client client.HostClient, err error) {
-	return http1.NewHostClient(s.option), nil
-}
-
-func NewClientFactory(option *http1.ClientOptions) suite.ClientFactory {
-	return &clientFactory{
-		option: option,
-	}
-}
diff --git a/pkg/protocol/request.go b/pkg/protocol/request.go
index 7969265..51b2b02 100644
--- a/pkg/protocol/request.go
+++ b/pkg/protocol/request.go
@@ -666,7 +666,7 @@ func (req *Request) parsePostArgs() {
 	if !bytes.HasPrefix(req.Header.ContentType(), bytestr.StrPostArgsContentType) {
 		return
 	}
-	req.postArgs.ParseBytes(req.Body())
+	req.postArgs.ParseBytes(req.BodyBytes())
 }
 
 // BodyE returns request body.
diff --git a/pkg/protocol/suite/client.go b/pkg/protocol/suite/client.go
deleted file mode 100644
index cdc6d16..0000000
--- a/pkg/protocol/suite/client.go
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * 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 suite
-
-import "github.com/cloudwego/hertz/pkg/protocol/client"
-
-type ClientFactory interface {
-	NewHostClient() (hc client.HostClient, err error)
-}
diff --git a/pkg/protocol/suite/server.go b/pkg/protocol/suite/suite.go
similarity index 100%
rename from pkg/protocol/suite/server.go
rename to pkg/protocol/suite/suite.go
diff --git a/pkg/route/routergroup.go b/pkg/route/routergroup.go
index 77d957f..030fea6 100644
--- a/pkg/route/routergroup.go
+++ b/pkg/route/routergroup.go
@@ -114,8 +114,6 @@ func (group *RouterGroup) handle(httpMethod, relativePath string, handlers app.H
 	return group.returnObj()
 }
 
-var upperLetterReg = regexp.MustCompile("^[A-Z]+$")
-
 // Handle registers a new request handle and middleware with the given path and method.
 // The last handler should be the real handler, the other ones should be middleware that can and should be shared among different routes.
 // See the example code in GitHub.
@@ -127,7 +125,7 @@ var upperLetterReg = regexp.MustCompile("^[A-Z]+$")
 // frequently used, non-standardized or custom methods (e.g. for internal
 // communication with a proxy).
 func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...app.HandlerFunc) IRoutes {
-	if matches := upperLetterReg.MatchString(httpMethod); !matches {
+	if matches, err := regexp.MatchString("^[A-Z]+$", httpMethod); !matches || err != nil {
 		panic("http method " + httpMethod + " is not valid")
 	}
 	return group.handle(httpMethod, relativePath, handlers)


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 45d099b..1d38c83 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -1,10 +1,10 @@
 name: Tests
 
-on: [ push, pull_request ]
+on: [push, pull_request]
 
 jobs:
   lint-and-ut:
-    runs-on: [ self-hosted, X64 ]
+    runs-on: self-hosted
     steps:
       - uses: actions/checkout@v3
 
diff --git a/pkg/app/client/client_test.go b/pkg/app/client/client_test.go
index 9365f32..bdfbcb7 100644
--- a/pkg/app/client/client_test.go
+++ b/pkg/app/client/client_test.go
@@ -93,10 +93,11 @@ func TestCloseIdleConnections(t *testing.T) {
 	}()
 	time.Sleep(time.Millisecond * 500)
 
-	c, _ := NewClient(WithDialFunc(func(addr string) (network.Conn, error) {
-		return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
-	}))
-
+	c := &Client{
+		Dial: func(addr string) (network.Conn, error) {
+			return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
+		},
+	}
 	if _, _, err := c.Get(context.Background(), nil, "http://google.com"); err != nil {
 		t.Fatal(err)
 	}
@@ -139,9 +140,11 @@ func TestClientInvalidURI(t *testing.T) {
 	}()
 	time.Sleep(time.Millisecond * 500)
 
-	c, _ := NewClient(WithDialFunc(func(addr string) (network.Conn, error) {
-		return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
-	}))
+	c := &Client{
+		Dial: func(addr string) (network.Conn, error) {
+			return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
+		},
+	}
 	req, res := protocol.AcquireRequest(), protocol.AcquireResponse()
 	defer func() {
 		protocol.ReleaseRequest(req)
@@ -175,9 +178,11 @@ func TestClientGetWithBody(t *testing.T) {
 	}()
 	time.Sleep(time.Millisecond * 500)
 
-	c, _ := NewClient(WithDialFunc(func(addr string) (network.Conn, error) {
-		return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
-	}))
+	c := &Client{
+		Dial: func(addr string) (network.Conn, error) {
+			return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
+		},
+	}
 	req, res := protocol.AcquireRequest(), protocol.AcquireResponse()
 	defer func() {
 		protocol.ReleaseRequest(req)
@@ -220,9 +225,11 @@ func TestClientURLAuth(t *testing.T) {
 	}()
 	time.Sleep(time.Millisecond * 500)
 
-	c, _ := NewClient(WithDialFunc(func(addr string) (network.Conn, error) {
-		return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
-	}))
+	c := &Client{
+		Dial: func(addr string) (network.Conn, error) {
+			return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
+		},
+	}
 	for up, expected := range cases {
 		req := protocol.AcquireRequest()
 		req.Header.SetMethod(consts.MethodGet)
@@ -254,10 +261,11 @@ func TestClientNilResp(t *testing.T) {
 	}()
 	time.Sleep(time.Millisecond * 500)
 
-	c, _ := NewClient(WithDialFunc(func(addr string) (network.Conn, error) {
-		return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
-	}))
-
+	c := &Client{
+		Dial: func(addr string) (network.Conn, error) {
+			return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
+		},
+	}
 	req := protocol.AcquireRequest()
 	req.Header.SetMethod(consts.MethodGet)
 	req.SetRequestURI("http://example.com")
@@ -279,9 +287,11 @@ func TestClientParseConn(t *testing.T) {
 	go engine.Run()
 	time.Sleep(time.Millisecond * 500)
 
-	c, _ := NewClient(WithDialFunc(func(addr string) (network.Conn, error) {
-		return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
-	}))
+	c := &Client{
+		Dial: func(addr string) (network.Conn, error) {
+			return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
+		},
+	}
 	req, res := protocol.AcquireRequest(), protocol.AcquireResponse()
 	defer func() {
 		protocol.ReleaseRequest(req)
@@ -322,9 +332,10 @@ func TestClientPostArgs(t *testing.T) {
 		engine.Close()
 	}()
 	time.Sleep(time.Millisecond * 500)
-	c, _ := NewClient(WithDialFunc(func(addr string) (network.Conn, error) {
+	c, _ := NewClient()
+	c.Dial = func(addr string) (network.Conn, error) {
 		return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
-	}))
+	}
 	req, res := protocol.AcquireRequest(), protocol.AcquireResponse()
 	defer func() {
 		protocol.ReleaseRequest(req)
@@ -367,9 +378,11 @@ func TestClientHeaderCase(t *testing.T) {
 	}()
 	time.Sleep(time.Second)
 
-	c, _ := NewClient(WithDialFunc(func(addr string) (network.Conn, error) {
+	c, _ := NewClient()
+	c.Dial = func(addr string) (network.Conn, error) {
 		return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
-	}), WithDisableHeaderNamesNormalizing(true))
+	}
+	c.DisableHeaderNamesNormalizing = true
 	code, body, err := c.Get(context.Background(), nil, "http://example.com")
 	if err != nil {
 		t.Error(err)
@@ -401,12 +414,10 @@ func TestClientReadTimeout(t *testing.T) {
 	time.Sleep(time.Millisecond * 500)
 
 	c := &http1.HostClient{
-		ClientOptions: &http1.ClientOptions{
-			ReadTimeout:               time.Second * 4,
-			MaxIdempotentCallAttempts: 1,
-			Dial: func(addr string) (network.Conn, error) {
-				return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
-			},
+		ReadTimeout:               time.Second * 4,
+		MaxIdempotentCallAttempts: 1,
+		Dial: func(addr string) (network.Conn, error) {
+			return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
 		},
 	}
 	dialer.SetDialer(standard.NewDialer())
@@ -472,9 +483,10 @@ func TestClientDefaultUserAgent(t *testing.T) {
 	}()
 	time.Sleep(time.Millisecond * 500)
 
-	c, _ := NewClient(WithDialFunc(func(addr string) (network.Conn, error) {
+	c, _ := NewClient()
+	c.Dial = func(addr string) (network.Conn, error) {
 		return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
-	}))
+	}
 	req := protocol.AcquireRequest()
 	res := protocol.AcquireResponse()
 
@@ -509,9 +521,11 @@ func TestClientSetUserAgent(t *testing.T) {
 	time.Sleep(time.Millisecond * 500)
 
 	userAgent := "I'm not hertz"
-	c, _ := NewClient(WithDialFunc(func(addr string) (network.Conn, error) {
+	c, _ := NewClient()
+	c.Dial = func(addr string) (network.Conn, error) {
 		return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
-	}), WithName(userAgent))
+	}
+	c.Name = userAgent
 	req := protocol.AcquireRequest()
 	res := protocol.AcquireResponse()
 
@@ -540,10 +554,11 @@ func TestClientNoUserAgent(t *testing.T) {
 		engine.Close()
 	}()
 	time.Sleep(time.Millisecond * 500)
-	c, _ := NewClient(WithDialFunc(func(addr string) (network.Conn, error) {
+	c, _ := NewClient()
+	c.Dial = func(addr string) (network.Conn, error) {
 		return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
-	}), WithNoDefaultUserAgentHeader(true))
-
+	}
+	c.NoDefaultUserAgentHeader = true
 	req := protocol.AcquireRequest()
 	res := protocol.AcquireResponse()
 
@@ -625,10 +640,10 @@ func TestClientDoWithCustomHeaders(t *testing.T) {
 	time.Sleep(time.Millisecond * 500)
 
 	// make sure that the client sends all the request headers and body.
-	c, _ := NewClient(WithDialFunc(func(addr string) (network.Conn, error) {
+	c, _ := NewClient()
+	c.Dial = func(addr string) (network.Conn, error) {
 		return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
-	}))
-
+	}
 	var req protocol.Request
 	req.Header.SetMethod(consts.MethodPost)
 	req.SetRequestURI(uri)
@@ -670,10 +685,11 @@ func TestClientDoTimeoutDisablePathNormalizing(t *testing.T) {
 		engine.Close()
 	}()
 	time.Sleep(time.Millisecond * 500)
-	c, _ := NewClient(WithDialFunc(func(addr string) (network.Conn, error) {
+	c, _ := NewClient()
+	c.Dial = func(addr string) (network.Conn, error) {
 		return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
-	}), WithDisablePathNormalizing(true))
-
+	}
+	c.DisablePathNormalizing = true
 	urlWithEncodedPath := "http://example.com/encoded/Y%2BY%2FY%3D/stuff"
 
 	var req protocol.Request
@@ -713,11 +729,9 @@ func TestHostClientPendingRequests(t *testing.T) {
 	time.Sleep(time.Second)
 
 	c := &http1.HostClient{
-		ClientOptions: &http1.ClientOptions{
-			Addr: "foobar",
-			Dial: func(addr string) (network.Conn, error) {
-				return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
-			},
+		Addr: "foobar",
+		Dial: func(addr string) (network.Conn, error) {
+			return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
 		},
 	}
 
@@ -806,13 +820,11 @@ func TestHostClientMaxConnsWithDeadline(t *testing.T) {
 	time.Sleep(time.Millisecond * 500)
 
 	c := &http1.HostClient{
-		ClientOptions: &http1.ClientOptions{
-			Addr: "foobar",
-			Dial: func(addr string) (network.Conn, error) {
-				return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
-			},
-			MaxConns: 1,
+		Addr: "foobar",
+		Dial: func(addr string) (network.Conn, error) {
+			return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
 		},
+		MaxConns: 1,
 	}
 
 	for i := 0; i < 5; i++ {
@@ -877,13 +889,11 @@ func TestHostClientMaxConnDuration(t *testing.T) {
 	time.Sleep(time.Millisecond * 500)
 
 	c := &http1.HostClient{
-		ClientOptions: &http1.ClientOptions{
-			Addr: "foobar",
-			Dial: func(addr string) (network.Conn, error) {
-				return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
-			},
-			MaxConnDuration: 10 * time.Millisecond,
+		Addr: "foobar",
+		Dial: func(addr string) (network.Conn, error) {
+			return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
 		},
+		MaxConnDuration: 10 * time.Millisecond,
 	}
 
 	for i := 0; i < 5; i++ {
@@ -925,12 +935,10 @@ func TestHostClientMultipleAddrs(t *testing.T) {
 
 	dialsCount := make(map[string]int)
 	c := &http1.HostClient{
-		ClientOptions: &http1.ClientOptions{
-			Addr: "foo,bar,baz",
-			Dial: func(addr string) (network.Conn, error) {
-				dialsCount[addr]++
-				return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
-			},
+		Addr: "foo,bar,baz",
+		Dial: func(addr string) (network.Conn, error) {
+			dialsCount[addr]++
+			return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
 		},
 	}
 
@@ -992,11 +1000,9 @@ func TestClientFollowRedirects(t *testing.T) {
 	time.Sleep(time.Millisecond * 500)
 
 	c := &http1.HostClient{
-		ClientOptions: &http1.ClientOptions{
-			Addr: "xxx",
-			Dial: func(addr string) (network.Conn, error) {
-				return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
-			},
+		Addr: "xxx",
+		Dial: func(addr string) (network.Conn, error) {
+			return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
 		},
 	}
 
@@ -1088,14 +1094,12 @@ func TestHostClientMaxConnWaitTimeoutSuccess(t *testing.T) {
 	time.Sleep(time.Millisecond * 500)
 
 	c := &http1.HostClient{
-		ClientOptions: &http1.ClientOptions{
-			Addr: "foobar",
-			Dial: func(addr string) (network.Conn, error) {
-				return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
-			},
-			MaxConns:           1,
-			MaxConnWaitTimeout: 200 * time.Millisecond,
+		Addr: "foobar",
+		Dial: func(addr string) (network.Conn, error) {
+			return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
 		},
+		MaxConns:           1,
+		MaxConnWaitTimeout: 200 * time.Millisecond,
 	}
 
 	for i := 0; i < 5; i++ {
@@ -1159,14 +1163,12 @@ func TestHostClientMaxConnWaitTimeoutError(t *testing.T) {
 	time.Sleep(time.Millisecond * 500)
 
 	c := &http1.HostClient{
-		ClientOptions: &http1.ClientOptions{
-			Addr: "foobar",
-			Dial: func(addr string) (network.Conn, error) {
-				return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
-			},
-			MaxConns:           1,
-			MaxConnWaitTimeout: 10 * time.Millisecond,
+		Addr: "foobar",
+		Dial: func(addr string) (network.Conn, error) {
+			return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
 		},
+		MaxConns:           1,
+		MaxConnWaitTimeout: 10 * time.Millisecond,
 	}
 
 	var errNoFreeConnsCount uint32
@@ -1433,7 +1435,7 @@ func TestSetMultipartFields(t *testing.T) {
 	go engine.Run()
 
 	time.Sleep(100 * time.Millisecond)
-	client, _ := NewClient(WithDialTimeout(50 * time.Millisecond))
+	client := &Client{DialTimeout: 50 * time.Millisecond}
 	req := protocol.AcquireRequest()
 	rsp := protocol.AcquireResponse()
 	defer func() {
diff --git a/pkg/protocol/http1/client_test.go b/pkg/protocol/http1/client_test.go
index f334889..4927bf2 100644
--- a/pkg/protocol/http1/client_test.go
+++ b/pkg/protocol/http1/client_test.go
@@ -70,12 +70,10 @@ func TestHostClientMaxConnWaitTimeoutWithEarlierDeadline(t *testing.T) {
 	)
 
 	c := &HostClient{
-		ClientOptions: &ClientOptions{
-			Addr:               "foobar",
-			Dial:               mock.SlowReadDialer,
-			MaxConns:           1,
-			MaxConnWaitTimeout: 50 * time.Millisecond,
-		},
+		Addr:               "foobar",
+		Dial:               mock.SlowReadDialer,
+		MaxConns:           1,
+		MaxConnWaitTimeout: 50 * time.Millisecond,
 	}
 
 	var errTimeoutCount uint32
diff --git a/pkg/protocol/http1/factory/server.go b/pkg/protocol/http1/factory/factory.go
similarity index 100%
rename from pkg/protocol/http1/factory/server.go
rename to pkg/protocol/http1/factory/factory.go
diff --git a/pkg/protocol/http1/req/request_test.go b/pkg/protocol/http1/req/request_test.go
index 2edee37..177770b 100644
--- a/pkg/protocol/http1/req/request_test.go
+++ b/pkg/protocol/http1/req/request_test.go
@@ -49,7 +49,6 @@ import (
 	"io"
 	"io/ioutil"
 	"mime/multipart"
-	"net/url"
 	"strings"
 	"testing"
 
@@ -263,31 +262,6 @@ func testRequestPostArgsSuccess(t *testing.T, req *protocol.Request, s string, e
 	}
 }
 
-func TestRequestPostArgsBodyStream(t *testing.T) {
-	var req protocol.Request
-	s := "POST / HTTP/1.1\r\nHost: aaa.com\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 8196\r\n\r\n"
-	contentB := make([]byte, 8192)
-	for i := 0; i < len(contentB); i++ {
-		contentB[i] = 'a'
-	}
-	content := string(contentB)
-	requestString := s + url.Values{"key": []string{content}}.Encode()
-
-	r := bytes.NewBufferString(requestString)
-	zr := netpoll.NewReader(r)
-	if err := ReadHeader(&req.Header, zr); err != nil {
-		t.Fatalf("Unexpected error when reading header %q: %s", s, err)
-	}
-
-	err := ReadBodyStream(&req, zr, 1024*4, false, false)
-	if err != nil {
-		t.Fatalf("Unexpected error when reading bodystream %q: %s", s, err)
-	}
-	if string(req.PostArgs().Peek("key")) != content {
-		assert.DeepEqual(t, content, string(req.PostArgs().Peek("key")))
-	}
-}
-
 func testRequestWriteError(t *testing.T, method, requestURI, host, userAgent, body string) {
 	var req protocol.Request