Repository: valyala/fasthttp
Pull request #1833 (2024-08-16 15:53:35)

Using atomic instead of mutex and delete scratch slice

newacorn: Whether using an array with a lock or a linked list with atomic operations to manage the workerChan resources, the subsequent operations on workerChan are I/O-intensive. Given that the operations are FILO and each element involves significant I/O, I don't think a linked list has any particular advantage.
NikoMalik: > Whether using an array with a lock or a linked list with atomic operations to manage the workerChan resources, the subsequent operations on workerChan are I/O-intensive. Given that the operations are FILO and each element involves significant I/O, I don't think a linked list has any particular advantage.



I got a boost in benchmark tests by almost a factor and a half


Patch:
diff --git a/README.md b/README.md
index cbdb9de..f43ea6d 100644
--- a/README.md
+++ b/README.md
@@ -600,7 +600,7 @@ This is an **unsafe** way, the result string and `[]byte` buffer share the same
 
 * *Which GO versions are supported by fasthttp?*
 
-  Go 1.18.x. Older versions won't be supported.
+  Go 1.21.x and newer. Older versions might work, but won't officially be supported.
 
 * *Please provide real benchmark data and server information*
 
diff --git a/client.go b/client.go
index 42a01d5..bd75ad1 100644
--- a/client.go
+++ b/client.go
@@ -655,11 +655,14 @@ type DialFunc func(addr string) (net.Conn, error)
 //   - foobar.com:8080
 type DialFuncWithTimeout func(addr string, timeout time.Duration) (net.Conn, error)
 
-// RetryIfFunc signature of retry if function.
-//
+// RetryIfFunc defines the signature of the retry if function.
 // Request argument passed to RetryIfFunc, if there are any request errors.
 type RetryIfFunc func(request *Request) bool
 
