Repository: cloudwego/hertz
Pull request #105 (2022-06-26 12:53:30)

feat: client abstracts hostclient layer
#### What type of PR is this?

feat

#### What this PR does / why we need it (English/Chinese):

en: client abstracts protocol layer for http2 and ALPN. Client removes ReadBufferSize and WriteBufferSize config.
zh: client 为 ALPN 和 http2 抽象出协议层 HostClient。client 删除 readbuffersize 和 writebuffersize 配置项。





Patch:
diff --git a/pkg/app/client/client.go b/pkg/app/client/client.go
index bfb8597..ddb7c4b 100644
--- a/pkg/app/client/client.go
+++ b/pkg/app/client/client.go
@@ -44,7 +44,6 @@ package client
 import (
 	"bytes"
 	"context"
-	"crypto/tls"
 	"fmt"
 	"sync"
 	"time"
@@ -53,10 +52,13 @@ 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")
@@ -163,7 +165,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
 }
 
@@ -215,7 +217,7 @@ func Post(ctx context.Context, dst []byte, url string, postArgs *protocol.Args)
 	return defaultClient.Post(ctx, dst, url, postArgs)
 }
 
-var defaultClient = Client{DialTimeout: consts.DefaultDialTimeout}
+var defaultClient, _ = NewClient(WithDialTimeout(consts.DefaultDialTimeout))
 
 // Client implements http client.
 //
@@ -225,145 +227,34 @@ var defaultClient = Client{DialTimeout: consts.DefaultDialTimeout}
 type Client struct {
 	noCopy nocopy.NoCopy //lint:ignore U1000 until noCopy is used
 
-	// 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 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 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
+	options *config.ClientOptions
 
-	// 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.
+	// 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.
 	//
-	// 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.
+	// The proxy type is determined by the URL scheme.
+	// "http" and "https" are supported. If the scheme is empty,
+	// "http" is assumed.
 	//
-	// By default will not waiting, return ErrNoFreeConns immediately
-	MaxConnWaitTimeout time.Duration
+	// If Proxy is nil or returns a nil *URL, no proxy is used.
+	Proxy protocol.Proxy
 
 	// RetryIf controls whether a retry should be attempted after an error.
 	//
 	// By default will use isIdempotent function
-	RetryIf client.RetryIfFunc
+	RetryIf func(request *protocol.Request) bool
 
-	// Connection will close after each request when set this to true.
-	KeepAlive bool
-
-	// ResponseBodyStream enables response body streaming
-	ResponseBodyStream bool
+	clientFactory suite.ClientFactory
 
 	mLock sync.Mutex
 	m     map[string]client.HostClient
 	ms    map[string]client.HostClient
 	mws   Middleware
+}
 
-	// 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
+func (c *Client) GetOptions() *config.ClientOptions {
+	return c.options
 }
 
 // SetProxy is used to set client proxy.
@@ -374,6 +265,16 @@ 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
@@ -532,7 +433,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.KeepAlive {
+	if !c.options.KeepAlive {
 		req.Header.SetConnectionClose(true)
 	}
 	uri := req.URI()
@@ -565,6 +466,7 @@ 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 {
@@ -573,40 +475,26 @@ 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 {
-		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,
+		if c.clientFactory == nil {
+			// load http1 client by default
+			c.clientFactory = factory.NewClientFactory(newHttp1OptionFromClient(c))
 		}
-		hc = http1.NewHostClient(hConfig)
+		hc, _ = c.clientFactory.NewHostClient()
+		hc.SetDynamicConfig(&client.DynamicConfig{
+			Addr:     h,
+			ProxyURI: proxyURI,
+			IsTLS:    isTLS,
+		})
 		m[h] = hc
 		if len(m) == 1 {
 			startCleaner = true
 		}
 	}
+
 	c.mLock.Unlock()
 
 	if startCleaner {
@@ -652,22 +540,17 @@ 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{
-		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,
+		options: opt,
 	}
+
 	return c, nil
 }
 
@@ -680,3 +563,26 @@ 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 57b3c84..af149f4 100644
--- a/pkg/app/client/option.go
+++ b/pkg/app/client/option.go
@@ -107,9 +107,37 @@ 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.DialFunc = f
+		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
 	}}
 }
