Repository: cloudwego/hertz
Pull request #344 (2022-11-03 05:36:38)

feat: support client and server writetimeout
#### What type of PR is this?
feat
#### Check the PR title.
<!--
The description of the title will be attached in Release Notes, 
so please describe it from user-oriented, what this PR does / why we need it.
Please check your PR title with the below requirements:
-->
- [x] This PR title match the format: \<type\>(optional scope): \<description\>
- [x] The description of this PR title is user-oriented and clear enough for others to understand.


#### (Optional) Translate the PR title into Chinese.
支持 client、server 的 writetimeout 和 client、server 请求级别的 timeout

#### (Optional) More detail description for this PR(en: English/zh: Chinese).
<!--
Provide more detailed info for review. If it is a perf type PR, perf data is suggested to give.
-->
en: support client and server writetimeout. Support writeTimeout in client and server request level.
zh(optional): 支持 client、server 的 writetimeout 和 client、server 请求级别的 timeout

#### Which issue(s) this PR fixes:
<!--
Automatically closes linked issue when PR is merged.
Eg: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
-->


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

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


```diff
@@           Coverage Diff            @@
##           develop     #344   +/-   ##
========================================
  Coverage    66.14%   66.14%           
========================================
  Files           85       85           
  Lines         8317     8365   +48     
========================================
+ Hits          5501     5533   +32     
- Misses        2456     2470   +14     
- Partials       360      362    +2     
```