+// RetryIfErrFunc defines the signature of the retry if error function.
+// request and error passed to RetryIfErrFunc, if there are any request errors.
+type RetryIfErrFunc func(request *Request, err error) bool
+
 // RoundTripper wraps every request/response.
 type RoundTripper interface {
 	RoundTrip(hc *HostClient, req *Request, resp *Response) (retry bool, err error)
@@ -709,10 +712,16 @@ type HostClient struct {
 	TLSConfig *tls.Config
 
 	// RetryIf controls whether a retry should be attempted after an error.
+	// By default, it uses the isIdempotent function.
 	//
-	// By default will use isIdempotent function
+	// Deprecated: Use RetryIfErr instead.
+	// Panics if both RetryIf and RetryIfErr are set.
 	RetryIf RetryIfFunc
 
+	// RetryIfErr controls whether a retry should be attempted after an error.
+	// By default, it uses the isIdempotent function.
+	RetryIfErr RetryIfErrFunc
+
 	connsWait *wantConnQueue
 
 	tlsConfigMap map[string]*tls.Config
@@ -1278,14 +1287,16 @@ func (c *HostClient) DoRedirects(req *Request, resp *Response, maxRedirectsCount
 func (c *HostClient) Do(req *Request, resp *Response) error {
 	var err error
 	var retry bool
+	// Based on issue #1744, we added the RetryIfErr field.
+	// To avoid ambiguous semantics due to mixed usage,
+	// only one of the fields can be set at a time.
+	if c.RetryIf != nil && c.RetryIfErr != nil {
+		panic("RetryIf and RetryIfErr must not be set at the same time")
+	}
 	maxAttempts := c.MaxIdemponentCallAttempts
 	if maxAttempts <= 0 {
 		maxAttempts = DefaultMaxIdemponentCallAttempts
 	}
-	isRequestRetryable := isIdempotent
-	if c.RetryIf != nil {
-		isRequestRetryable = c.RetryIf
-	}
 	attempts := 0
 	hasBodyStream := req.IsBodyStream()
 
@@ -1318,7 +1329,17 @@ func (c *HostClient) Do(req *Request, resp *Response) error {
 		if hasBodyStream {
 			break
 		}
-		if !isRequestRetryable(req) {
+
+		switch {
+		case c.RetryIf != nil:
+			retry = c.RetryIf(req)
+		case c.RetryIfErr != nil:
+			retry = c.RetryIfErr(req, err)
+		default:
+			retry = isIdempotent(req)
+		}
+
+		if !retry {
 			// Retry non-idempotent requests if the server closes
 			// the connection before sending the response.
 			//
@@ -1365,9 +1386,7 @@ func (c *HostClient) do(req *Request, resp *Response) (bool, error) {
 		defer ReleaseResponse(resp)
 	}
 
-	ok, err := c.doNonNilReqResp(req, resp)
-
-	return ok, err
+	return c.doNonNilReqResp(req, resp)
 }
 
 func (c *HostClient) doNonNilReqResp(req *Request, resp *Response) (bool, error) {
@@ -1552,6 +1571,7 @@ func (c *HostClient) acquireConn(reqTimeout time.Duration, connectionClose bool)
 		case <-w.ready:
 			return w.conn, w.err
 		case <-tc.C:
+			c.connsWait.failedWaiters.Add(1)
 			if timeoutOverridden {
 				return nil, ErrTimeout
 			}
@@ -1697,6 +1717,7 @@ func (c *HostClient) decConnsCount() {
 				dialed = true
 				break
 			}
+			c.connsWait.failedWaiters.Add(-1)
 		}
 	}
 	if !dialed {
@@ -1749,8 +1770,19 @@ func (c *HostClient) releaseConn(cc *clientConn) {
 			w := q.popFront()
 			if w.waiting() {
 				delivered = w.tryDeliver(cc, nil)
-				break
+				// This is the last resort to hand over conCount sema.
+				// We must ensure that there are no valid waiters in connsWait
+				// when we exit this loop.
+				//
+				// We did not apply the same looping pattern in the decConnsCount
+				// method because it needs to create a new time-spent connection,
+				// and the decConnsCount call chain will inevitably reach this point.
+				// When MaxConnWaitTimeout>0.
+				if delivered {
+					break
+				}
 			}
+			c.connsWait.failedWaiters.Add(-1)
 		}
 	}
 	if !delivered {
@@ -2106,11 +2138,17 @@ type wantConnQueue struct {
 	head    []*wantConn
 	tail    []*wantConn
 	headPos int
+	// failedWaiters is the number of waiters in the head or tail queue,
+	// but is invalid.
+	// These state waiters cannot truly be considered as waiters; the current
+	// implementation does not immediately remove them when they become
+	// invalid but instead only marks them.
+	failedWaiters atomic.Int64
 }
 
 // len returns the number of items in the queue.
 func (q *wantConnQueue) len() int {
-	return len(q.head) - q.headPos + len(q.tail)
+	return len(q.head) - q.headPos + len(q.tail) - int(q.failedWaiters.Load())
 }
 
 // pushBack adds w to the back of the queue.
@@ -2154,6 +2192,7 @@ func (q *wantConnQueue) clearFront() (cleaned bool) {
 			return cleaned
 		}
 		q.popFront()
+		q.failedWaiters.Add(-1)
 		cleaned = true
 	}
 }
diff --git a/fasthttpproxy/dialer.go b/fasthttpproxy/dialer.go
new file mode 100644
index 0000000..9846491
--- /dev/null
+++ b/fasthttpproxy/dialer.go
@@ -0,0 +1,263 @@
+package fasthttpproxy
+
+import (
+	"bufio"
+	"encoding/base64"
+	"errors"
+	"fmt"
+	"net"
+	"net/url"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/valyala/fasthttp"
+	"golang.org/x/net/http/httpproxy"
+	"golang.org/x/net/proxy"
+)
+
+var (
+	// Used for caching authentication information when using an HTTP proxy,
+	// it helps avoid re-encoding the authentication details when the ProxyURL
+	// changes along with the request URL.
+	authCache    = sync.Map{}
+	colonTLSPort = ":443"
+	tmpURL       = &url.URL{Scheme: httpsScheme, Host: "example.com"}
+)
+
+// Dialer embeds both fasthttp.TCPDialer and httpproxy.Config, allowing it
+// to take advantage of the optimizations provided by fasthttp for dialing while also
+// utilizing the finer-grained configuration options offered by httpproxy.
+type Dialer struct {
+	fasthttp.TCPDialer
+	// Support HTTPProxy, HTTPSProxy and NoProxy configuration.
+	//
+	// HTTPProxy represents the value of the HTTP_PROXY or
+	// http_proxy environment variable. It will be used as the proxy
+	// URL for HTTP requests unless overridden by NoProxy.
+	//
+	// HTTPSProxy represents the HTTPS_PROXY or https_proxy
+	// environment variable. It will be used as the proxy URL for
+	// HTTPS requests unless overridden by NoProxy.
+	//
+	// NoProxy represents the NO_PROXY or no_proxy environment
+	// variable. It specifies a string that contains comma-separated values
+	// specifying hosts that should be excluded from proxying. Each value is
+	// represented by an IP address prefix (1.2.3.4), an IP address prefix in
+	// CIDR notation (1.2.3.4/8), a domain name, or a special DNS label (*).
+	// An IP address prefix and domain name can also include a literal port
+	// number (1.2.3.4:80).
+	// A domain name matches that name and all subdomains. A domain name with
+	// a leading "." matches subdomains only. For example "foo.com" matches
+	// "foo.com" and "bar.foo.com"; ".y.com" matches "x.y.com" but not "y.com".
+	// A single asterisk (*) indicates that no proxying should be done.
+	// A best effort is made to parse the string and errors are
+	// ignored.
+	httpproxy.Config
+	// Attempt to connect to both ipv4 and ipv6 addresses if set to true.
+	// By default, dial only to ipv4 addresses,
+	// since unfortunately ipv6 remains broken in many networks worldwide :)
+	//
+	// This field from the fasthttp client is provided redundantly here because
+	// when we customize the Dial function for the client, its DialDualStack field
+	// configuration becomes ineffective.
+	DialDualStack bool
+	// Dial timeout.
+	//
+	// This field from the fasthttp client is provided redundantly here because
+	// when we customize the Dial function for the client, its DialTimeout field
+	// configuration becomes ineffective.
+	Timeout time.Duration
+	// The timeout for sending a CONNECT request when using an HTTP proxy.
+	ConnectTimeout time.Duration
+}
+
+// GetDialFunc method returns a fasthttp-style dial function. The useEnv parameter
+// determines whether the proxy address comes from Dialer.Config or from environment variables.
+func (d *Dialer) GetDialFunc(useEnv bool) (dialFunc fasthttp.DialFunc, err error) {
+	config := &d.Config
+	if useEnv {
+		config = httpproxy.FromEnvironment()
+	}
+	proxyURLIsSame := false
+	if config.HTTPSProxy == config.HTTPProxy && config.NoProxy == "" {
+		proxyURLIsSame = true
+	}
+	network := "tcp4"
+	if d.DialDualStack {
+		network = "tcp"
+	}
+	proxyFunc := config.ProxyFunc()
+	if proxyURLIsSame {
+		var proxyURL *url.URL
+		var proxyDialer proxy.Dialer
+		proxyURL, err = proxyFunc(tmpURL)
+		if err != nil {
+			return nil, err
+		}
+		if proxyURL == nil {
+			// dial directly
+			return func(addr string) (net.Conn, error) {
+				return d.Dial(network, addr)
+			}, nil
+		}
+		switch proxyURL.Scheme {
+		case "socks5", "socks5h":
+			proxyDialer, err = proxy.FromURL(proxyURL, d)
+			if err != nil {
+				return
+			}
+		case "http":
+			proxyAddr, auth := addrAndAuth(proxyURL)
+			proxyDialer = DialerFunc(func(network, addr string) (conn net.Conn, err error) {
+				return httpProxyDial(d, network, addr, proxyAddr, auth)
+			})
+		default:
+			return nil, errors.New("proxy: unknown scheme: " + proxyURL.Scheme)
+		}
+		return func(addr string) (net.Conn, error) {
+			return proxyDialer.Dial(network, addr)
+		}, nil
+	}
+	// slow path when the proxyURL changes along with the request URL.
+	return func(addr string) (conn net.Conn, err error) {
+		var proxyDialer proxy.Dialer
+		var proxyURL *url.URL
+		scheme := httpsScheme
+		if !strings.HasSuffix(addr, colonTLSPort) {
+			scheme = httpScheme
+		}
+		reqURL := &url.URL{Host: addr, Scheme: scheme}
+		proxyURL, err = proxyFunc(reqURL)
+		if err != nil {
+			return
+		}
+		if proxyURL == nil {
+			// dial directly
+			return d.Dial(network, addr)
+		}
+		switch proxyURL.Scheme {
+		case "socks5", "socks5h":
+			proxyDialer, err = proxy.FromURL(proxyURL, d)
+			if err != nil {
+				return
+			}
+		case "http":
+			proxyAddr, auth := addrAndAuth(proxyURL)
+			proxyDialer = DialerFunc(func(network, addr string) (conn net.Conn, err error) {
+				return httpProxyDial(d, network, addr, proxyAddr, auth)
+			})
+		default:
+			return nil, errors.New("proxy: unknown scheme: " + proxyURL.Scheme)
+		}
+		return proxyDialer.Dial(network, addr)
+	}, nil
+}
+
+// Dial is solely for implementing the proxy.Dialer interface.
+func (d *Dialer) Dial(network, addr string) (conn net.Conn, err error) {
+	if network == "tcp4" {
+		if d.Timeout > 0 {
+			return d.TCPDialer.DialTimeout(addr, d.Timeout)
+		}
+		return d.TCPDialer.Dial(addr)
+	}
+	if network == "tcp" {
+		if d.Timeout > 0 {
+			return d.TCPDialer.DialDualStackTimeout(addr, d.Timeout)
+		}
+		return d.TCPDialer.DialDualStack(addr)
+	}
+	err = errors.New("dont support the network: " + network)
+	return
+}
+
+func (d *Dialer) connectTimeout() time.Duration {
+	return d.ConnectTimeout
+}
+
+// In the httpProxyDial function, the proxy.Dialer that implements
+// this interface can retrieve timeout information when sending the CONNECT
+// method to the HTTP proxy.
+type httpProxyDialer interface {
+	connectTimeout() time.Duration
+}
+
+// DialerFunc Make a function of type func(network, addr string) (net.Conn, error)
+// implement the proxy.Dialer interface.
+type DialerFunc func(network, addr string) (net.Conn, error)
+
+func (d DialerFunc) Dial(network, addr string) (net.Conn, error) {
+	return d(network, addr)
+}
+
+// Establish a connection through an HTTP proxy.
+func httpProxyDial(dialer proxy.Dialer, network, addr, proxyAddr, auth string) (conn net.Conn, err error) {
+	conn, err = dialer.Dial(network, proxyAddr)
+	if err != nil {
+		return
+	}
+	var connectTimeout time.Duration
+	hp, ok := dialer.(httpProxyDialer)
+	if ok {
+		connectTimeout = hp.connectTimeout()
+	}
+
+	if connectTimeout > 0 {
+		if err = conn.SetDeadline(time.Now().Add(connectTimeout)); err != nil {
+			_ = conn.Close()
+			return nil, err
+		}
+		defer func() {
+			_ = conn.SetDeadline(time.Time{})
+		}()
+	}
+	req := "CONNECT " + addr + " HTTP/1.1\r\nHost: " + addr + "\r\n"
+	if auth != "" {
+		req += "Proxy-Authorization: Basic " + auth + "\r\n"
+	}
+	req += "\r\n"
+	_, err = conn.Write([]byte(req))
+	if err != nil {
+		_ = conn.Close()
+		return
+	}
+	res := fasthttp.AcquireResponse()
+	defer fasthttp.ReleaseResponse(res)
+	res.SkipBody = true
+	if err = res.Read(bufio.NewReaderSize(conn, 1024)); err != nil {
+		_ = conn.Close()
+		return
+	}
+	if res.Header.StatusCode() != 200 {
+		_ = conn.Close()
+		err = fmt.Errorf("could not connect to proxyAddr: %s status code: %d", proxyAddr, res.Header.StatusCode())
+		return
+	}
+	return
+}
+
+// Cache authentication information for HTTP proxies.
+type proxyInfo struct {
+	auth string
+	addr string
+}
+
+func addrAndAuth(pu *url.URL) (proxyAddr, auth string) {
+	if pu.User == nil {
+		proxyAddr = pu.Host + pu.Path
+		return
+	}
+	var info *proxyInfo
+	v, ok := authCache.Load(pu)
+	if ok {
+		info = v.(*proxyInfo)
+		return info.addr, info.auth
+	}
+	info = &proxyInfo{
+		auth: base64.StdEncoding.EncodeToString([]byte(pu.User.String())),
+		addr: pu.Host + pu.Path,
+	}
+	authCache.Store(pu, info)
+	return info.addr, info.auth
+}
diff --git a/fasthttpproxy/http.go b/fasthttpproxy/http.go
index a1a9e4c..80885d0 100644
--- a/fasthttpproxy/http.go
+++ b/fasthttpproxy/http.go
@@ -1,14 +1,10 @@
 package fasthttpproxy
 
 import (
-	"bufio"
-	"encoding/base64"
-	"fmt"
-	"net"
-	"strings"
 	"time"
 
 	"github.com/valyala/fasthttp"
+	"golang.org/x/net/http/httpproxy"
 )
 
 // FasthttpHTTPDialer returns a fasthttp.DialFunc that dials using
@@ -25,6 +21,7 @@ func FasthttpHTTPDialer(proxy string) fasthttp.DialFunc {
 
 // FasthttpHTTPDialerTimeout returns a fasthttp.DialFunc that dials using
 // the provided HTTP proxy using the given timeout.
+// The timeout parameter determines both the dial timeout and the CONNECT request timeout.
 //
 // Example usage:
 //
@@ -32,75 +29,7 @@ func FasthttpHTTPDialer(proxy string) fasthttp.DialFunc {
 //		Dial: fasthttpproxy.FasthttpHTTPDialerTimeout("username:password@localhost:9050", time.Second * 2),
 //	}
 func FasthttpHTTPDialerTimeout(proxy string, timeout time.Duration) fasthttp.DialFunc {
-	var auth string
-	if strings.Contains(proxy, "@") {
-		index := strings.LastIndex(proxy, "@")
-		auth = base64.StdEncoding.EncodeToString([]byte(proxy[:index]))
-		proxy = proxy[index+1:]
-	}
-
-	return func(addr string) (net.Conn, error) {
-		var conn net.Conn
-		var err error
-		start := time.Now()
-
-		if strings.HasPrefix(proxy, "[") {
-			// ipv6
-			if timeout == 0 {
-				conn, err = fasthttp.DialDualStack(proxy)
-			} else {
-				conn, err = fasthttp.DialDualStackTimeout(proxy, timeout)
-			}
-		} else {
-			// ipv4
-			if timeout == 0 {
-				conn, err = fasthttp.Dial(proxy)
-			} else {
-				conn, err = fasthttp.DialTimeout(proxy, timeout)
-			}
-		}
-
-		if err != nil {
-			return nil, err
-		}
-
-		if timeout > 0 {
-			if err = conn.SetDeadline(start.Add(timeout)); err != nil {
-				conn.Close()
-				return nil, err
-			}
-		}
-
-		req := "CONNECT " + addr + " HTTP/1.1\r\nHost: " + addr + "\r\n"
-		if auth != "" {
-			req += "Proxy-Authorization: Basic " + auth + "\r\n"
-		}
-		req += "\r\n"
-
-		if _, err := conn.Write([]byte(req)); err != nil {
-			conn.Close()
-			return nil, err
-		}
-
-		res := fasthttp.AcquireResponse()
-		defer fasthttp.ReleaseResponse(res)
-
-		res.SkipBody = true
-
-		if err := res.Read(bufio.NewReader(conn)); err != nil {
-			conn.Close()
-			return nil, err
-		}
-		if res.Header.StatusCode() != 200 {
-			conn.Close()
-			return nil, fmt.Errorf("could not connect to proxy: %s status code: %d", proxy, res.Header.StatusCode())
-		}
-		if timeout > 0 {
-			if err := conn.SetDeadline(time.Time{}); err != nil {
-				conn.Close()
-				return nil, err
-			}
-		}
-		return conn, nil
-	}
+	d := Dialer{Config: httpproxy.Config{HTTPProxy: proxy, HTTPSProxy: proxy}, Timeout: timeout, ConnectTimeout: timeout}
+	dialFunc, _ := d.GetDialFunc(false)
+	return dialFunc
 }
diff --git a/fasthttpproxy/proxy_env.go b/fasthttpproxy/proxy_env.go
index 1448593..342b093 100644
--- a/fasthttpproxy/proxy_env.go
+++ b/fasthttpproxy/proxy_env.go
@@ -1,22 +1,14 @@
 package fasthttpproxy
 
 import (
-	"bufio"
-	"encoding/base64"
-	"fmt"
-	"net"
-	"net/url"
-	"sync/atomic"
 	"time"
 
 	"github.com/valyala/fasthttp"
-	"golang.org/x/net/http/httpproxy"
 )
 
 const (
 	httpsScheme = "https"
 	httpScheme  = "http"
-	tlsPort     = "443"
 )
 
 // FasthttpProxyHTTPDialer returns a fasthttp.DialFunc that dials using
@@ -33,6 +25,7 @@ func FasthttpProxyHTTPDialer() fasthttp.DialFunc {
 
 // FasthttpProxyHTTPDialerTimeout returns a fasthttp.DialFunc that dials using
 // the env(HTTP_PROXY, HTTPS_PROXY and NO_PROXY) configured HTTP proxy using the given timeout.
+// The timeout parameter determines both the dial timeout and the CONNECT request timeout.
 //
 // Example usage:
 //
@@ -40,106 +33,7 @@ func FasthttpProxyHTTPDialer() fasthttp.DialFunc {
 //		Dial: fasthttpproxy.FasthttpProxyHTTPDialerTimeout(time.Second * 2),
 //	}
 func FasthttpProxyHTTPDialerTimeout(timeout time.Duration) fasthttp.DialFunc {
-	proxier := httpproxy.FromEnvironment().ProxyFunc()
-
-	// encoded auth barrier for http and https proxy.
-	authHTTPStorage := &atomic.Value{}
-	authHTTPSStorage := &atomic.Value{}
-
-	return func(addr string) (net.Conn, error) {
-		start := time.Now()
-		port, _, err := net.SplitHostPort(addr)
-		if err != nil {
-			return nil, fmt.Errorf("unexpected addr format: %w", err)
-		}
-
-		reqURL := &url.URL{Host: addr, Scheme: httpScheme}
-		if port == tlsPort {
-			reqURL.Scheme = httpsScheme
-		}
-		proxyURL, err := proxier(reqURL)
-		if err != nil {
-			return nil, err
-		}
-
-		if proxyURL == nil {
-			if timeout == 0 {
-				return fasthttp.Dial(addr)
-			}
-			return fasthttp.DialTimeout(addr, timeout)
-		}
-
-		var conn net.Conn
-		if timeout == 0 {
-			conn, err = fasthttp.Dial(proxyURL.Host)
-		} else {
-			conn, err = fasthttp.DialTimeout(proxyURL.Host, timeout)
-		}
-		if err != nil {
-			return nil, err
-		}
-
-		if timeout > 0 {
-			if err := conn.SetDeadline(start.Add(timeout)); err != nil {
-				if connErr := conn.Close(); connErr != nil {
-					return nil, fmt.Errorf("conn close err %v precede by set conn deadline %w", connErr, err)
-				}
-			}
-		}
-
-		req := "CONNECT " + addr + " HTTP/1.1\r\n"
-
-		if proxyURL.User != nil {
-			authBarrierStorage := authHTTPStorage
-			if port == tlsPort {
-				authBarrierStorage = authHTTPSStorage
-			}
-
-			auth := authBarrierStorage.Load()
-			if auth == nil {
-				authBarrier := base64.StdEncoding.EncodeToString([]byte(proxyURL.User.String()))
-				auth = &authBarrier
-				authBarrierStorage.Store(auth)
-			}
-
-			req += "Proxy-Authorization: Basic " + *auth.(*string) + "\r\n"
-		}
-		req += "\r\n"
-
-		if _, err := conn.Write([]byte(req)); err != nil {
-			if connErr := conn.Close(); connErr != nil {
-				return nil, fmt.Errorf("conn close err %v precede by write conn err %w", connErr, err)
-			}
-			return nil, err
-		}
-
-		res := fasthttp.AcquireResponse()
-		defer fasthttp.ReleaseResponse(res)
-
-		res.SkipBody = true
-
-		if err := res.Read(bufio.NewReader(conn)); err != nil {
-			if connErr := conn.Close(); connErr != nil {
-				return nil, fmt.Errorf("conn close err %v precede by read conn err %w", connErr, err)
-			}
-			return nil, err
-		}
-		if res.Header.StatusCode() != 200 {
-			if connErr := conn.Close(); connErr != nil {
-				return nil, fmt.Errorf(
-					"conn close err %w precede by connect to proxy: code: %d body %q",
-					connErr, res.StatusCode(), string(res.Body()))
-			}
-			return nil, fmt.Errorf("could not connect to proxy: code: %d body %q", res.StatusCode(), string(res.Body()))
-		}
-		if timeout > 0 {
-			if err := conn.SetDeadline(time.Time{}); err != nil {
-				if connErr := conn.Close(); connErr != nil {
-					return nil, fmt.Errorf("conn close err %v precede by clear conn deadline err %w", connErr, err)
-				}
-				return nil, err
-			}
-		}
-		return conn, nil
-	}
+	d := Dialer{Timeout: timeout, ConnectTimeout: timeout}
+	dialFunc, _ := d.GetDialFunc(true)
+	return dialFunc
 }
diff --git a/fasthttpproxy/socks5.go b/fasthttpproxy/socks5.go
index a01c204..0328cd2 100644
--- a/fasthttpproxy/socks5.go
+++ b/fasthttpproxy/socks5.go
@@ -1,11 +1,8 @@
 package fasthttpproxy
 
 import (
-	"net"
-	"net/url"
-
 	"github.com/valyala/fasthttp"
-	"golang.org/x/net/proxy"
+	"golang.org/x/net/http/httpproxy"
 )
 
 // FasthttpSocksDialer returns a fasthttp.DialFunc that dials using
@@ -17,23 +14,7 @@ import (
 //		Dial: fasthttpproxy.FasthttpSocksDialer("socks5://localhost:9050"),
 //	}
 func FasthttpSocksDialer(proxyAddr string) fasthttp.DialFunc {
-	var (
-		u      *url.URL
-		err    error
-		dialer proxy.Dialer
-	)
-	if u, err = url.Parse(proxyAddr); err == nil {
-		dialer, err = proxy.FromURL(u, proxy.Direct)
-	}
-	// It would be nice if we could return the error here. But we can't
-	// change our API so just keep returning it in the returned Dial function.
-	// Besides the implementation of proxy.SOCKS5() at the time of writing this
-	// will always return nil as error.
-
-	return func(addr string) (net.Conn, error) {
-		if err != nil {
-			return nil, err
-		}
-		return dialer.Dial("tcp", addr)
-	}
+	d := Dialer{Config: httpproxy.Config{HTTPProxy: proxyAddr, HTTPSProxy: proxyAddr}}
+	dialFunc, _ := d.GetDialFunc(false)
+	return dialFunc
 }
diff --git a/fs.go b/fs.go
index f61e98c..e455560 100644
--- a/fs.go
+++ b/fs.go
@@ -819,6 +819,7 @@ func newCacheManager(fs *FS) cacheManager {
 		cache:         make(map[string]*fsFile),
 		cacheBrotli:   make(map[string]*fsFile),
 		cacheGzip:     make(map[string]*fsFile),
+		cacheZstd:     make(map[string]*fsFile),
 	}
 
 	go instance.handleCleanCache(fs.CleanStop)
@@ -850,6 +851,7 @@ type inMemoryCacheManager struct {
 	cache         map[string]*fsFile
 	cacheBrotli   map[string]*fsFile
 	cacheGzip     map[string]*fsFile
+	cacheZstd     map[string]*fsFile
 	cacheDuration time.Duration
 	cacheLock     sync.Mutex
 }
@@ -869,6 +871,8 @@ func (cm *inMemoryCacheManager) getFsCache(cacheKind CacheKind) map[string]*fsFi
 		fileCache = cm.cacheBrotli
 	case gzipCacheKind:
 		fileCache = cm.cacheGzip
+	case zstdCacheKind:
+		fileCache = cm.cacheZstd
 	}
 
 	return fileCache
@@ -959,6 +963,7 @@ func (cm *inMemoryCacheManager) cleanCache(pendingFiles []*fsFile) []*fsFile {
 	pendingFiles, filesToRelease = cleanCacheNolock(cm.cache, pendingFiles, filesToRelease, cm.cacheDuration)
 	pendingFiles, filesToRelease = cleanCacheNolock(cm.cacheBrotli, pendingFiles, filesToRelease, cm.cacheDuration)
 	pendingFiles, filesToRelease = cleanCacheNolock(cm.cacheGzip, pendingFiles, filesToRelease, cm.cacheDuration)
+	pendingFiles, filesToRelease = cleanCacheNolock(cm.cacheZstd, pendingFiles, filesToRelease, cm.cacheDuration)
 
 	cm.cacheLock.Unlock()
 
diff --git a/header.go b/header.go
index b0eb804..b6d8ffe 100644
--- a/header.go
+++ b/header.go
@@ -39,7 +39,8 @@ type ResponseHeader struct {
 	trailer []argsKV
 
 	cookies []argsKV
-	bufKV   argsKV
+	bufK    []byte
+	bufV    []byte
 
 	statusCode    int
 	contentLength int
@@ -80,7 +81,8 @@ type RequestHeader struct {
 	// stores an immutable copy of headers as they were received from the
 	// wire.
 	rawHeaders []byte
-	bufKV      argsKV
+	bufK       []byte
+	bufV       []byte
 
 	contentLength int
 
@@ -100,7 +102,7 @@ type RequestHeader struct {
 // SetContentRange sets 'Content-Range: bytes startPos-endPos/contentLength'
 // header.
 func (h *ResponseHeader) SetContentRange(startPos, endPos, contentLength int) {
-	b := h.bufKV.value[:0]
+	b := h.bufV[:0]
 	b = append(b, strBytes...)
 	b = append(b, ' ')
 	b = AppendUint(b, startPos)
@@ -108,9 +110,9 @@ func (h *ResponseHeader) SetContentRange(startPos, endPos, contentLength int) {
 	b = AppendUint(b, endPos)
 	b = append(b, '/')
 	b = AppendUint(b, contentLength)
-	h.bufKV.value = b
+	h.bufV = b
 
-	h.setNonSpecial(strContentRange, h.bufKV.value)
+	h.setNonSpecial(strContentRange, h.bufV)
 }
 
 // SetByteRange sets 'Range: bytes=startPos-endPos' header.
@@ -118,7 +120,7 @@ func (h *ResponseHeader) SetContentRange(startPos, endPos, contentLength int) {
 //   - If startPos is negative, then 'bytes=-startPos' value is set.
 //   - If endPos is negative, then 'bytes=startPos-' value is set.
 func (h *RequestHeader) SetByteRange(startPos, endPos int) {
-	b := h.bufKV.value[:0]
+	b := h.bufV[:0]
 	b = append(b, strBytes...)
 	b = append(b, '=')
 	if startPos >= 0 {
@@ -130,9 +132,9 @@ func (h *RequestHeader) SetByteRange(startPos, endPos int) {
 	if endPos >= 0 {
 		b = AppendUint(b, endPos)
 	}
-	h.bufKV.value = b
+	h.bufV = b
 
-	h.setNonSpecial(strRange, h.bufKV.value)
+	h.setNonSpecial(strRange, h.bufV)
 }
 
 // StatusCode returns response status code.
@@ -173,8 +175,8 @@ func (h *ResponseHeader) SetProtocol(protocol []byte) {
 
 // SetLastModified sets 'Last-Modified' header to the given value.
 func (h *ResponseHeader) SetLastModified(t time.Time) {
-	h.bufKV.value = AppendHTTPDate(h.bufKV.value[:0], t)
-	h.setNonSpecial(strLastModified, h.bufKV.value)
+	h.bufV = AppendHTTPDate(h.bufV[:0], t)
+	h.setNonSpecial(strLastModified, h.bufV)
 }
 
 // ConnectionClose returns true if 'Connection: close' header is set.
@@ -411,30 +413,30 @@ func (h *RequestHeader) SetContentEncodingBytes(contentEncoding []byte) {
 // 'multipart/form-data; boundary=...'
 // where ... is substituted by the given boundary.
 func (h *RequestHeader) SetMultipartFormBoundary(boundary string) {
-	b := h.bufKV.value[:0]
+	b := h.bufV[:0]
 	b = append(b, strMultipartFormData...)
 	b = append(b, ';', ' ')
 	b = append(b, strBoundary...)
 	b = append(b, '=')
 	b = append(b, boundary...)
-	h.bufKV.value = b
+	h.bufV = b
 
-	h.SetContentTypeBytes(h.bufKV.value)
+	h.SetContentTypeBytes(h.bufV)
 }
 
 // SetMultipartFormBoundaryBytes sets the following Content-Type:
 // 'multipart/form-data; boundary=...'
 // where ... is substituted by the given boundary.
 func (h *RequestHeader) SetMultipartFormBoundaryBytes(boundary []byte) {
-	b := h.bufKV.value[:0]
+	b := h.bufV[:0]
 	b = append(b, strMultipartFormData...)
 	b = append(b, ';', ' ')
 	b = append(b, strBoundary...)
 	b = append(b, '=')
 	b = append(b, boundary...)
-	h.bufKV.value = b
+	h.bufV = b
 
-	h.SetContentTypeBytes(h.bufKV.value)
+	h.SetContentTypeBytes(h.bufV)
 }
 
 // SetTrailer sets header Trailer value for chunked response
@@ -540,9 +542,9 @@ func (h *ResponseHeader) AddTrailerBytes(trailer []byte) error {
 			err = ErrBadTrailer
 			continue
 		}
-		h.bufKV.key = append(h.bufKV.key[:0], key...)
-		normalizeHeaderKey(h.bufKV.key, h.disableNormalizing)
-		h.trailer = appendArgBytes(h.trailer, h.bufKV.key, nil, argsNoValue)
+		h.bufK = append(h.bufK[:0], key...)
+		normalizeHeaderKey(h.bufK, h.disableNormalizing)
+		h.trailer = appendArgBytes(h.trailer, h.bufK, nil, argsNoValue)
 	}
 
 	return err
@@ -888,9 +890,9 @@ func (h *RequestHeader) AddTrailerBytes(trailer []byte) error {
 			err = ErrBadTrailer
 			continue
 		}
-		h.bufKV.key = append(h.bufKV.key[:0], key...)
-		normalizeHeaderKey(h.bufKV.key, h.disableNormalizing)
-		h.trailer = appendArgBytes(h.trailer, h.bufKV.key, nil, argsNoValue)
+		h.bufK = append(h.bufK[:0], key...)
+		normalizeHeaderKey(h.bufK, h.disableNormalizing)
+		h.trailer = appendArgBytes(h.trailer, h.bufK, nil, argsNoValue)
 	}
 
 	return err
@@ -954,8 +956,8 @@ func (h *ResponseHeader) IsHTTP11() bool {
 // HasAcceptEncoding returns true if the header contains
 // the given Accept-Encoding value.
 func (h *RequestHeader) HasAcceptEncoding(acceptEncoding string) bool {
-	h.bufKV.value = append(h.bufKV.value[:0], acceptEncoding...)
-	return h.HasAcceptEncodingBytes(h.bufKV.value)
+	h.bufV = append(h.bufV[:0], acceptEncoding...)
+	return h.HasAcceptEncodingBytes(h.bufV)
 }
 
 // HasAcceptEncodingBytes returns true if the header contains
@@ -1284,8 +1286,8 @@ func (h *RequestHeader) VisitAll(f func(key, value []byte)) {
 
 	h.collectCookies()
 	if len(h.cookies) > 0 {
-		h.bufKV.value = appendRequestCookieBytes(h.bufKV.value[:0], h.cookies)
-		f(strCookie, h.bufKV.value)
+		h.bufV = appendRequestCookieBytes(h.bufV[:0], h.cookies)
+		f(strCookie, h.bufV)
 	}
 	visitArgs(h.h, f)
 	if h.ConnectionClose() {
@@ -1313,15 +1315,15 @@ func (h *RequestHeader) VisitAllInOrder(f func(key, value []byte)) {
 
 // Del deletes header with the given key.
 func (h *ResponseHeader) Del(key string) {
-	k := getHeaderKeyBytes(&h.bufKV, key, h.disableNormalizing)
-	h.del(k)
+	h.bufK = getHeaderKeyBytes(h.bufK, key, h.disableNormalizing)
+	h.del(h.bufK)
 }
 
 // DelBytes deletes header with the given key.
 func (h *ResponseHeader) DelBytes(key []byte) {
-	h.bufKV.key = append(h.bufKV.key[:0], key...)
-	normalizeHeaderKey(h.bufKV.key, h.disableNormalizing)
-	h.del(h.bufKV.key)
+	h.bufK = append(h.bufK[:0], key...)
+	normalizeHeaderKey(h.bufK, h.disableNormalizing)
+	h.del(h.bufK)
 }
 
 func (h *ResponseHeader) del(key []byte) {
@@ -1347,15 +1349,15 @@ func (h *ResponseHeader) del(key []byte) {
 
 // Del deletes header with the given key.
 func (h *RequestHeader) Del(key string) {
-	k := getHeaderKeyBytes(&h.bufKV, key, h.disableNormalizing)
-	h.del(k)
+	h.bufK = getHeaderKeyBytes(h.bufK, key, h.disableNormalizing)
+	h.del(h.bufK)
 }
 
 // DelBytes deletes header with the given key.
 func (h *RequestHeader) DelBytes(key []byte) {
-	h.bufKV.key = append(h.bufKV.key[:0], key...)
-	normalizeHeaderKey(h.bufKV.key, h.disableNormalizing)
-	h.del(h.bufKV.key)
+	h.bufK = append(h.bufK[:0], key...)
+	normalizeHeaderKey(h.bufK, h.disableNormalizing)
+	h.del(h.bufK)
 }
 
 func (h *RequestHeader) del(key []byte) {
@@ -1563,8 +1565,8 @@ func (h *ResponseHeader) AddBytesKV(key, value []byte) {
 		return
 	}
 
-	k := getHeaderKeyBytes(&h.bufKV, b2s(key), h.disableNormalizing)
-	h.h = appendArgBytes(h.h, k, value, argsHasValue)
+	h.bufK = getHeaderKeyBytes(h.bufK, b2s(key), h.disableNormalizing)
+	h.h = appendArgBytes(h.h, h.bufK, value, argsHasValue)
 }
 
 // Set sets the given 'key: value' header.
@@ -1574,8 +1576,8 @@ func (h *ResponseHeader) AddBytesKV(key, value []byte) {
 //
 // Use Add for setting multiple header values under the same key.
 func (h *ResponseHeader) Set(key, value string) {
-	initHeaderKV(&h.bufKV, key, value, h.disableNormalizing)
-	h.SetCanonical(h.bufKV.key, h.bufKV.value)
+	h.bufK, h.bufV = initHeaderKV(h.bufK, h.bufV, key, value, h.disableNormalizing)
+	h.SetCanonical(h.bufK, h.bufV)
 }
 
 // SetBytesK sets the given 'key: value' header.
@@ -1585,8 +1587,8 @@ func (h *ResponseHeader) Set(key, value string) {
 //
 // Use AddBytesK for setting multiple header values under the same key.
 func (h *ResponseHeader) SetBytesK(key []byte, value string) {
-	h.bufKV.value = append(h.bufKV.value[:0], value...)
-	h.SetBytesKV(key, h.bufKV.value)
+	h.bufV = append(h.bufV[:0], value...)
+	h.SetBytesKV(key, h.bufV)
 }
 
 // SetBytesV sets the given 'key: value' header.
@@ -1596,8 +1598,8 @@ func (h *ResponseHeader) SetBytesK(key []byte, value string) {
 //
 // Use AddBytesV for setting multiple header values under the same key.
 func (h *ResponseHeader) SetBytesV(key string, value []byte) {
-	k := getHeaderKeyBytes(&h.bufKV, key, h.disableNormalizing)
-	h.SetCanonical(k, value)
+	h.bufK = getHeaderKeyBytes(h.bufK, key, h.disableNormalizing)
+	h.SetCanonical(h.bufK, value)
 }
 
 // SetBytesKV sets the given 'key: value' header.
@@ -1607,9 +1609,9 @@ func (h *ResponseHeader) SetBytesV(key string, value []byte) {
 //
 // Use AddBytesKV for setting multiple header values under the same key.
 func (h *ResponseHeader) SetBytesKV(key, value []byte) {
-	h.bufKV.key = append(h.bufKV.key[:0], key...)
-	normalizeHeaderKey(h.bufKV.key, h.disableNormalizing)
-	h.SetCanonical(h.bufKV.key, value)
+	h.bufK = append(h.bufK[:0], key...)
+	normalizeHeaderKey(h.bufK, h.disableNormalizing)
+	h.SetCanonical(h.bufK, value)
 }
 
 // SetCanonical sets the given 'key: value' header assuming that
@@ -1774,8 +1776,8 @@ func (h *RequestHeader) AddBytesKV(key, value []byte) {
 		return
 	}
 
-	k := getHeaderKeyBytes(&h.bufKV, b2s(key), h.disableNormalizing)
-	h.h = appendArgBytes(h.h, k, value, argsHasValue)
+	h.bufK = getHeaderKeyBytes(h.bufK, b2s(key), h.disableNormalizing)
+	h.h = appendArgBytes(h.h, h.bufK, value, argsHasValue)
 }
 
 // Set sets the given 'key: value' header.
@@ -1785,8 +1787,8 @@ func (h *RequestHeader) AddBytesKV(key, value []byte) {
 //
 // Use Add for setting multiple header values under the same key.
 func (h *RequestHeader) Set(key, value string) {
-	initHeaderKV(&h.bufKV, key, value, h.disableNormalizing)
-	h.SetCanonical(h.bufKV.key, h.bufKV.value)
+	h.bufK, h.bufV = initHeaderKV(h.bufK, h.bufV, key, value, h.disableNormalizing)
+	h.SetCanonical(h.bufK, h.bufV)
 }
 
 // SetBytesK sets the given 'key: value' header.
@@ -1796,8 +1798,8 @@ func (h *RequestHeader) Set(key, value string) {
 //
 // Use AddBytesK for setting multiple header values under the same key.
 func (h *RequestHeader) SetBytesK(key []byte, value string) {
-	h.bufKV.value = append(h.bufKV.value[:0], value...)
-	h.SetBytesKV(key, h.bufKV.value)
+	h.bufV = append(h.bufV[:0], value...)
+	h.SetBytesKV(key, h.bufV)
 }
 
 // SetBytesV sets the given 'key: value' header.
@@ -1807,8 +1809,8 @@ func (h *RequestHeader) SetBytesK(key []byte, value string) {
 //
 // Use AddBytesV for setting multiple header values under the same key.
 func (h *RequestHeader) SetBytesV(key string, value []byte) {
-	k := getHeaderKeyBytes(&h.bufKV, key, h.disableNormalizing)
-	h.SetCanonical(k, value)
+	h.bufK = getHeaderKeyBytes(h.bufK, key, h.disableNormalizing)
+	h.SetCanonical(h.bufK, value)
 }
 
 // SetBytesKV sets the given 'key: value' header.
@@ -1818,9 +1820,9 @@ func (h *RequestHeader) SetBytesV(key string, value []byte) {
 //
 // Use AddBytesKV for setting multiple header values under the same key.
 func (h *RequestHeader) SetBytesKV(key, value []byte) {
-	h.bufKV.key = append(h.bufKV.key[:0], key...)
-	normalizeHeaderKey(h.bufKV.key, h.disableNormalizing)
-	h.SetCanonical(h.bufKV.key, value)
+	h.bufK = append(h.bufK[:0], key...)
+	normalizeHeaderKey(h.bufK, h.disableNormalizing)
+	h.SetCanonical(h.bufK, value)
 }
 
 // SetCanonical sets the given 'key: value' header assuming that
@@ -1841,8 +1843,8 @@ func (h *RequestHeader) SetCanonical(key, value []byte) {
 // either though ReleaseResponse or your request handler returning.
 // Do not store references to the returned value. Make copies instead.
 func (h *ResponseHeader) Peek(key string) []byte {
-	k := getHeaderKeyBytes(&h.bufKV, key, h.disableNormalizing)
-	return h.peek(k)
+	h.bufK = getHeaderKeyBytes(h.bufK, key, h.disableNormalizing)
+	return h.peek(h.bufK)
 }
 
 // PeekBytes returns header value for the given key.
@@ -1851,9 +1853,9 @@ func (h *ResponseHeader) Peek(key string) []byte {
 // either though ReleaseResponse or your request handler returning.
 // Do not store references to returned value. Make copies instead.
 func (h *ResponseHeader) PeekBytes(key []byte) []byte {
-	h.bufKV.key = append(h.bufKV.key[:0], key...)
-	normalizeHeaderKey(h.bufKV.key, h.disableNormalizing)
-	return h.peek(h.bufKV.key)
+	h.bufK = append(h.bufK[:0], key...)
+	normalizeHeaderKey(h.bufK, h.disableNormalizing)
+	return h.peek(h.bufK)
 }
 
 // Peek returns header value for the given key.
@@ -1862,8 +1864,8 @@ func (h *ResponseHeader) PeekBytes(key []byte) []byte {
 // either though ReleaseRequest or your request handler returning.
 // Do not store references to returned value. Make copies instead.
 func (h *RequestHeader) Peek(key string) []byte {
-	k := getHeaderKeyBytes(&h.bufKV, key, h.disableNormalizing)
-	return h.peek(k)
+	h.bufK = getHeaderKeyBytes(h.bufK, key, h.disableNormalizing)
+	return h.peek(h.bufK)
 }
 
 // PeekBytes returns header value for the given key.
@@ -1872,9 +1874,9 @@ func (h *RequestHeader) Peek(key string) []byte {
 // either though ReleaseRequest or your request handler returning.
 // Do not store references to returned value. Make copies instead.
 func (h *RequestHeader) PeekBytes(key []byte) []byte {
-	h.bufKV.key = append(h.bufKV.key[:0], key...)
-	normalizeHeaderKey(h.bufKV.key, h.disableNormalizing)
-	return h.peek(h.bufKV.key)
+	h.bufK = append(h.bufK[:0], key...)
+	normalizeHeaderKey(h.bufK, h.disableNormalizing)
+	return h.peek(h.bufK)
 }
 
 func (h *ResponseHeader) peek(key []byte) []byte {
@@ -1935,8 +1937,8 @@ func (h *RequestHeader) peek(key []byte) []byte {
 // Any future calls to the Peek* will modify the returned value.
 // Do not store references to returned value. Make copies instead.
 func (h *RequestHeader) PeekAll(key string) [][]byte {
-	k := getHeaderKeyBytes(&h.bufKV, key, h.disableNormalizing)
-	return h.peekAll(k)
+	h.bufK = getHeaderKeyBytes(h.bufK, key, h.disableNormalizing)
+	return h.peekAll(h.bufK)
 }
 
 func (h *RequestHeader) peekAll(key []byte) [][]byte {
@@ -1983,8 +1985,8 @@ func (h *RequestHeader) peekAll(key []byte) [][]byte {
 // Any future calls to the Peek* will modify the returned value.
 // Do not store references to returned value. Make copies instead.
 func (h *ResponseHeader) PeekAll(key string) [][]byte {
-	k := getHeaderKeyBytes(&h.bufKV, key, h.disableNormalizing)
-	return h.peekAll(k)
+	h.bufK = getHeaderKeyBytes(h.bufK, key, h.disableNormalizing)
+	return h.peekAll(h.bufK)
 }
 
 func (h *ResponseHeader) peekAll(key []byte) [][]byte {
@@ -2412,8 +2414,8 @@ func (h *ResponseHeader) WriteTo(w io.Writer) (int64, error) {
 // either though ReleaseRequest or your request handler returning.
 // Do not store references to returned value. Make copies instead.
 func (h *ResponseHeader) Header() []byte {
-	h.bufKV.value = h.AppendBytes(h.bufKV.value[:0])
-	return h.bufKV.value
+	h.bufV = h.AppendBytes(h.bufV[:0])
+	return h.bufV
 }
 
 // writeTrailer writes response trailer to w.
@@ -2430,13 +2432,13 @@ func (h *ResponseHeader) writeTrailer(w *bufio.Writer) error {
 // either though ReleaseRequest or your request handler returning.
 // Do not store references to returned value. Make copies instead.
 func (h *ResponseHeader) TrailerHeader() []byte {
-	h.bufKV.value = h.bufKV.value[:0]
+	h.bufV = h.bufV[:0]
 	for _, t := range h.trailer {
 		value := h.peek(t.key)
-		h.bufKV.value = appendHeaderLine(h.bufKV.value, t.key, value)
+		h.bufV = appendHeaderLine(h.bufV, t.key, value)
 	}
-	h.bufKV.value = append(h.bufKV.value, strCRLF...)
-	return h.bufKV.value
+	h.bufV = append(h.bufV, strCRLF...)
+	return h.bufV
 }
 
 // String returns response header representation.
@@ -2544,8 +2546,8 @@ func (h *RequestHeader) WriteTo(w io.Writer) (int64, error) {
 // either though ReleaseRequest or your request handler returning.
 // Do not store references to returned value. Make copies instead.
 func (h *RequestHeader) Header() []byte {
-	h.bufKV.value = h.AppendBytes(h.bufKV.value[:0])
-	return h.bufKV.value
+	h.bufV = h.AppendBytes(h.bufV[:0])
+	return h.bufV
 }
 
 // writeTrailer writes request trailer to w.
@@ -2562,13 +2564,13 @@ func (h *RequestHeader) writeTrailer(w *bufio.Writer) error {
 // either though ReleaseRequest or your request handler returning.
 // Do not store references to returned value. Make copies instead.
 func (h *RequestHeader) TrailerHeader() []byte {
-	h.bufKV.value = h.bufKV.value[:0]
+	h.bufV = h.bufV[:0]
 	for _, t := range h.trailer {
 		value := h.peek(t.key)
-		h.bufKV.value = appendHeaderLine(h.bufKV.value, t.key, value)
+		h.bufV = appendHeaderLine(h.bufV, t.key, value)
 	}
-	h.bufKV.value = append(h.bufKV.value, strCRLF...)
-	return h.bufKV.value
+	h.bufV = append(h.bufV, strCRLF...)
+	return h.bufV
 }
 
 // RawHeaders returns raw header key/value bytes.
@@ -3419,17 +3421,18 @@ func nextLine(b []byte) ([]byte, []byte, error) {
 	return b[:n], b[nNext+1:], nil
 }
 
-func initHeaderKV(kv *argsKV, key, value string, disableNormalizing bool) {
-	kv.key = getHeaderKeyBytes(kv, key, disableNormalizing)
+func initHeaderKV(bufK, bufV []byte, key, value string, disableNormalizing bool) ([]byte, []byte) {
+	bufK = getHeaderKeyBytes(bufK, key, disableNormalizing)
 	// https://tools.ietf.org/html/rfc7230#section-3.2.4
-	kv.value = append(kv.value[:0], value...)
-	kv.value = removeNewLines(kv.value)
+	bufV = append(bufV[:0], value...)
+	bufV = removeNewLines(bufV)
+	return bufK, bufV
 }
 
-func getHeaderKeyBytes(kv *argsKV, key string, disableNormalizing bool) []byte {
-	kv.key = append(kv.key[:0], key...)
-	normalizeHeaderKey(kv.key, disableNormalizing)
-	return kv.key
+func getHeaderKeyBytes(bufK []byte, key string, disableNormalizing bool) []byte {
+	bufK = append(bufK[:0], key...)
+	normalizeHeaderKey(bufK, disableNormalizing)
+	return bufK
 }
 
 func normalizeHeaderValue(ov, ob []byte, headerLength int) (nv, nb []byte, nhl int) {
diff --git a/http.go b/http.go
index ef6eafc..0aa5bff 100644
--- a/http.go
+++ b/http.go
@@ -1446,7 +1446,8 @@ func (resp *Response) ReadLimitBody(r *bufio.Reader, maxBodySize int) error {
 		}
 	}
 
-	if resp.Header.ContentLength() == -1 && !resp.StreamBody {
+	// A response without a body can't have trailers.
+	if resp.Header.ContentLength() == -1 && !resp.StreamBody && !resp.mustSkipBody() {
 		err = resp.Header.ReadTrailer(r)
 		if err != nil && err != io.EOF {
 			if isConnectionReset(err) {
@@ -1600,10 +1601,10 @@ func (req *Request) Write(w *bufio.Writer) error {
 			nl := len(uri.username) + len(uri.password) + 1
 			nb := nl + len(strBasicSpace)
 			tl := nb + base64.StdEncoding.EncodedLen(nl)
-			if tl > cap(req.Header.bufKV.value) {
-				req.Header.bufKV.value = make([]byte, 0, tl)
+			if tl > cap(req.Header.bufV) {
+				req.Header.bufV = make([]byte, 0, tl)
 			}
-			buf := req.Header.bufKV.value[:0]
+			buf := req.Header.bufV[:0]
 			buf = append(buf, uri.username...)
 			buf = append(buf, strColon...)
 			buf = append(buf, uri.password...)
diff --git a/server.go b/server.go
index 3e9b087..7f48531 100644
--- a/server.go
+++ b/server.go
@@ -1597,14 +1597,14 @@ func (s *Server) NextProto(key string, nph ServeHandler) {
 func (s *Server) getNextProto(c net.Conn) (proto string, err error) {
 	if tlsConn, ok := c.(connTLSer); ok {
 		if s.ReadTimeout > 0 {
-			if err := c.SetReadDeadline(time.Now().Add(s.ReadTimeout)); err != nil {
-				panic(fmt.Sprintf("BUG: error in SetReadDeadline(%v): %v", s.ReadTimeout, err))
+			if err = c.SetReadDeadline(time.Now().Add(s.ReadTimeout)); err != nil {
+				return
 			}
 		}
 
 		if s.WriteTimeout > 0 {
-			if err := c.SetWriteDeadline(time.Now().Add(s.WriteTimeout)); err != nil {
-				panic(fmt.Sprintf("BUG: error in SetWriteDeadline(%v): %v", s.WriteTimeout, err))
+			if err = c.SetWriteDeadline(time.Now().Add(s.WriteTimeout)); err != nil {
+				return
 			}
 		}
 
@@ -2137,8 +2137,8 @@ func (s *Server) serveConn(c net.Conn) (err error) {
 		// Remove read or write deadlines that might have previously been set.
 		// The next handler is responsible for setting its own deadlines.
 		if s.ReadTimeout > 0 || s.WriteTimeout > 0 {
-			if err := c.SetDeadline(zeroTime); err != nil {
-				panic(fmt.Sprintf("BUG: error in SetDeadline(zeroTime): %v", err))
+			if err = c.SetDeadline(zeroTime); err != nil {
+				return
 			}
 		}
 
@@ -2177,7 +2177,7 @@ func (s *Server) serveConn(c net.Conn) (err error) {
 		// If this is a keep-alive connection set the idle timeout.
 		if connRequestNum > 1 {
 			if d := s.idleTimeout(); d > 0 {
-				if err := c.SetReadDeadline(time.Now().Add(d)); err != nil {
+				if err = c.SetReadDeadline(time.Now().Add(d)); err != nil {
 					break
 				}
 			}
@@ -2221,13 +2221,13 @@ func (s *Server) serveConn(c net.Conn) (err error) {
 			s.setState(c, StateActive)
 
 			if s.ReadTimeout > 0 {
-				if err := c.SetReadDeadline(time.Now().Add(s.ReadTimeout)); err != nil {
+				if err = c.SetReadDeadline(time.Now().Add(s.ReadTimeout)); err != nil {
 					break
 				}
 			} else if s.IdleTimeout > 0 && connRequestNum > 1 {
 				// If this was an idle connection and the server has an IdleTimeout but
 				// no ReadTimeout then we should remove the ReadTimeout.
-				if err := c.SetReadDeadline(zeroTime); err != nil {
+				if err = c.SetReadDeadline(zeroTime); err != nil {
 					break
 				}
 			}
@@ -2261,8 +2261,8 @@ func (s *Server) serveConn(c net.Conn) (err error) {
 					reqConf := onHdrRecv(&ctx.Request.Header)
 					if reqConf.ReadTimeout > 0 {
 						deadline := time.Now().Add(reqConf.ReadTimeout)
-						if err := c.SetReadDeadline(deadline); err != nil {
-							panic(fmt.Sprintf("BUG: error in SetReadDeadline(%v): %v", deadline, err))
+						if err = c.SetReadDeadline(deadline); err != nil {
+							break
 						}
 					}
 					switch {
@@ -2401,14 +2401,14 @@ func (s *Server) serveConn(c net.Conn) (err error) {
 		ctx.hijackNoResponse = false
 
 		if writeTimeout > 0 {
-			if err := c.SetWriteDeadline(time.Now().Add(writeTimeout)); err != nil {
-				panic(fmt.Sprintf("BUG: error in SetWriteDeadline(%v): %v", writeTimeout, err))
+			if err = c.SetWriteDeadline(time.Now().Add(writeTimeout)); err != nil {
+				break
 			}
 			previousWriteTimeout = writeTimeout
 		} else if previousWriteTimeout > 0 {
 			// We don't want a write timeout but we previously set one, remove it.
-			if err := c.SetWriteDeadline(zeroTime); err != nil {
-				panic(fmt.Sprintf("BUG: error in SetWriteDeadline(zeroTime): %v", err))
+			if err = c.SetWriteDeadline(zeroTime); err != nil {
+				break
 			}
 			previousWriteTimeout = 0
 		}
diff --git a/workerpool.go b/workerpool.go
index 9ecd948..bc3a3c5 100644
--- a/workerpool.go
+++ b/workerpool.go
@@ -6,6 +6,7 @@ import (
 	"runtime"
 	"strings"
 	"sync"
+	"sync/atomic"
 	"time"
 )
 
@@ -21,29 +22,57 @@ type workerPool struct {
 
 	// Function for serving server connections.
 	// It must leave c unclosed.
+	ready      workerChanStack
 	WorkerFunc ServeHandler
 
 	stopCh chan struct{}
 
 	connState func(net.Conn, ConnState)
 
-	ready []*workerChan
-
 	MaxWorkersCount int
 
 	MaxIdleWorkerDuration time.Duration
 
-	workersCount int
+	workersCount int32
 
-	lock sync.Mutex
+	mustStop atomic.Bool
 
 	LogAllErrors bool
-	mustStop     bool
 }
 
 type workerChan struct {
-	lastUseTime time.Time
-	ch          chan net.Conn
+	next *workerChan
+
+	ch chan net.Conn
+
+	lastUseTime int64
+}
+
+type workerChanStack struct {
+	head atomic.Pointer[workerChan]
+}
+
+func (s *workerChanStack) push(ch *workerChan) {
+	for {
+		oldHead := s.head.Load()
+		ch.next = oldHead
+		if s.head.CompareAndSwap(oldHead, ch) {
+			break
+		}
+	}
+}
+
+func (s *workerChanStack) pop() *workerChan {
+	for {
+		oldHead := s.head.Load()
+		if oldHead == nil {
+			return nil
+		}
+
+		if s.head.CompareAndSwap(oldHead, oldHead.next) {
+			return oldHead
+		}
+	}
 }
 
 func (wp *workerPool) Start() {
@@ -58,9 +87,8 @@ func (wp *workerPool) Start() {
 		}
 	}
 	go func() {
-		var scratch []*workerChan
 		for {
-			wp.clean(&scratch)
+			wp.clean()
 			select {
 			case <-stopCh:
 				return
@@ -81,15 +109,15 @@ func (wp *workerPool) Stop() {
 	// Stop all the workers waiting for incoming connections.
 	// Do not wait for busy workers - they will stop after
 	// serving the connection and noticing wp.mustStop = true.
-	wp.lock.Lock()
-	ready := wp.ready
-	for i := range ready {
-		ready[i].ch <- nil
-		ready[i] = nil
+
+	for {
+		ch := wp.ready.pop()
+		if ch == nil {
+			break
+		}
+		ch.ch <- nil
 	}
-	wp.ready = ready[:0]
-	wp.mustStop = true
-	wp.lock.Unlock()
+	wp.mustStop.Store(true)
 }
 
 func (wp *workerPool) getMaxIdleWorkerDuration() time.Duration {
@@ -99,49 +127,21 @@ func (wp *workerPool) getMaxIdleWorkerDuration() time.Duration {
 	return wp.MaxIdleWorkerDuration
 }
 
-func (wp *workerPool) clean(scratch *[]*workerChan) {
+func (wp *workerPool) clean() {
 	maxIdleWorkerDuration := wp.getMaxIdleWorkerDuration()
+	criticalTime := time.Now().Add(-maxIdleWorkerDuration).UnixNano()
 
-	// Clean least recently used workers if they didn't serve connections
-	// for more than maxIdleWorkerDuration.
-	criticalTime := time.Now().Add(-maxIdleWorkerDuration)
-
-	wp.lock.Lock()
-	ready := wp.ready
-	n := len(ready)
-
-	// Use binary-search algorithm to find out the index of the least recently worker which can be cleaned up.
-	l, r := 0, n-1
-	for l <= r {
-		mid := (l + r) / 2
-		if criticalTime.After(wp.ready[mid].lastUseTime) {
-			l = mid + 1
-		} else {
-			r = mid - 1
+	for {
+		current := wp.ready.head.Load()
+		if current == nil || atomic.LoadInt64(&current.lastUseTime) >= criticalTime {
+			break
 		}
-	}
-	i := r
-	if i == -1 {
-		wp.lock.Unlock()
-		return
-	}
 
-	*scratch = append((*scratch)[:0], ready[:i+1]...)
-	m := copy(ready, ready[i+1:])
-	for i = m; i < n; i++ {
-		ready[i] = nil
-	}
-	wp.ready = ready[:m]
-	wp.lock.Unlock()
-
-	// Notify obsolete workers to stop.
-	// This notification must be outside the wp.lock, since ch.ch
-	// may be blocking and may consume a lot of time if many workers
-	// are located on non-local CPUs.
-	tmp := *scratch
-	for i := range tmp {
-		tmp[i].ch <- nil
-		tmp[i] = nil
+		next := current.next
+		if wp.ready.head.CompareAndSwap(current, next) {
+			current.ch <- nil
+			wp.workerChanPool.Put(current)
+		}
 	}
 }
 
@@ -169,47 +169,32 @@ var workerChanCap = func() int {
 }()
 
 func (wp *workerPool) getCh() *workerChan {
-	var ch *workerChan
-	createWorker := false
-
-	wp.lock.Lock()
-	ready := wp.ready
-	n := len(ready) - 1
-	if n < 0 {
-		if wp.workersCount < wp.MaxWorkersCount {
-			createWorker = true
-			wp.workersCount++
+	for {
+		ch := wp.ready.pop()
+		if ch != nil {
+			return ch
 		}
-	} else {
-		ch = ready[n]
-		ready[n] = nil
-		wp.ready = ready[:n]
-	}
-	wp.lock.Unlock()
 
-	if ch == nil {
-		if !createWorker {
-			return nil
+		currentWorkers := atomic.LoadInt32(&wp.workersCount)
+		if currentWorkers < int32(wp.MaxWorkersCount) {
+			if atomic.CompareAndSwapInt32(&wp.workersCount, currentWorkers, currentWorkers+1) {
+				ch = wp.workerChanPool.Get().(*workerChan)
+				go wp.workerFunc(ch)
+				return ch
+			}
+		} else {
+			break
 		}
-		vch := wp.workerChanPool.Get()
-		ch = vch.(*workerChan)
-		go func() {
-			wp.workerFunc(ch)
-			wp.workerChanPool.Put(vch)
-		}()
 	}
-	return ch
+	return nil
 }
 
 func (wp *workerPool) release(ch *workerChan) bool {
-	ch.lastUseTime = time.Now()
-	wp.lock.Lock()
-	if wp.mustStop {
-		wp.lock.Unlock()
+	atomic.StoreInt64(&ch.lastUseTime, time.Now().UnixNano())
+	if wp.mustStop.Load() {
 		return false
 	}
-	wp.ready = append(wp.ready, ch)
-	wp.lock.Unlock()
+	wp.ready.push(ch)
 	return true
 }
 
@@ -245,7 +230,5 @@ func (wp *workerPool) workerFunc(ch *workerChan) {
 		}
 	}
 
-	wp.lock.Lock()
-	wp.workersCount--
-	wp.lock.Unlock()
+	atomic.AddInt32(&wp.workersCount, -1)
 }


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 731b456..0ab885b 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -9,7 +9,7 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        go-version: [1.19.x, 1.20.x, 1.21.x, 1.22.x]
+        go-version: [1.21.x, 1.22.x, 1.23.x]
         os: [ubuntu-latest, macos-latest, windows-latest, macos-14]
     runs-on: ${{ matrix.os }}
     steps:
diff --git a/client_test.go b/client_test.go
index 13ae07b..19629fc 100644
--- a/client_test.go
+++ b/client_test.go
@@ -3484,3 +3484,52 @@ func TestDialTimeout(t *testing.T) {
 		})
 	}
 }
+
+func TestClientHeadWithBody(t *testing.T) {
+	t.Parallel()
+
+	ln := fasthttputil.NewInmemoryListener()
+	defer ln.Close()
+
+	go func() {
+		c, err := ln.Accept()
+		if err != nil {
+			t.Error(err)
+		}
+		c.Write([]byte("HTTP/1.1 200 OK\r\n" + //nolint:errcheck
+			"content-type: text/plain\r\n" +
+			"transfer-encoding: chunked\r\n\r\n" +
+			"24\r\nThis is the data in the first chunk \r\n" +
+			"1B\r\nand this is the second one \r\n" +
+			"0\r\n\r\n",
+		))
+	}()
+
+	c := &Client{
+		Dial: func(addr string) (net.Conn, error) {
+			return ln.Dial()
+		},
+		ReadTimeout:               time.Millisecond * 10,
+		MaxIdemponentCallAttempts: 1,
+	}
+
+	req := AcquireRequest()
+	req.SetRequestURI("http://127.0.0.1:7070")
+	req.Header.SetMethod(MethodHead)
+
+	resp := AcquireResponse()
+
+	err := c.Do(req, resp)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// The second request on the same connection is going to give a timeout as it can't find
+	// a proper request in what is left on the connection.
+	err = c.Do(req, resp)
+	if err == nil {
+		t.Error("expected timeout error")
+	} else if err != ErrTimeout {
+		t.Error(err)
+	}
+}
diff --git a/fasthttpproxy/dialer_test.go b/fasthttpproxy/dialer_test.go
new file mode 100644
index 0000000..e2802c1
--- /dev/null
+++ b/fasthttpproxy/dialer_test.go
@@ -0,0 +1,306 @@
+package fasthttpproxy
+
+import (
+	"bufio"
+	"io"
+	"net"
+	"strings"
+	"sync/atomic"
+	"testing"
+
+	"github.com/valyala/fasthttp"
+	"golang.org/x/net/http/httpproxy"
+)
+
+func TestDialer_GetDialFunc(t *testing.T) {
+	counts := make([]atomic.Int64, 4)
+	proxyListenPorts := []string{"8001", "8002", "8003", "8004"}
+	lns := startProxyServer(t, proxyListenPorts, counts)
+	defer func() {
+		for _, l := range lns {
+			l.Close()
+		}
+	}()
+	t.Setenv("HTTP_PROXY", "http://127.0.0.1:"+proxyListenPorts[2])
+	t.Setenv("HTTPS_PROXY", "http://127.0.0.1:"+proxyListenPorts[3])
+	t.Setenv("NO_PROXY", "example.com")
+	type fields struct {
+		httpProxy  string
+		httpsProxy string
+		noProxy    string
+	}
+	type args struct {
+		useEnv bool
+	}
+	tests := []struct {
+		name           string
+		fields         fields
+		args           args
+		wantCounts     []int64
+		dialAddr       string
+		wantErrMessage string
+	}{
+		{
+			name: "proxy information comes from the configuration. dial https host",
+			fields: fields{
+				httpProxy:  "http://127.0.0.1:" + proxyListenPorts[0],
+				httpsProxy: "http://127.0.0.1:" + proxyListenPorts[1],
+				noProxy:    "example.com",
+			},
+			args: args{
+				useEnv: false,
+			},
+			wantCounts: []int64{0, 1, 0, 0},
+			dialAddr:   "www.google.com:443",
+		},
+		{
+			name: "proxy information comes from the configuration. dial http host",
+			fields: fields{
+				httpProxy:  "http://127.0.0.1:" + proxyListenPorts[0],
+				httpsProxy: "http://127.0.0.1:" + proxyListenPorts[1],
+				noProxy:    "example.com",
+			},
+			args: args{
+				useEnv: false,
+			},
+			wantCounts: []int64{1, 0, 0, 0},
+			dialAddr:   "www.google.com:80",
+		},
+		{
+			name: "proxy information comes from the configuration. dial http host matched with noProxy",
+			fields: fields{
+				httpProxy:  "http://127.0.0.1:" + proxyListenPorts[0],
+				httpsProxy: "http://127.0.0.1:" + proxyListenPorts[1],
+				noProxy:    "example.com",
+			},
+			args: args{
+				useEnv: false,
+			},
+			wantCounts: []int64{0, 0, 0, 0},
+			dialAddr:   "example.com:80",
+		},
+		{
+			name: "proxy information comes from the configuration. dial https host matched with noProxy",
+			fields: fields{
+				httpProxy:  "http://127.0.0.1:" + proxyListenPorts[0],
+				httpsProxy: "http://127.0.0.1:" + proxyListenPorts[1],
+				noProxy:    "example.com",
+			},
+			args: args{
+				useEnv: false,
+			},
+			wantCounts: []int64{0, 0, 0, 0},
+			dialAddr:   "example.com:443",
+		},
+		{
+			name: "proxy information comes from the env. dial http host",
+			fields: fields{
+				httpProxy:  "http://127.0.0.1:" + proxyListenPorts[0],
+				httpsProxy: "http://127.0.0.1:" + proxyListenPorts[1],
+				noProxy:    "example.com",
+			},
+			args: args{
+				useEnv: true,
+			},
+			wantCounts: []int64{0, 0, 1, 0},
+			dialAddr:   "www.google.com:80",
+		},
+		{
+			name: "proxy information comes from the env. dial https host",
+			fields: fields{
+				httpProxy:  "http://127.0.0.1:" + proxyListenPorts[0],
+				httpsProxy: "http://127.0.0.1:" + proxyListenPorts[1],
+				noProxy:    "example.com",
+			},
+			args: args{
+				useEnv: true,
+			},
+			wantCounts: []int64{0, 0, 0, 1},
+			dialAddr:   "www.google.com:443",
+		},
+
+		{
+			name: "proxy information comes from the env. dial http host matched with noProxy",
+			fields: fields{
+				httpProxy:  "http://127.0.0.1:" + proxyListenPorts[0],
+				httpsProxy: "http://127.0.0.1:" + proxyListenPorts[1],
+				noProxy:    "example.com",
+			},
+			args: args{
+				useEnv: true,
+			},
+			wantCounts: []int64{0, 0, 0, 0},
+			dialAddr:   "example.com:80",
+		},
+		{
+			name: "proxy information comes from the env. dial https host matched with noProxy",
+			fields: fields{
+				httpProxy:  "http://127.0.0.1:" + proxyListenPorts[0],
+				httpsProxy: "http://127.0.0.1:" + proxyListenPorts[1],
+				noProxy:    "example.com",
+			},
+			args: args{
+				useEnv: true,
+			},
+			wantCounts: []int64{0, 0, 0, 0},
+			dialAddr:   "example.com:443",
+		},
+		{
+			name: "proxy information comes from the configuration and httpProxy same with httpsProxy. dial http host",
+			fields: fields{
+				httpProxy:  "http://127.0.0.1:" + proxyListenPorts[0],
+				httpsProxy: "http://127.0.0.1:" + proxyListenPorts[0],
+				noProxy:    "example.com",
+			},
+			args: args{
+				useEnv: false,
+			},
+			wantCounts: []int64{1, 0, 0, 0},
+			dialAddr:   "www.google.com:80",
+		},
+		{
+			name: "proxy information comes from the configuration and httpProxy same with httpsProxy. dial https host",
+			fields: fields{
+				httpProxy:  "http://127.0.0.1:" + proxyListenPorts[0],
+				httpsProxy: "http://127.0.0.1:" + proxyListenPorts[0],
+				noProxy:    "example.com",
+			},
+			args: args{
+				useEnv: false,
+			},
+			wantCounts: []int64{1, 0, 0, 0},
+			dialAddr:   "www.google.com:443",
+		},
+		{
+			name: "proxy information comes from the configuration and httpProxy same with httpsProxy. dial http host matched with noProxy",
+			fields: fields{
+				httpProxy:  "http://127.0.0.1:" + proxyListenPorts[0],
+				httpsProxy: "http://127.0.0.1:" + proxyListenPorts[0],
+				noProxy:    "example.com",
+			},
+			args: args{
+				useEnv: false,
+			},
+			wantCounts: []int64{0, 0, 0, 0},
+			dialAddr:   "example.com:80",
+		},
+		{
+			name: "proxy information comes from the configuration and httpProxy same with httpsProxy. dial https host matched with noProxy",
+			fields: fields{
+				httpProxy:  "http://127.0.0.1:" + proxyListenPorts[0],
+				httpsProxy: "http://127.0.0.1:" + proxyListenPorts[0],
+				noProxy:    "example.com",
+			},
+			args: args{
+				useEnv: false,
+			},
+			wantCounts: []int64{0, 0, 0, 0},
+			dialAddr:   "example.com:443",
+		},
+		{
+			name: "return an error for unsupported proxy protocols.",
+			fields: fields{
+				httpProxy:  "socket6://127.0.0.1:" + proxyListenPorts[0],
+				httpsProxy: "socket6://127.0.0.1:" + proxyListenPorts[0],
+			},
+			args: args{
+				useEnv: false,
+			},
+			wantCounts:     []int64{0, 0, 0, 0},
+			dialAddr:       "www.google.com:80",
+			wantErrMessage: "proxy: unknown scheme: socket6",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			d := getDialer(tt.fields.httpProxy, tt.fields.httpsProxy, tt.fields.noProxy)
+			dialFunc, err := d.GetDialFunc(tt.args.useEnv)
+			if (err != nil) != (tt.wantErrMessage != "") {
+				t.Fatalf("GetDialFunc() error = %v, wantErr %v", err, tt.wantErrMessage)
+				return
+			}
+			if tt.wantErrMessage != "" {
+				if err.Error() != tt.wantErrMessage {
+					t.Fatalf("want error message: %s, got: %s", err.Error(), tt.wantErrMessage)
+				}
+				return
+			}
+			_, err = dialFunc(tt.dialAddr)
+			if err != nil {
+				t.Fatal(err)
+			}
+			if !countsEqual(getCounts(counts), tt.wantCounts) {
+				t.Errorf("GetDialFunc() counts = %v, want %v", getCounts(counts), tt.wantCounts)
+			}
+		})
+		for i := 0; i < len(counts); i++ {
+			counts[i].Store(0)
+		}
+	}
+}
+
+func startProxyServer(t *testing.T, ports []string, counts []atomic.Int64) (lns []net.Listener) {
+	for i, port := range ports {
+		ln, err := net.Listen("tcp", ":"+port)
+		if err != nil {
+			t.Fatal(err)
+		}
+		lns = append(lns, ln)
+		i := i
+		go func() {
+			req := fasthttp.AcquireRequest()
+			for {
+				conn, err := ln.Accept()
+				if err != nil {
+					if err != io.EOF && !strings.Contains(err.Error(), "use of closed network connection") {
+						t.Error(err)
+					}
+					break
+				}
+				err = req.Read(bufio.NewReader(conn))
+				if err != nil {
+					t.Error(err)
+				}
+				if string(req.Header.Method()) == "CONNECT" {
+					counts[i].Add(1)
+				}
+				_, err = conn.Write([]byte("HTTP/1.1 200 Connection Established\r\n\r\n"))
+				if err != nil {
+					t.Error(err)
+				}
+				req.Reset()
+			}
+			fasthttp.ReleaseRequest(req)
+		}()
+	}
+	return
+}
+
+func getDialer(httpProxy, httpsProxy, noProxy string) *Dialer {
+	return &Dialer{
+		Config: httpproxy.Config{
+			HTTPProxy:  httpProxy,
+			HTTPSProxy: httpsProxy,
+			NoProxy:    noProxy,
+		},
+	}
+}
+
+func getCounts(counts []atomic.Int64) (r []int64) {
+	for i := 0; i < len(counts); i++ {
+		r = append(r, counts[i].Load())
+	}
+	return
+}
+
+func countsEqual(a, b []int64) bool {
+	if len(a) != len(b) {
+		return false
+	}
+	for i := range a {
+		if b[i] != a[i] {
+			return false
+		}
+	}
+	return true
+}
diff --git a/fs_test.go b/fs_test.go
index 2faf16c..1f7df00 100644
--- a/fs_test.go
+++ b/fs_test.go
@@ -10,6 +10,7 @@ import (
 	"path/filepath"
 	"runtime"
 	"sort"
+	"strings"
 	"testing"
 	"time"
 )
@@ -939,3 +940,66 @@ func TestServeFileDirectoryRedirect(t *testing.T) {
 		t.Fatalf("Unexpected status code %d for file '/fs.go'. Expecting %d.", ctx.Response.StatusCode(), StatusOK)
 	}
 }
+
+func TestFileCacheForZstd(t *testing.T) {
+	f, err := os.CreateTemp(os.TempDir(), "test")
+	if err != nil {
+		t.Fatal(err)
+	}
+	data := bytes.Repeat([]byte("1"), 1000)
+	changedData := bytes.Repeat([]byte("2"), 1000)
+	_, err = f.Write(data)
+	if err != nil {
+		t.Fatal(err)
+	}
+	err = f.Sync()
+	if err != nil {
+		t.Fatal(err)
+	}
+	fs := FS{Root: os.TempDir(), Compress: true, CacheDuration: time.Second * 60}
+	h := fs.NewRequestHandler()
+	var ctx RequestCtx
+	var req Request
+	req.Header.Set("Accept-Encoding", "zstd")
+	req.SetRequestURI("http://foobar.com/" + strings.TrimPrefix(f.Name(), os.TempDir()))
+	ctx.Init(&req, nil, nil)
+	h(&ctx)
+	if !bytes.Equal(ctx.Response.Header.ContentEncoding(), []byte("zstd")) {
+		t.Fatalf("Unexpected 'Content-Encoding' %q. Expecting %q", ctx.Response.Header.ContentEncoding(), "zstd")
+	}
+	ctx.Response.Reset()
+	_, err = f.Seek(0, io.SeekStart)
+	if err != nil {
+		t.Fatal(err)
+	}
+	_, err = f.Write(changedData)
+	if err != nil {
+		t.Fatal(err)
+	}
+	f.Close()
+	h(&ctx)
+	if !bytes.Equal(ctx.Response.Header.ContentEncoding(), []byte("zstd")) {
+		t.Fatalf("Unexpected 'Content-Encoding' %q. Expecting %q", ctx.Response.Header.ContentEncoding(), "zstd")
+	}
+	d, err := acquireZstdReader(strings.NewReader(string(ctx.Response.Body())))
+	if err != nil {
+		t.Fatalf("invalid zstd reader")
+	}
+	plainText, err := io.ReadAll(d)
+	d.Close()
+	if err != nil {
+		t.Fatal(err)
+	}
+	if !bytes.Equal(plainText, data) {
+		t.Fatalf("Unexpected response body %q. Expecting %q . Zstd cache doesn't work", plainText, data)
+	}
+	ctx.Request.Header.Del("Accept-Encoding")
+	ctx.Response.Reset()
+	h(&ctx)
+	if !bytes.Equal(ctx.Response.Header.ContentEncoding(), []byte("")) {
+		t.Fatalf("Unexpected 'Content-Encoding' %q. Expecting %q", ctx.Response.Header.ContentEncoding(), "")
+	}
+	if !bytes.Equal(ctx.Response.Body(), changedData) {
+		t.Fatalf("Unexpected response body %q. Expecting %q", ctx.Response.Body(), data)
+	}
+}
diff --git a/header_test.go b/header_test.go
index 0b46cd8..37b775b 100644
--- a/header_test.go
+++ b/header_test.go
@@ -1465,8 +1465,10 @@ func TestResponseHeaderCopyTo(t *testing.T) {
 	}
 
 	// flush buf
-	h.bufKV = argsKV{}
-	h1.bufKV = argsKV{}
+	h.bufK = []byte{}
+	h.bufV = []byte{}
+	h1.bufK = []byte{}
+	h1.bufV = []byte{}
 
 	if !reflect.DeepEqual(&h, &h1) {
 		t.Fatalf("ResponseHeaderCopyTo fail, src: \n%+v\ndst: \n%+v\n", &h, &h1)
@@ -1508,8 +1510,10 @@ func TestRequestHeaderCopyTo(t *testing.T) {
 	}
 
 	// flush buf
-	h.bufKV = argsKV{}
-	h1.bufKV = argsKV{}
+	h.bufK = []byte{}
+	h.bufV = []byte{}
+	h1.bufK = []byte{}
+	h1.bufV = []byte{}
 
 	if !reflect.DeepEqual(&h, &h1) {
 		t.Fatalf("RequestHeaderCopyTo fail, src: \n%+v\ndst: \n%+v\n", &h, &h1)
@@ -2961,6 +2965,8 @@ func verifyRequestHeader(t *testing.T, h *RequestHeader, expectedContentLength i
 }
 
 func verifyResponseTrailer(t *testing.T, h *ResponseHeader, expectedTrailers map[string]string) {
+	t.Helper()
+
 	for k, v := range expectedTrailers {
 		got := h.Peek(k)
 		if !bytes.Equal(got, []byte(v)) {
diff --git a/http_test.go b/http_test.go
index a9f440a..86ceaae 100644
--- a/http_test.go
+++ b/http_test.go
@@ -2021,25 +2021,27 @@ func TestResponseReadWithoutBody(t *testing.T) {
 	var resp Response
 
 	testResponseReadWithoutBody(t, &resp, "HTTP/1.1 304 Not Modified\r\nContent-Type: aa\r\nContent-Length: 1235\r\n\r\n", false,
-		304, 1235, "aa", nil)
+		304, 1235, "aa")
 
-	testResponseReadWithoutBody(t, &resp, "HTTP/1.1 204 Foo Bar\r\nContent-Type: aab\r\nTransfer-Encoding: chunked\r\n\r\n0\r\nFoo: bar\r\n\r\n", false,
-		204, -1, "aab", map[string]string{"Foo": "bar"})
+	testResponseReadWithoutBody(t, &resp, "HTTP/1.1 204 Foo Bar\r\nContent-Type: aab\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\n", false,
+		204, -1, "aab")
 
 	testResponseReadWithoutBody(t, &resp, "HTTP/1.1 123 AAA\r\nContent-Type: xxx\r\nContent-Length: 3434\r\n\r\n", false,
-		123, 3434, "xxx", nil)
+		123, 3434, "xxx")
 
 	testResponseReadWithoutBody(t, &resp, "HTTP 200 OK\r\nContent-Type: text/xml\r\nContent-Length: 123\r\n\r\nfoobar\r\n", true,
-		200, 123, "text/xml", nil)
+		200, 123, "text/xml")
 
 	// '100 Continue' must be skipped.
 	testResponseReadWithoutBody(t, &resp, "HTTP/1.1 100 Continue\r\nFoo-bar: baz\r\n\r\nHTTP/1.1 329 aaa\r\nContent-Type: qwe\r\nContent-Length: 894\r\n\r\n", true,
-		329, 894, "qwe", nil)
+		329, 894, "qwe")
 }
 
 func testResponseReadWithoutBody(t *testing.T, resp *Response, s string, skipBody bool,
-	expectedStatusCode, expectedContentLength int, expectedContentType string, expectedTrailer map[string]string,
+	expectedStatusCode, expectedContentLength int, expectedContentType string,
 ) {
+	t.Helper()
+
 	r := bytes.NewBufferString(s)
 	rb := bufio.NewReader(r)
 	resp.SkipBody = skipBody
@@ -2051,7 +2053,6 @@ func testResponseReadWithoutBody(t *testing.T, resp *Response, s string, skipBod
 		t.Fatalf("Unexpected response body %q. Expected %q. response=%q", resp.Body(), "", s)
 	}
 	verifyResponseHeader(t, &resp.Header, expectedStatusCode, expectedContentLength, expectedContentType, "")
-	verifyResponseTrailer(t, &resp.Header, expectedTrailer)
 
 	// verify that ordinal response is read after null-body response
 	resp.SkipBody = false