diff --git a/pkg/common/config/client_option.go b/pkg/common/config/client_option.go
index 9f4deaa..4ecc757 100644
--- a/pkg/common/config/client_option.go
+++ b/pkg/common/config/client_option.go
@@ -43,7 +43,67 @@ type ClientOptions struct {
 	ReadTimeout               time.Duration
 	TLSConfig                 *tls.Config
 	ResponseBodyStream        bool
-	DialFunc                  func(addr string) (network.Conn, error)
+
+	// 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
 }
 
 func NewClientOptions(opts []ClientOption) *ClientOptions {
@@ -55,6 +115,7 @@ 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 ee6fb53..33cec15 100644
--- a/pkg/protocol/client/client.go
+++ b/pkg/protocol/client/client.go
@@ -70,6 +70,7 @@ var (
 
 type HostClient interface {
 	Doer
+	SetDynamicConfig(dc *DynamicConfig)
 	CloseIdleConnections()
 	ShouldRemove() bool
 	ConnectionCount() int
@@ -80,7 +81,8 @@ type Doer interface {
 }
 
 type HostClientConfig struct {
-	Addr string
+	DynamicConfig
+
 	Name string
 
 	NoDefaultUserAgentHeader      bool
@@ -93,8 +95,6 @@ type HostClientConfig struct {
 
 	MaxConns                  int
 	MaxIdempotentCallAttempts int
-	ReadBufferSize            int
-	WriteBufferSize           int
 	MaxResponseBodySize       int
 
 	Dial               DialFunc
@@ -107,8 +107,13 @@ 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/http1/client.go b/pkg/protocol/http1/client.go
index 838b350..f52ccda 100644
--- a/pkg/protocol/http1/client.go
+++ b/pkg/protocol/http1/client.go
@@ -58,7 +58,6 @@ 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"
@@ -88,141 +87,7 @@ var (
 type HostClient struct {
 	noCopy nocopy.NoCopy //lint:ignore U1000 until noCopy is used
 
-	// 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
+	*ClientOptions
 
 	clientName  atomic.Value
 	lastUseTime uint32
@@ -242,8 +107,12 @@ type HostClient struct {
 	pendingRequests int32
 
 	connsCleanerRun bool
+}
 
-	proxyURI *protocol.URI
+func (c *HostClient) SetDynamicConfig(dc *client.DynamicConfig) {
+	c.Addr = dc.Addr
+	c.ProxyURI = dc.ProxyURI
+	c.IsTLS = dc.IsTLS
 }
 
 type clientConn struct {
@@ -548,9 +417,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)
@@ -982,7 +851,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
 		}
@@ -996,8 +865,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 == "" {
@@ -1164,30 +1033,137 @@ func (q *wantConnQueue) clearFront() (cleaned bool) {
 	}
 }
 
-func NewHostClient(c *client.HostClientConfig) client.HostClient {
+func NewHostClient(c *ClientOptions) client.HostClient {
 	return &HostClient{
-		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,
+		ClientOptions: c,
 	}
 }
+
+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
new file mode 100644
index 0000000..ad181aa
--- /dev/null
+++ b/pkg/protocol/http1/factory/client.go
@@ -0,0 +1,39 @@
+/*
+ * 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/http1/factory/factory.go b/pkg/protocol/http1/factory/server.go
similarity index 100%
rename from pkg/protocol/http1/factory/factory.go
rename to pkg/protocol/http1/factory/server.go
diff --git a/pkg/protocol/suite/client.go b/pkg/protocol/suite/client.go
new file mode 100644
index 0000000..cdc6d16
--- /dev/null
+++ b/pkg/protocol/suite/client.go
@@ -0,0 +1,23 @@
+/*
+ * 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)
+}


Test patch:
diff --git a/pkg/app/client/client_test.go b/pkg/app/client/client_test.go
index bdfbcb7..9365f32 100644
--- a/pkg/app/client/client_test.go
+++ b/pkg/app/client/client_test.go
@@ -93,11 +93,10 @@ func TestCloseIdleConnections(t *testing.T) {
 	}()
 	time.Sleep(time.Millisecond * 500)
 
-	c := &Client{
-		Dial: func(addr string) (network.Conn, error) {
-			return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
-		},
-	}
+	c, _ := NewClient(WithDialFunc(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)
 	}
@@ -140,11 +139,9 @@ func TestClientInvalidURI(t *testing.T) {
 	}()
 	time.Sleep(time.Millisecond * 500)
 
-	c := &Client{
-		Dial: func(addr string) (network.Conn, error) {
-			return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
-		},
-	}
+	c, _ := NewClient(WithDialFunc(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)
@@ -178,11 +175,9 @@ func TestClientGetWithBody(t *testing.T) {
 	}()
 	time.Sleep(time.Millisecond * 500)
 
-	c := &Client{
-		Dial: func(addr string) (network.Conn, error) {
-			return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
-		},
-	}
+	c, _ := NewClient(WithDialFunc(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)
@@ -225,11 +220,9 @@ func TestClientURLAuth(t *testing.T) {
 	}()
 	time.Sleep(time.Millisecond * 500)
 
-	c := &Client{
-		Dial: func(addr string) (network.Conn, error) {
-			return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
-		},
-	}
+	c, _ := NewClient(WithDialFunc(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)
@@ -261,11 +254,10 @@ func TestClientNilResp(t *testing.T) {
 	}()
 	time.Sleep(time.Millisecond * 500)
 
-	c := &Client{
-		Dial: func(addr string) (network.Conn, error) {
-			return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
-		},
-	}
+	c, _ := NewClient(WithDialFunc(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")
@@ -287,11 +279,9 @@ func TestClientParseConn(t *testing.T) {
 	go engine.Run()
 	time.Sleep(time.Millisecond * 500)
 
-	c := &Client{
-		Dial: func(addr string) (network.Conn, error) {
-			return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
-		},
-	}
+	c, _ := NewClient(WithDialFunc(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)
@@ -332,10 +322,9 @@ func TestClientPostArgs(t *testing.T) {
 		engine.Close()
 	}()
 	time.Sleep(time.Millisecond * 500)
-	c, _ := NewClient()
-	c.Dial = func(addr string) (network.Conn, error) {
+	c, _ := NewClient(WithDialFunc(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)
@@ -378,11 +367,9 @@ func TestClientHeaderCase(t *testing.T) {
 	}()
 	time.Sleep(time.Second)
 
-	c, _ := NewClient()
-	c.Dial = func(addr string) (network.Conn, error) {
+	c, _ := NewClient(WithDialFunc(func(addr string) (network.Conn, error) {
 		return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
-	}
-	c.DisableHeaderNamesNormalizing = true
+	}), WithDisableHeaderNamesNormalizing(true))
 	code, body, err := c.Get(context.Background(), nil, "http://example.com")
 	if err != nil {
 		t.Error(err)
@@ -414,10 +401,12 @@ func TestClientReadTimeout(t *testing.T) {
 	time.Sleep(time.Millisecond * 500)
 
 	c := &http1.HostClient{
-		ReadTimeout:               time.Second * 4,
-		MaxIdempotentCallAttempts: 1,
-		Dial: func(addr string) (network.Conn, error) {
-			return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
+		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)
+			},
 		},
 	}
 	dialer.SetDialer(standard.NewDialer())
@@ -483,10 +472,9 @@ func TestClientDefaultUserAgent(t *testing.T) {
 	}()
 	time.Sleep(time.Millisecond * 500)
 
-	c, _ := NewClient()
-	c.Dial = func(addr string) (network.Conn, error) {
+	c, _ := NewClient(WithDialFunc(func(addr string) (network.Conn, error) {
 		return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
-	}
+	}))
 	req := protocol.AcquireRequest()
 	res := protocol.AcquireResponse()
 
@@ -521,11 +509,9 @@ func TestClientSetUserAgent(t *testing.T) {
 	time.Sleep(time.Millisecond * 500)
 
 	userAgent := "I'm not hertz"
-	c, _ := NewClient()
-	c.Dial = func(addr string) (network.Conn, error) {
+	c, _ := NewClient(WithDialFunc(func(addr string) (network.Conn, error) {
 		return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
-	}
-	c.Name = userAgent
+	}), WithName(userAgent))
 	req := protocol.AcquireRequest()
 	res := protocol.AcquireResponse()
 
@@ -554,11 +540,10 @@ func TestClientNoUserAgent(t *testing.T) {
 		engine.Close()
 	}()
 	time.Sleep(time.Millisecond * 500)
-	c, _ := NewClient()
-	c.Dial = func(addr string) (network.Conn, error) {
+	c, _ := NewClient(WithDialFunc(func(addr string) (network.Conn, error) {
 		return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
-	}
-	c.NoDefaultUserAgentHeader = true
+	}), WithNoDefaultUserAgentHeader(true))
+
 	req := protocol.AcquireRequest()
 	res := protocol.AcquireResponse()
 
@@ -640,10 +625,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()
-	c.Dial = func(addr string) (network.Conn, error) {
+	c, _ := NewClient(WithDialFunc(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)
@@ -685,11 +670,10 @@ func TestClientDoTimeoutDisablePathNormalizing(t *testing.T) {
 		engine.Close()
 	}()
 	time.Sleep(time.Millisecond * 500)
-	c, _ := NewClient()
-	c.Dial = func(addr string) (network.Conn, error) {
+	c, _ := NewClient(WithDialFunc(func(addr string) (network.Conn, error) {
 		return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
-	}
-	c.DisablePathNormalizing = true
+	}), WithDisablePathNormalizing(true))
+
 	urlWithEncodedPath := "http://example.com/encoded/Y%2BY%2FY%3D/stuff"
 
 	var req protocol.Request
@@ -729,9 +713,11 @@ func TestHostClientPendingRequests(t *testing.T) {
 	time.Sleep(time.Second)
 
 	c := &http1.HostClient{
-		Addr: "foobar",
-		Dial: func(addr string) (network.Conn, error) {
-			return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
+		ClientOptions: &http1.ClientOptions{
+			Addr: "foobar",
+			Dial: func(addr string) (network.Conn, error) {
+				return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
+			},
 		},
 	}
 
@@ -820,11 +806,13 @@ func TestHostClientMaxConnsWithDeadline(t *testing.T) {
 	time.Sleep(time.Millisecond * 500)
 
 	c := &http1.HostClient{
-		Addr: "foobar",
-		Dial: func(addr string) (network.Conn, error) {
-			return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
+		ClientOptions: &http1.ClientOptions{
+			Addr: "foobar",
+			Dial: func(addr string) (network.Conn, error) {
+				return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
+			},
+			MaxConns: 1,
 		},
-		MaxConns: 1,
 	}
 
 	for i := 0; i < 5; i++ {
@@ -889,11 +877,13 @@ func TestHostClientMaxConnDuration(t *testing.T) {
 	time.Sleep(time.Millisecond * 500)
 
 	c := &http1.HostClient{
-		Addr: "foobar",
-		Dial: func(addr string) (network.Conn, error) {
-			return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
+		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,
 		},
-		MaxConnDuration: 10 * time.Millisecond,
 	}
 
 	for i := 0; i < 5; i++ {
@@ -935,10 +925,12 @@ func TestHostClientMultipleAddrs(t *testing.T) {
 
 	dialsCount := make(map[string]int)
 	c := &http1.HostClient{
-		Addr: "foo,bar,baz",
-		Dial: func(addr string) (network.Conn, error) {
-			dialsCount[addr]++
-			return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
+		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)
+			},
 		},
 	}
 
@@ -1000,9 +992,11 @@ func TestClientFollowRedirects(t *testing.T) {
 	time.Sleep(time.Millisecond * 500)
 
 	c := &http1.HostClient{
-		Addr: "xxx",
-		Dial: func(addr string) (network.Conn, error) {
-			return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
+		ClientOptions: &http1.ClientOptions{
+			Addr: "xxx",
+			Dial: func(addr string) (network.Conn, error) {
+				return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
+			},
 		},
 	}
 
@@ -1094,12 +1088,14 @@ func TestHostClientMaxConnWaitTimeoutSuccess(t *testing.T) {
 	time.Sleep(time.Millisecond * 500)
 
 	c := &http1.HostClient{
-		Addr: "foobar",
-		Dial: func(addr string) (network.Conn, error) {
-			return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
+		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,
 		},
-		MaxConns:           1,
-		MaxConnWaitTimeout: 200 * time.Millisecond,
 	}
 
 	for i := 0; i < 5; i++ {
@@ -1163,12 +1159,14 @@ func TestHostClientMaxConnWaitTimeoutError(t *testing.T) {
 	time.Sleep(time.Millisecond * 500)
 
 	c := &http1.HostClient{
-		Addr: "foobar",
-		Dial: func(addr string) (network.Conn, error) {
-			return dialer.DialConnection(opt.Network, opt.Addr, time.Second, nil)
+		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,
 		},
-		MaxConns:           1,
-		MaxConnWaitTimeout: 10 * time.Millisecond,
 	}
 
 	var errNoFreeConnsCount uint32
@@ -1435,7 +1433,7 @@ func TestSetMultipartFields(t *testing.T) {
 	go engine.Run()
 
 	time.Sleep(100 * time.Millisecond)
-	client := &Client{DialTimeout: 50 * time.Millisecond}
+	client, _ := NewClient(WithDialTimeout(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 4927bf2..f334889 100644
--- a/pkg/protocol/http1/client_test.go
+++ b/pkg/protocol/http1/client_test.go
@@ -70,10 +70,12 @@ func TestHostClientMaxConnWaitTimeoutWithEarlierDeadline(t *testing.T) {
 	)
 
 	c := &HostClient{
-		Addr:               "foobar",
-		Dial:               mock.SlowReadDialer,
-		MaxConns:           1,
-		MaxConnWaitTimeout: 50 * time.Millisecond,
+		ClientOptions: &ClientOptions{
+			Addr:               "foobar",
+			Dial:               mock.SlowReadDialer,
+			MaxConns:           1,
+			MaxConnWaitTimeout: 50 * time.Millisecond,
+		},
 	}
 
 	var errTimeoutCount uint32