| [Impacted Files](https://codecov.io/gh/cloudwego/hertz/pull/344?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego) | Coverage Δ | |
|---|---|---|
| [pkg/app/server/option.go](https://codecov.io/gh/cloudwego/hertz/pull/344/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego#diff-cGtnL2FwcC9zZXJ2ZXIvb3B0aW9uLmdv) | `93.49% <0.00%> (-3.15%)` | :arrow_down: |
| [pkg/common/config/option.go](https://codecov.io/gh/cloudwego/hertz/pull/344/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego#diff-cGtnL2NvbW1vbi9jb25maWcvb3B0aW9uLmdv) | `100.00% <ø> (ø)` | |
| [pkg/common/test/mock/network.go](https://codecov.io/gh/cloudwego/hertz/pull/344/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego#diff-cGtnL2NvbW1vbi90ZXN0L21vY2svbmV0d29yay5nbw==) | `0.00% <0.00%> (ø)` | |
| [pkg/network/netpoll/transport.go](https://codecov.io/gh/cloudwego/hertz/pull/344/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego#diff-cGtnL25ldHdvcmsvbmV0cG9sbC90cmFuc3BvcnQuZ28=) | `0.00% <0.00%> (ø)` | |
| [pkg/network/standard/connection.go](https://codecov.io/gh/cloudwego/hertz/pull/344/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego#diff-cGtnL25ldHdvcmsvc3RhbmRhcmQvY29ubmVjdGlvbi5nbw==) | `81.10% <0.00%> (-1.08%)` | :arrow_down: |
| [pkg/protocol/http1/client.go](https://codecov.io/gh/cloudwego/hertz/pull/344/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego#diff-cGtnL3Byb3RvY29sL2h0dHAxL2NsaWVudC5nbw==) | `36.28% <54.16%> (+0.42%)` | :arrow_up: |
| [pkg/common/config/request\_option.go](https://codecov.io/gh/cloudwego/hertz/pull/344/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego#diff-cGtnL2NvbW1vbi9jb25maWcvcmVxdWVzdF9vcHRpb24uZ28=) | `100.00% <100.00%> (ø)` | |
| [pkg/protocol/request.go](https://codecov.io/gh/cloudwego/hertz/pull/344/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego#diff-cGtnL3Byb3RvY29sL3JlcXVlc3QuZ28=) | `83.24% <100.00%> (ø)` | |
| [pkg/protocol/http1/req/request.go](https://codecov.io/gh/cloudwego/hertz/pull/344/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego#diff-cGtnL3Byb3RvY29sL2h0dHAxL3JlcS9yZXF1ZXN0Lmdv) | `61.88% <0.00%> (+1.88%)` | :arrow_up: |

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

</details>

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

Duslia: In the case of setting MaxConnWaitTimeout, if the number of connections in the connection pool exceeds the maximum number of connections and needs to establish a connection while waiting, the dialtimeout on the hostclient is used  instead of the dialtimeout in request options.

在设置了MaxConnWaitTimeout的情况下，如果连接池连接数超过了最大连接数量等待时需要建立连接的情况下，使用的是hostclient 上的dialtimeout 而不是 req options 里的 dialtimeout

Patch:
diff --git a/go.mod b/go.mod
index f5be276..450f1f6 100644
--- a/go.mod
+++ b/go.mod
@@ -6,7 +6,7 @@ require (
 	github.com/bytedance/go-tagexpr/v2 v2.9.2
 	github.com/bytedance/gopkg v0.0.0-20220413063733-65bf48ffb3a7
 	github.com/bytedance/sonic v1.5.0
-	github.com/cloudwego/netpoll v0.2.6
+	github.com/cloudwego/netpoll v0.3.1
 	github.com/fsnotify/fsnotify v1.5.4
 	github.com/tidwall/gjson v1.13.0 // indirect
 	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
diff --git a/go.sum b/go.sum
index 0d17981..f9885f8 100644
--- a/go.sum
+++ b/go.sum
@@ -6,8 +6,8 @@ github.com/bytedance/sonic v1.5.0 h1:XWdTi8bwPgxIML+eNV1IwNuTROK6EUrQ65ey8yd6fRQ
 github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
 github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06 h1:1sDoSuDPWzhkdzNVxCxtIaKiAe96ESVPv8coGwc1gZ4=
 github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
-github.com/cloudwego/netpoll v0.2.6 h1:vzN8cyayoa9RdCOG87tqkYO/j2hA4SMLC+vkcNUq6uI=
-github.com/cloudwego/netpoll v0.2.6/go.mod h1:1T2WVuQ+MQw6h6DpE45MohSvDTKdy2DlzCx2KsnPI4E=
+github.com/cloudwego/netpoll v0.3.1 h1:xByoORmCLIyKZ8gS+da06WDo3j+jvmhaqS2KeKejtBk=
+github.com/cloudwego/netpoll v0.3.1/go.mod h1:1T2WVuQ+MQw6h6DpE45MohSvDTKdy2DlzCx2KsnPI4E=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
diff --git a/pkg/app/client/option.go b/pkg/app/client/option.go
index 343ce62..8a64ed8 100644
--- a/pkg/app/client/option.go
+++ b/pkg/app/client/option.go
@@ -140,3 +140,10 @@ func WithRetryConfig(opts ...retry.Option) config.ClientOption {
 		o.RetryConfig = retryCfg
 	}}
 }
+
+// WithWriteTimeout sets write timeout.
+func WithWriteTimeout(t time.Duration) config.ClientOption {
+	return config.ClientOption{F: func(o *config.ClientOptions) {
+		o.WriteTimeout = t
+	}}
+}
diff --git a/pkg/app/server/option.go b/pkg/app/server/option.go
index 9447734..4a4a8c6 100644
--- a/pkg/app/server/option.go
+++ b/pkg/app/server/option.go
@@ -47,6 +47,15 @@ func WithReadTimeout(t time.Duration) config.Option {
 	}}
 }
 
+// WithWriteTimeout sets write timeout.
+//
+// Connection will be closed when write request timeout.
+func WithWriteTimeout(t time.Duration) config.Option {
+	return config.Option{F: func(o *config.Options) {
+		o.WriteTimeout = t
+	}}
+}
+
 // WithIdleTimeout sets idle timeout.
 //
 // Close the connection when the successive request timeout (in keepalive mode).
diff --git a/pkg/common/config/option.go b/pkg/common/config/option.go
index 15037f0..3b24f82 100644
--- a/pkg/common/config/option.go
+++ b/pkg/common/config/option.go
@@ -43,6 +43,7 @@ const (
 type Options struct {
 	KeepAliveTimeout             time.Duration
 	ReadTimeout                  time.Duration
+	WriteTimeout                 time.Duration
 	IdleTimeout                  time.Duration
 	RedirectTrailingSlash        bool
 	MaxRequestBodySize           int
diff --git a/pkg/common/config/request_option.go b/pkg/common/config/request_option.go
index 8d4bc5a..3737ac5 100644
--- a/pkg/common/config/request_option.go
+++ b/pkg/common/config/request_option.go
@@ -16,11 +16,17 @@
 
 package config
 
+import "time"
+
 var preDefinedOpts []RequestOption
 
 type RequestOptions struct {
 	tags map[string]string
 	isSD bool
+
+	dialTimeout  time.Duration
+	readTimeout  time.Duration
+	writeTimeout time.Duration
 }
 
 // RequestOption is the only struct to set request-level options.
@@ -55,6 +61,40 @@ func WithSD(b bool) RequestOption {
 	}}
 }
 
+// WithDialTimeout sets dial timeout.
+//
+// This is the request level configuration. It has a higher
+// priority than the client level configuration
+// Note: it won't take effect in the case of the number of
+// connections in the connection pool exceeds the maximum
+// number of connections and needs to establish a connection
+// while waiting.
+func WithDialTimeout(t time.Duration) RequestOption {
+	return RequestOption{F: func(o *RequestOptions) {
+		o.dialTimeout = t
+	}}
+}
+
+// WithReadTimeout sets read timeout.
+//
+// This is the request level configuration. It has a higher
+// priority than the client level configuration
+func WithReadTimeout(t time.Duration) RequestOption {
+	return RequestOption{F: func(o *RequestOptions) {
+		o.readTimeout = t
+	}}
+}
+
+// WithWriteTimeout sets write timeout.
+//
+// This is the request level configuration. It has a higher
+// priority than the client level configuration
+func WithWriteTimeout(t time.Duration) RequestOption {
+	return RequestOption{F: func(o *RequestOptions) {
+		o.writeTimeout = t
+	}}
+}
+
 func (o *RequestOptions) Apply(opts []RequestOption) {
 	for _, op := range opts {
 		op.F(o)
@@ -73,6 +113,18 @@ func (o *RequestOptions) IsSD() bool {
 	return o.isSD
 }
 
+func (o *RequestOptions) DialTimeout() time.Duration {
+	return o.dialTimeout
+}
+
+func (o *RequestOptions) ReadTimeout() time.Duration {
+	return o.readTimeout
+}
+
+func (o *RequestOptions) WriteTimeout() time.Duration {
+	return o.writeTimeout
+}
+
 func (o *RequestOptions) CopyTo(dst *RequestOptions) {
 	if dst.tags == nil {
 		dst.tags = make(map[string]string)
@@ -83,6 +135,9 @@ func (o *RequestOptions) CopyTo(dst *RequestOptions) {
 	}
 
 	dst.isSD = o.isSD
+	dst.readTimeout = o.readTimeout
+	dst.writeTimeout = o.writeTimeout
+	dst.dialTimeout = o.dialTimeout
 }
 
 // SetPreDefinedOpts Pre define some RequestOption here
diff --git a/pkg/common/errors/errors.go b/pkg/common/errors/errors.go
index dd247be..dc3f4bb 100644
--- a/pkg/common/errors/errors.go
+++ b/pkg/common/errors/errors.go
@@ -55,6 +55,9 @@ var (
 	ErrHijacked        = errors.New("connection has been hijacked")
 	ErrIdleTimeout     = errors.New("idle timeout")
 	ErrTimeout         = errors.New("timeout")
+	ErrReadTimeout     = errors.New("read timeout")
+	ErrWriteTimeout    = errors.New("write timeout")
+	ErrDialTimeout     = errors.New("dial timeout")
 	ErrNothingRead     = errors.New("nothing read")
 	ErrShortConnection = errors.New("short connection")
 )
diff --git a/pkg/network/connection.go b/pkg/network/connection.go
index 9a5e358..c963f2b 100644
--- a/pkg/network/connection.go
+++ b/pkg/network/connection.go
@@ -83,6 +83,7 @@ type Conn interface {
 
 	// SetReadTimeout should work for every Read process
 	SetReadTimeout(t time.Duration) error
+	SetWriteTimeout(t time.Duration) error
 }
 
 type ConnTLSer interface {
diff --git a/pkg/network/netpoll/transport.go b/pkg/network/netpoll/transport.go
index 844445e..1ea41f4 100644
--- a/pkg/network/netpoll/transport.go
+++ b/pkg/network/netpoll/transport.go
@@ -37,6 +37,7 @@ type transporter struct {
 	addr             string
 	keepAliveTimeout time.Duration
 	readTimeout      time.Duration
+	writeTimeout     time.Duration
 	listener         net.Listener
 	eventLoop        netpoll.EventLoop
 	listenConfig     *net.ListenConfig
@@ -49,6 +50,7 @@ func NewTransporter(options *config.Options) network.Transporter {
 		addr:             options.Addr,
 		keepAliveTimeout: options.KeepAliveTimeout,
 		readTimeout:      options.ReadTimeout,
+		writeTimeout:     options.WriteTimeout,
 		listener:         nil,
 		eventLoop:        nil,
 		listenConfig:     options.ListenConfig,
@@ -74,6 +76,9 @@ func (t *transporter) ListenAndServe(onReq network.OnData) (err error) {
 		netpoll.WithIdleTimeout(t.keepAliveTimeout),
 		netpoll.WithOnPrepare(func(conn netpoll.Connection) context.Context {
 			conn.SetReadTimeout(t.readTimeout) // nolint:errcheck
+			if t.writeTimeout > 0 {
+				conn.SetWriteTimeout(t.writeTimeout)
+			}
 			return context.Background()
 		}),
 	}
diff --git a/pkg/network/standard/connection.go b/pkg/network/standard/connection.go
index d15f060..b1d96e0 100644
--- a/pkg/network/standard/connection.go
+++ b/pkg/network/standard/connection.go
@@ -44,6 +44,13 @@ type Conn struct {
 	maxSize      int      // history max malloc size
 }
 
+func (c *Conn) SetWriteTimeout(t time.Duration) error {
+	if t <= 0 {
+		return c.c.SetWriteDeadline(time.Time{})
+	}
+	return c.c.SetWriteDeadline(time.Now().Add(t))
+}
+
 func (c *Conn) SetReadTimeout(t time.Duration) error {
 	if t <= 0 {
 		return c.c.SetReadDeadline(time.Time{})
diff --git a/pkg/protocol/http1/client.go b/pkg/protocol/http1/client.go
index d5d5d03..a1f18cb 100644
--- a/pkg/protocol/http1/client.go
+++ b/pkg/protocol/http1/client.go
@@ -57,6 +57,7 @@ import (
 	"github.com/cloudwego/hertz/internal/bytestr"
 	"github.com/cloudwego/hertz/internal/nocopy"
 	"github.com/cloudwego/hertz/pkg/app/client/retry"
+	"github.com/cloudwego/hertz/pkg/common/config"
 	errs "github.com/cloudwego/hertz/pkg/common/errors"
 	"github.com/cloudwego/hertz/pkg/common/hlog"
 	"github.com/cloudwego/hertz/pkg/common/timer"
@@ -408,6 +409,33 @@ func (c *HostClient) do(req *protocol.Request, resp *protocol.Response) (bool, e
 	return canIdempotentRetry, err
 }
 
+type requestConfig struct {
+	dialTimeout  time.Duration
+	readTimeout  time.Duration
+	writeTimeout time.Duration
+}
+
+func (c *HostClient) preHandleConfig(o *config.RequestOptions) requestConfig {
+	rc := requestConfig{
+		dialTimeout:  c.DialTimeout,
+		readTimeout:  c.ReadTimeout,
+		writeTimeout: c.WriteTimeout,
+	}
+	if o.ReadTimeout() > 0 {
+		rc.readTimeout = o.ReadTimeout()
+	}
+
+	if o.WriteTimeout() > 0 {
+		rc.writeTimeout = o.WriteTimeout()
+	}
+
+	if o.DialTimeout() > 0 {
+		rc.dialTimeout = o.DialTimeout()
+	}
+
+	return rc
+}
+
 func (c *HostClient) doNonNilReqResp(req *protocol.Request, resp *protocol.Response) (bool, error) {
 	if req == nil {
 		panic("BUG: req cannot be nil")
@@ -418,9 +446,10 @@ func (c *HostClient) doNonNilReqResp(req *protocol.Request, resp *protocol.Respo
 
 	atomic.StoreUint32(&c.lastUseTime, uint32(time.Now().Unix()-startTimeUnix))
 
+	rc := c.preHandleConfig(req.Options())
+
 	// Free up resources occupied by response before sending the request,
 	// so the GC may reclaim these resources (e.g. response body).
-
 	// backing up SkipBody in case it was set explicitly
 	customSkipBody := resp.SkipBody
 	resp.Reset()
@@ -429,7 +458,7 @@ func (c *HostClient) doNonNilReqResp(req *protocol.Request, resp *protocol.Respo
 	if c.DisablePathNormalizing {
 		req.URI().DisablePathNormalizing = true
 	}
-	cc, err := c.acquireConn()
+	cc, err := c.acquireConn(rc.dialTimeout)
 	// if getting connection error, fast fail
 	if err != nil {
 		return false, err
@@ -445,11 +474,8 @@ func (c *HostClient) doNonNilReqResp(req *protocol.Request, resp *protocol.Respo
 
 	resp.ParseNetAddr(conn)
 
-	if c.WriteTimeout > 0 {
-		// Set Deadline every time, since golang has fixed the performance issue
-		// See https://github.com/golang/go/issues/15133#issuecomment-271571395 for details
-		currentTime := time.Now()
-		if err = conn.SetWriteDeadline(currentTime.Add(c.WriteTimeout)); err != nil {
+	if rc.writeTimeout > 0 {
+		if err = conn.SetWriteTimeout(rc.writeTimeout); err != nil {
 			c.closeConn(cc)
 			// try another connection if retry is enabled
 			return true, err
@@ -486,10 +512,10 @@ func (c *HostClient) doNonNilReqResp(req *protocol.Request, resp *protocol.Respo
 		return true, err
 	}
 
-	if c.ReadTimeout > 0 {
+	if rc.readTimeout > 0 {
 		// Set Deadline every time, since golang has fixed the performance issue
 		// See https://github.com/golang/go/issues/15133#issuecomment-271571395 for details
-		if err = conn.SetReadTimeout(c.ReadTimeout); err != nil {
+		if err = conn.SetReadTimeout(rc.readTimeout); err != nil {
 			c.closeConn(cc)
 			// try another connection if retry is enabled
 			return true, err
@@ -543,7 +569,7 @@ func (c *HostClient) SetMaxConns(newMaxConns int) {
 	c.connsLock.Unlock()
 }
 
-func (c *HostClient) acquireConn() (cc *clientConn, err error) {
+func (c *HostClient) acquireConn(dialTimeout time.Duration) (cc *clientConn, err error) {
 	createConn := false
 	startCleaner := false
 
@@ -594,6 +620,11 @@ func (c *HostClient) acquireConn() (cc *clientConn, err error) {
 			}
 		}()
 
+		// Note: In the case of setting MaxConnWaitTimeout, if the number
+		// of connections in the connection pool exceeds the maximum
+		// number of connections and needs to establish a connection while
+		// waiting, the dialtimeout on the hostclient is used instead of
+		// the dialtimeout in request options.
 		c.queueForIdle(w)
 
 		select {
@@ -608,7 +639,7 @@ func (c *HostClient) acquireConn() (cc *clientConn, err error) {
 		go c.connsCleaner()
 	}
 
-	conn, err := c.dialHostHard()
+	conn, err := c.dialHostHard(dialTimeout)
 	if err != nil {
 		c.decConnsCount()
 		return nil, err
@@ -629,7 +660,7 @@ func (c *HostClient) queueForIdle(w *wantConn) {
 }
 
 func (c *HostClient) dialConnFor(w *wantConn) {
-	conn, err := c.dialHostHard()
+	conn, err := c.dialHostHard(c.DialTimeout)
 	if err != nil {
 		w.tryDeliver(nil, err)
 		c.decConnsCount()
@@ -858,7 +889,7 @@ func (c *HostClient) nextAddr() string {
 	return addr
 }
 
-func (c *HostClient) dialHostHard() (conn network.Conn, err error) {
+func (c *HostClient) dialHostHard(dialTimeout time.Duration) (conn network.Conn, err error) {
 	// attempt to dial all the available hosts before giving up.
 
 	c.addrsLock.Lock()
@@ -870,12 +901,11 @@ func (c *HostClient) dialHostHard() (conn network.Conn, err error) {
 		n = 1
 	}
 
-	timeout := c.DialTimeout
-	deadline := time.Now().Add(timeout)
+	deadline := time.Now().Add(dialTimeout)
 	for n > 0 {
 		addr := c.nextAddr()
 		tlsConfig := c.cachedTLSConfig(addr)
-		conn, err = dialAddr(addr, c.Dialer, c.DialDualStack, tlsConfig, timeout, c.ProxyURI, c.IsTLS)
+		conn, err = dialAddr(addr, c.Dialer, c.DialDualStack, tlsConfig, dialTimeout, c.ProxyURI, c.IsTLS)
 		if err == nil {
 			return conn, nil
 		}


Test patch:
diff --git a/pkg/app/client/option_test.go b/pkg/app/client/option_test.go
index 149d6a7..39ac268 100644
--- a/pkg/app/client/option_test.go
+++ b/pkg/app/client/option_test.go
@@ -43,6 +43,7 @@ func TestClientOptions(t *testing.T) {
 			retry.WithMaxJitter(1*time.Second),
 			retry.WithDelayPolicy(retry.CombineDelay(retry.FixedDelayPolicy, retry.BackOffDelayPolicy, retry.RandomDelayPolicy)),
 		),
+		WithWriteTimeout(time.Second),
 	})
 	assert.DeepEqual(t, 100*time.Millisecond, opt.DialTimeout)
 	assert.DeepEqual(t, 128, opt.MaxConnsPerHost)
@@ -51,6 +52,7 @@ func TestClientOptions(t *testing.T) {
 	assert.DeepEqual(t, 5*time.Second, opt.MaxConnWaitTimeout)
 	assert.DeepEqual(t, false, opt.KeepAlive)
 	assert.DeepEqual(t, 1*time.Second, opt.ReadTimeout)
+	assert.DeepEqual(t, 1*time.Second, opt.WriteTimeout)
 	assert.DeepEqual(t, true, opt.ResponseBodyStream)
 	assert.DeepEqual(t, uint(2), opt.RetryConfig.MaxAttemptTimes)
 	assert.DeepEqual(t, 100*time.Millisecond, opt.RetryConfig.Delay)
diff --git a/pkg/common/config/option_test.go b/pkg/common/config/option_test.go
index 71eeae7..49ea661 100644
--- a/pkg/common/config/option_test.go
+++ b/pkg/common/config/option_test.go
@@ -18,6 +18,7 @@ package config
 
 import (
 	"testing"
+	"time"
 
 	"github.com/cloudwego/hertz/pkg/app/server/registry"
 	"github.com/cloudwego/hertz/pkg/common/test/assert"
@@ -30,6 +31,7 @@ func TestDefaultOptions(t *testing.T) {
 	assert.DeepEqual(t, defaultKeepAliveTimeout, options.KeepAliveTimeout)
 	assert.DeepEqual(t, defaultReadTimeout, options.ReadTimeout)
 	assert.DeepEqual(t, defaultReadTimeout, options.IdleTimeout)
+	assert.DeepEqual(t, time.Duration(0), options.WriteTimeout)
 	assert.True(t, options.RedirectTrailingSlash)
 	assert.True(t, options.RedirectTrailingSlash)
 	assert.False(t, options.HandleMethodNotAllowed)
diff --git a/pkg/common/config/request_option_test.go b/pkg/common/config/request_option_test.go
index c044c43..8041f58 100644
--- a/pkg/common/config/request_option_test.go
+++ b/pkg/common/config/request_option_test.go
@@ -18,6 +18,7 @@ package config
 
 import (
 	"testing"
+	"time"
 
 	"github.com/cloudwego/hertz/pkg/common/test/assert"
 )
@@ -29,10 +30,16 @@ func TestRequestOptions(t *testing.T) {
 		WithTag("c", "d"),
 		WithTag("e", "f"),
 		WithSD(true),
+		WithDialTimeout(time.Second),
+		WithReadTimeout(time.Second),
+		WithWriteTimeout(time.Second),
 	})
 	assert.DeepEqual(t, "b", opt.Tag("a"))
 	assert.DeepEqual(t, "d", opt.Tag("c"))
 	assert.DeepEqual(t, "f", opt.Tag("e"))
+	assert.DeepEqual(t, time.Second, opt.DialTimeout())
+	assert.DeepEqual(t, time.Second, opt.ReadTimeout())
+	assert.DeepEqual(t, time.Second, opt.WriteTimeout())
 	assert.True(t, opt.IsSD())
 }
 
@@ -52,6 +59,9 @@ func TestRequestOptionsWithDefaultOpts(t *testing.T) {
 	assert.False(t, opt.IsSD())
 	SetPreDefinedOpts()
 	assert.Nil(t, preDefinedOpts)
+	assert.DeepEqual(t, time.Duration(0), opt.WriteTimeout())
+	assert.DeepEqual(t, time.Duration(0), opt.ReadTimeout())
+	assert.DeepEqual(t, time.Duration(0), opt.DialTimeout())
 }
 
 // TestRequestOptions_CopyTo test request options copy to another one
diff --git a/pkg/common/test/mock/network.go b/pkg/common/test/mock/network.go
index 29777ad..c7111b2 100644
--- a/pkg/common/test/mock/network.go
+++ b/pkg/common/test/mock/network.go
@@ -34,14 +34,28 @@ type Conn struct {
 	zw          network.ReadWriter
 }
 
+func (m *Conn) SetWriteTimeout(t time.Duration) error {
+	// TODO implement me
+	panic("implement me")
+}
+
 type SlowReadConn struct {
 	*Conn
 }
 
+func (m *SlowReadConn) SetWriteTimeout(t time.Duration) error {
+	// TODO implement me
+	panic("implement me")
+}
+
 func SlowReadDialer(addr string) (network.Conn, error) {
 	return NewSlowReadConn(""), nil
 }
 
+func SlowWriteDialer(addr string) (network.Conn, error) {
+	return NewSlowWriteConn(""), nil
+}
+
 func (m *Conn) ReadBinary(n int) (p []byte, err error) {
 	return m.zr.(netpoll.Reader).ReadBinary(n)
 }
@@ -100,7 +114,7 @@ func (m *SlowReadConn) Peek(i int) ([]byte, error) {
 	time.Sleep(100 * time.Millisecond)
 	if err != nil || len(b) != i {
 		time.Sleep(m.readTimeout)
-		return nil, errs.ErrTimeout
+		return nil, errs.ErrReadTimeout
 	}
 	return b, err
 }
@@ -119,6 +133,30 @@ func NewSlowReadConn(source string) *SlowReadConn {
 	return &SlowReadConn{NewConn(source)}
 }
 
+type SlowWriteConn struct {
+	*Conn
+	writeTimeout time.Duration
+}
+
+func (m *SlowWriteConn) SetWriteTimeout(t time.Duration) error {
+	m.writeTimeout = t
+	return nil
+}
+
+func NewSlowWriteConn(source string) *SlowWriteConn {
+	return &SlowWriteConn{NewConn(source), 0}
+}
+
+func (m *SlowWriteConn) Flush() error {
+	err := m.zw.Flush()
+	time.Sleep(100 * time.Millisecond)
+	if err == nil {
+		time.Sleep(m.writeTimeout)
+		return errs.ErrWriteTimeout
+	}
+	return err
+}
+
 func (m *Conn) Close() error {
 	return nil
 }
diff --git a/pkg/network/netpoll/connection_test.go b/pkg/network/netpoll/connection_test.go
index 79dc80b..893b328 100644
--- a/pkg/network/netpoll/connection_test.go
+++ b/pkg/network/netpoll/connection_test.go
@@ -85,6 +85,11 @@ type mockConn struct {
 	off int
 }
 
+func (m *mockConn) SetWriteTimeout(timeout time.Duration) error {
+	// TODO implement me
+	panic("implement me")
+}
+
 // mockConn's methods is simplified for unit test
 // Peek returns the next n bytes without advancing the reader
 func (m *mockConn) Peek(n int) (b []byte, err error) {
diff --git a/pkg/protocol/http1/client_test.go b/pkg/protocol/http1/client_test.go
index 299a028..fa6d6c9 100644
--- a/pkg/protocol/http1/client_test.go
+++ b/pkg/protocol/http1/client_test.go
@@ -55,6 +55,9 @@ import (
 	"testing"
 	"time"
 
+	"github.com/cloudwego/hertz/pkg/common/config"
+	"github.com/cloudwego/hertz/pkg/common/test/assert"
+
 	errs "github.com/cloudwego/hertz/pkg/common/errors"
 	"github.com/cloudwego/hertz/pkg/common/test/mock"
 	"github.com/cloudwego/hertz/pkg/network"
@@ -223,3 +226,100 @@ func (m *mockDialer) DialTimeout(network, address string, timeout time.Duration,
 func (m *mockDialer) AddTLS(conn network.Conn, tlsConfig *tls.Config) (network.Conn, error) {
 	return nil, nil
 }
+
+type slowDialer struct {
+	*mockDialer
+}
+
+func (s *slowDialer) DialConnection(network, address string, timeout time.Duration, tlsConfig *tls.Config) (conn network.Conn, err error) {
+	time.Sleep(timeout)
+	return nil, errs.ErrDialTimeout
+}
+
+func TestReadTimeoutPriority(t *testing.T) {
+	c := &HostClient{
+		ClientOptions: &ClientOptions{
+			Dialer: newSlowConnDialer(func(network, addr string) (network.Conn, error) {
+				return mock.SlowReadDialer(addr)
+			}),
+			MaxConns:           1,
+			MaxConnWaitTimeout: 50 * time.Millisecond,
+			ReadTimeout:        time.Second * 3,
+		},
+		Addr: "foobar",
+	}
+
+	req := protocol.AcquireRequest()
+	req.SetRequestURI("http://foobar/baz")
+	req.SetOptions(config.WithReadTimeout(time.Second * 1))
+	resp := protocol.AcquireResponse()
+
+	ch := make(chan error, 1)
+	go func() {
+		ch <- c.Do(context.Background(), req, resp)
+	}()
+	select {
+	case <-time.After(time.Second * 2):
+		t.Fatalf("should use readTimeout in request options")
+	case err := <-ch:
+		assert.DeepEqual(t, errs.ErrTimeout.Error(), err.Error())
+	}
+}
+
+func TestWriteTimeoutPriority(t *testing.T) {
+	c := &HostClient{
+		ClientOptions: &ClientOptions{
+			Dialer: newSlowConnDialer(func(network, addr string) (network.Conn, error) {
+				return mock.SlowWriteDialer(addr)
+			}),
+			MaxConns:           1,
+			MaxConnWaitTimeout: 50 * time.Millisecond,
+			WriteTimeout:       time.Second * 3,
+		},
+		Addr: "foobar",
+	}
+
+	req := protocol.AcquireRequest()
+	req.SetRequestURI("http://foobar/baz")
+	req.SetOptions(config.WithWriteTimeout(time.Second * 1))
+	resp := protocol.AcquireResponse()
+
+	ch := make(chan error, 1)
+	go func() {
+		ch <- c.Do(context.Background(), req, resp)
+	}()
+	select {
+	case <-time.After(time.Second * 2000):
+		t.Fatalf("should use writeTimeout in request options")
+	case err := <-ch:
+		assert.DeepEqual(t, errs.ErrWriteTimeout.Error(), err.Error())
+	}
+}
+
+func TestDialTimeoutPriority(t *testing.T) {
+	c := &HostClient{
+		ClientOptions: &ClientOptions{
+			Dialer:             &slowDialer{},
+			MaxConns:           1,
+			MaxConnWaitTimeout: 50 * time.Millisecond,
+			DialTimeout:        time.Second * 3,
+		},
+		Addr: "foobar",
+	}
+
+	req := protocol.AcquireRequest()
+	req.SetRequestURI("http://foobar/baz")
+	req.SetOptions(config.WithDialTimeout(time.Second * 1))
+	resp := protocol.AcquireResponse()
+
+	ch := make(chan error, 1)
+	go func() {
+		ch <- c.Do(context.Background(), req, resp)
+	}()
+	select {
+	case <-time.After(time.Second * 2000):
+		t.Fatalf("should use dialTimeout in request options")
+	case err := <-ch:
+		assert.DeepEqual(t, errs.ErrDialTimeout.Error(), err.Error())
+	}
+}
diff --git a/pkg/route/engine_test.go b/pkg/route/engine_test.go
index f3007ac..1d53194 100644
--- a/pkg/route/engine_test.go
+++ b/pkg/route/engine_test.go
@@ -455,6 +455,11 @@ func TestRenderHtmlOfFilesWithAutoRender(t *testing.T) {
 
 type mockConn struct{}
 
+func (m *mockConn) SetWriteTimeout(t time.Duration) error {
+	// TODO implement me
+	panic("implement me")
+}
+
 func (m *mockConn) ReadBinary(n int) (p []byte, err error) {
 	panic("implement me")
 }