Repository: cloudwego/hertz
Pull request #583 (2023-02-02 07:03:57)

chore: release v0.5.2


Patch:
diff --git a/.golangci.yaml b/.golangci.yaml
new file mode 100644
index 0000000..23c8d36
--- /dev/null
+++ b/.golangci.yaml
@@ -0,0 +1,35 @@
+# Options for analysis running.
+run:
+  # include `vendor` `third_party` `testdata` `examples` `Godeps` `builtin`
+  skip-dirs-use-default: true
+# output configuration options
+
+  # Timeout for analysis, e.g. 30s, 5m.
+  # Default: 1m
+  timeout: 30m
+
+output:
+  # Format: colored-line-number|line-number|json|tab|checkstyle|code-climate|junit-xml|github-actions
+  format: colored-line-number
+# All available settings of specific linters.
+# Refer to https://golangci-lint.run/usage/linters
+linters-settings:
+  govet:
+    # Disable analyzers by name.
+    # Run `go tool vet help` to see all analyzers.
+    disable:
+      - stdmethods
+linters:
+  enable:
+    - gofumpt
+    - goimports
+    - gofmt
+    - govet
+  disable:
+    - errcheck
+    - typecheck
+    - deadcode
+    - varcheck
+    - staticcheck
+issues:
+  exclude-use-default: true
\ No newline at end of file
diff --git a/cmd/hz/app/app.go b/cmd/hz/app/app.go
index 8625de4..1f639a9 100644
--- a/cmd/hz/app/app.go
+++ b/cmd/hz/app/app.go
@@ -136,7 +136,7 @@ func Init() *cli.App {
 	verboseFlag := cli.BoolFlag{Name: "verbose,vv", Usage: "turn on verbose mode", Destination: &globalArgs.Verbose}
 
 	idlFlag := cli.StringSliceFlag{Name: "idl", Usage: "Specify the IDL file path. (.thrift or .proto)"}
-	moduleFlag := cli.StringFlag{Name: "module", Aliases: []string{"mod"}, Usage: "Specify the Go module name to generate go.mod.", Destination: &globalArgs.Gomod}
+	moduleFlag := cli.StringFlag{Name: "module", Aliases: []string{"mod"}, Usage: "Specify the Go module name.", Destination: &globalArgs.Gomod}
 	serviceNameFlag := cli.StringFlag{Name: "service", Usage: "Specify the service name.", Destination: &globalArgs.ServiceName}
 	outDirFlag := cli.StringFlag{Name: "out_dir", Usage: "Specify the project path.", Destination: &globalArgs.OutDir}
 	handlerDirFlag := cli.StringFlag{Name: "handler_dir", Usage: "Specify the handler path.", Destination: &globalArgs.HandlerDir}
@@ -212,6 +212,7 @@ func Init() *cli.App {
 			Usage: "Update an existing Hertz project",
 			Flags: []cli.Flag{
 				&idlFlag,
+				&moduleFlag,
 				&outDirFlag,
 				&handlerDirFlag,
 				&modelDirFlag,
diff --git a/cmd/hz/meta/const.go b/cmd/hz/meta/const.go
index 00c0aa8..2e4d82f 100644
--- a/cmd/hz/meta/const.go
+++ b/cmd/hz/meta/const.go
@@ -19,7 +19,7 @@ package meta
 import "runtime"
 
 // Version hz version
-const Version = "v0.5.1"
+const Version = "v0.5.2"
 
 // Mode hz run modes
 type Mode int
diff --git a/cmd/hz/protobuf/api/api.pb.go b/cmd/hz/protobuf/api/api.pb.go
index 219a013..6a38b1b 100644
--- a/cmd/hz/protobuf/api/api.pb.go
+++ b/cmd/hz/protobuf/api/api.pb.go
@@ -7,10 +7,11 @@
 package api
 
 import (
+	reflect "reflect"
+
 	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
 	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
 	descriptorpb "google.golang.org/protobuf/types/descriptorpb"
-	reflect "reflect"
 )
 
 const (
diff --git a/licenses/LICENSE-fsnotify b/licenses/LICENSE-fsnotify
new file mode 100644
index 0000000..c656a43
--- /dev/null
+++ b/licenses/LICENSE-fsnotify
@@ -0,0 +1,25 @@
+Copyright © 2012 The Go Authors. All rights reserved.
+Copyright © fsnotify Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+  list of conditions and the following disclaimer.
+* Redistributions in binary form must reproduce the above copyright notice, this
+  list of conditions and the following disclaimer in the documentation and/or
+  other materials provided with the distribution.
+* Neither the name of Google Inc. nor the names of its contributors may be used
+  to endorse or promote products derived from this software without specific
+  prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
diff --git a/pkg/app/client/option.go b/pkg/app/client/option.go
index 34f4196..3e03c44 100644
--- a/pkg/app/client/option.go
+++ b/pkg/app/client/option.go
@@ -180,8 +180,7 @@ type customDialer struct {
 	dialFunc network.DialFunc
 }
 
-func (m *customDialer) DialConnection(network, address string, timeout time.Duration,
-	tlsConfig *tls.Config) (conn network.Conn, err error) {
+func (m *customDialer) DialConnection(network, address string, timeout time.Duration, tlsConfig *tls.Config) (conn network.Conn, err error) {
 	if m.dialFunc != nil {
 		return m.dialFunc(address)
 	}
diff --git a/pkg/app/server/render/html.go b/pkg/app/server/render/html.go
index f31e33a..2a9fe8a 100644
--- a/pkg/app/server/render/html.go
+++ b/pkg/app/server/render/html.go
@@ -113,7 +113,6 @@ type HTMLDebug struct {
 	sync.Once
 	Template        *template.Template
 	RefreshInterval time.Duration
-	updateTimeStamp time.Time
 
 	Files   []string
 	FuncMap template.FuncMap
@@ -161,14 +160,10 @@ func (h *HTMLDebug) startChecker() {
 	if h.RefreshInterval > 0 {
 		go func() {
 			hlog.SystemLogger().Debugf("[HTMLDebug] HTML template reloader started with interval %v", h.RefreshInterval)
-			for {
-				n := time.Now()
-				if n.UTC().Sub(h.updateTimeStamp.UTC()) > h.RefreshInterval {
-					hlog.SystemLogger().Debugf("[HTMLDebug] triggering HTML template reloader")
-					h.reloadCh <- struct{}{}
-					hlog.SystemLogger().Debugf("[HTMLDebug] HTML template has been reloaded, next reload in %v", h.RefreshInterval)
-					h.updateTimeStamp = time.Now()
-				}
+			for range time.Tick(h.RefreshInterval) {
+				hlog.SystemLogger().Debugf("[HTMLDebug] triggering HTML template reloader")
+				h.reloadCh <- struct{}{}
+				hlog.SystemLogger().Debugf("[HTMLDebug] HTML template has been reloaded, next reload in %v", h.RefreshInterval)
 			}
 		}()
 		return
diff --git a/pkg/common/json/sonic.go b/pkg/common/json/sonic.go
index af0174e..dad1e63 100644
--- a/pkg/common/json/sonic.go
+++ b/pkg/common/json/sonic.go
@@ -13,9 +13,10 @@
 // limitations under the License.
 //
 
-//go:build (linux || windows || darwin) && amd64
+//go:build (linux || windows || darwin) && amd64 && !stdjson
 // +build linux windows darwin
 // +build amd64
+// +build !stdjson
 
 package json
 
diff --git a/pkg/network/netpoll/connection.go b/pkg/network/netpoll/connection.go
index 683f9c3..7c26ef9 100644
--- a/pkg/network/netpoll/connection.go
+++ b/pkg/network/netpoll/connection.go
@@ -1,21 +1,17 @@
-//go:build !windows
-// +build !windows
-
-/*
- * Copyright 2022 CloudWeGo Authors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
+// Copyright 2022 CloudWeGo Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
 
 package netpoll
 
@@ -29,7 +25,6 @@ import (
 	"github.com/cloudwego/hertz/pkg/common/hlog"
 	"github.com/cloudwego/hertz/pkg/network"
 	"github.com/cloudwego/netpoll"
-	"golang.org/x/sys/unix"
 )
 
 type Conn struct {
@@ -37,7 +32,7 @@ type Conn struct {
 }
 
 func (c *Conn) ToHertzError(err error) error {
-	if errors.Is(err, netpoll.ErrConnClosed) || errors.Is(err, unix.EPIPE) {
+	if errors.Is(err, netpoll.ErrConnClosed) || errors.Is(err, syscall.EPIPE) {
 		return errs.ErrConnectionClosed
 	}
 	return err
diff --git a/pkg/network/netpoll/dial.go b/pkg/network/netpoll/dial.go
index 70adbc1..86a371e 100644
--- a/pkg/network/netpoll/dial.go
+++ b/pkg/network/netpoll/dial.go
@@ -13,9 +13,6 @@
 // limitations under the License.
 //
 
-//go:build !windows
-// +build !windows
-
 package netpoll
 
 import (
diff --git a/pkg/network/netpoll/transport.go b/pkg/network/netpoll/transport.go
index c213060..12e54a9 100644
--- a/pkg/network/netpoll/transport.go
+++ b/pkg/network/netpoll/transport.go
@@ -1,22 +1,21 @@
+// Copyright 2022 CloudWeGo Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
 //go:build !windows
 // +build !windows
 
-/*
- * Copyright 2022 CloudWeGo Authors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
 package netpoll
 
 import (
diff --git a/pkg/protocol/args.go b/pkg/protocol/args.go
index 5731db5..ebf9ae5 100644
--- a/pkg/protocol/args.go
+++ b/pkg/protocol/args.go
@@ -224,6 +224,16 @@ func peekArgBytes(h []argsKV, k []byte) []byte {
 	return nil
 }
 
+func peekAllArgBytesToDst(dst [][]byte, h []argsKV, k []byte) [][]byte {
+	for i, n := 0, len(h); i < n; i++ {
+		kv := &h[i]
+		if bytes.Equal(kv.key, k) {
+			dst = append(dst, kv.value)
+		}
+	}
+	return dst
+}
+
 func delAllArgsBytes(args []argsKV, key []byte) []argsKV {
 	return delAllArgs(args, bytesconv.B2s(key))
 }
diff --git a/pkg/protocol/header.go b/pkg/protocol/header.go
index b447cce..dfc5c8d 100644
--- a/pkg/protocol/header.go
+++ b/pkg/protocol/header.go
@@ -83,6 +83,7 @@ type RequestHeader struct {
 	host        []byte
 	contentType []byte
 	userAgent   []byte
+	mulHeader   [][]byte
 
 	h     []argsKV
 	bufKV argsKV
@@ -121,6 +122,7 @@ type ResponseHeader struct {
 
 	contentType []byte
 	server      []byte
+	mulHeader   [][]byte
 
 	h     []argsKV
 	bufKV argsKV
@@ -486,6 +488,7 @@ func (h *ResponseHeader) ResetSkipNormalize() {
 
 	h.h = h.h[:0]
 	h.cookies = h.cookies[:0]
+	h.mulHeader = h.mulHeader[:0]
 }
 
 // ContentLength returns Content-Length header value.
@@ -687,6 +690,94 @@ func (h *ResponseHeader) peek(key []byte) []byte {
 	}
 }
 
+// PeekAll returns all header value for the given key.
+//
+// The returned value is valid until the request is released,
+// either though ReleaseResponse or your request handler returning.
+// Any future calls to the Peek* will modify the returned value.
+// Do not store references to returned value. Use ResponseHeader.GetAll(key) instead.
+func (h *ResponseHeader) PeekAll(key string) [][]byte {
+	k := getHeaderKeyBytes(&h.bufKV, key, h.disableNormalizing)
+	return h.peekAll(k)
+}
+
+func (h *ResponseHeader) peekAll(key []byte) [][]byte {
+	h.mulHeader = h.mulHeader[:0]
+	switch string(key) {
+	case consts.HeaderContentType:
+		if contentType := h.ContentType(); len(contentType) > 0 {
+			h.mulHeader = append(h.mulHeader, contentType)
+		}
+	case consts.HeaderContentEncoding:
+		if contentEncoding := h.ContentEncoding(); len(contentEncoding) > 0 {
+			h.mulHeader = append(h.mulHeader, contentEncoding)
+		}
+	case consts.HeaderServer:
+		if server := h.Server(); len(server) > 0 {
+			h.mulHeader = append(h.mulHeader, server)
+		}
+	case consts.HeaderConnection:
+		if h.ConnectionClose() {
+			h.mulHeader = append(h.mulHeader, bytestr.StrClose)
+		} else {
+			h.mulHeader = peekAllArgBytesToDst(h.mulHeader, h.h, key)
+		}
+	case consts.HeaderContentLength:
+		h.mulHeader = append(h.mulHeader, h.contentLengthBytes)
+	case consts.HeaderSetCookie:
+		h.mulHeader = append(h.mulHeader, appendResponseCookieBytes(nil, h.cookies))
+	default:
+		h.mulHeader = peekAllArgBytesToDst(h.mulHeader, h.h, key)
+	}
+	return h.mulHeader
+}
+
+// PeekAll returns all header value for the given key.
+//
+// The returned value is valid until the request is released,
+// either though ReleaseRequest or your request handler returning.
+// Any future calls to the Peek* will modify the returned value.
+// Do not store references to returned value. Use RequestHeader.GetAll(key) instead.
+func (h *RequestHeader) PeekAll(key string) [][]byte {
+	k := getHeaderKeyBytes(&h.bufKV, key, h.disableNormalizing)
+	return h.peekAll(k)
+}
+
+func (h *RequestHeader) peekAll(key []byte) [][]byte {
+	h.mulHeader = h.mulHeader[:0]
+	switch string(key) {
+	case consts.HeaderHost:
+		if host := h.Host(); len(host) > 0 {
+			h.mulHeader = append(h.mulHeader, host)
+		}
+	case consts.HeaderContentType:
+		if contentType := h.ContentType(); len(contentType) > 0 {
+			h.mulHeader = append(h.mulHeader, contentType)
+		}
+	case consts.HeaderUserAgent:
+		if ua := h.UserAgent(); len(ua) > 0 {
+			h.mulHeader = append(h.mulHeader, ua)
+		}
+	case consts.HeaderConnection:
+		if h.ConnectionClose() {
+			h.mulHeader = append(h.mulHeader, bytestr.StrClose)
+		} else {
+			h.mulHeader = peekAllArgBytesToDst(h.mulHeader, h.h, key)
+		}
+	case consts.HeaderContentLength:
+		h.mulHeader = append(h.mulHeader, h.contentLengthBytes)
+	case consts.HeaderCookie:
+		if h.cookiesCollected {
+			h.mulHeader = append(h.mulHeader, appendRequestCookieBytes(nil, h.cookies))
+		} else {
+			h.mulHeader = peekAllArgBytesToDst(h.mulHeader, h.h, key)
+		}
+	default:
+		h.mulHeader = peekAllArgBytesToDst(h.mulHeader, h.h, key)
+	}
+	return h.mulHeader
+}
+
 // SetContentTypeBytes sets Content-Type header value.
 func (h *ResponseHeader) SetContentTypeBytes(contentType []byte) {
 	h.contentType = append(h.contentType[:0], contentType...)
@@ -1299,6 +1390,7 @@ func (h *RequestHeader) ResetSkipNormalize() {
 	h.cookiesCollected = false
 
 	h.rawHeaders = h.rawHeaders[:0]
+	h.mulHeader = h.mulHeader[:0]
 }
 
 func peekRawHeader(buf, key []byte) []byte {
@@ -1482,6 +1574,28 @@ func (h *ResponseHeader) Get(key string) string {
 	return string(h.Peek(key))
 }
 
+// GetAll returns all header value for the given key
+// it is concurrent safety and long lifetime.
+func (h *RequestHeader) GetAll(key string) []string {
+	res := make([]string, 0)
+	headers := h.PeekAll(key)
+	for _, header := range headers {
+		res = append(res, string(header))
+	}
+	return res
+}
+
+// GetAll returns all header value for the given key and is concurrent safety.
+// it is concurrent safety and long lifetime.
+func (h *ResponseHeader) GetAll(key string) []string {
+	res := make([]string, 0)
+	headers := h.PeekAll(key)
+	for _, header := range headers {
+		res = append(res, string(header))
+	}
+	return res
+}
+
 func appendHeaderLine(dst, key, value []byte) []byte {
 	dst = append(dst, key...)
 	dst = append(dst, bytestr.StrColonSpace...)
diff --git a/pkg/protocol/uri_windows.go b/pkg/protocol/uri_windows.go
index 774644b..abf13e7 100644
--- a/pkg/protocol/uri_windows.go
+++ b/pkg/protocol/uri_windows.go
@@ -46,7 +46,8 @@ package protocol
 
 func addLeadingSlash(dst, src []byte) []byte {
 	// zero length and "C:/" case
-	if len(src) == 0 || (len(src) > 2 && src[1] != ':') {
+	isDisk := len(src) > 2 && src[1] == ':'
+	if len(src) == 0 || (!isDisk && src[0] != '/') {
 		dst = append(dst, '/')
 	}
 
diff --git a/version.go b/version.go
index b57f3fe..98ad92c 100644
--- a/version.go
+++ b/version.go
@@ -19,5 +19,5 @@ package hertz
 // Name and Version info of this framework, used for statistics and debug
 const (
 	Name    = "Hertz"
-	Version = "v0.5.1"
+	Version = "v0.5.2"
 )


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 0e42b00..1adafe4 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -22,18 +22,18 @@ jobs:
           key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
           restore-keys: |
             ${{ runner.os }}-go-
-      - name: Lint
-        run: |
-          go vet -stdmethods=false $(go list ./...)
-          go install mvdan.cc/gofumpt@v0.2.0
-          test -z "$(gofumpt -l -extra .)"
+      - name: Golangci Lint
+        # https://golangci-lint.run/
+        uses: golangci/golangci-lint-action@v3
+        with:
+          version: latest
       - name: Unit Test
         run: go test -race -covermode=atomic -coverprofile=coverage.txt ./...
 
       - name: Codecov
         run: bash <(curl -s https://codecov.io/bash)
 
-  lint-and-ut-windows:
+  ut-windows:
     strategy:
       matrix:
         version: [ 1.18, 1.19 ]
@@ -52,13 +52,5 @@ jobs:
           key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
           restore-keys: |
             ${{ runner.os }}-go-
-      - name: Lint
-        run: |
-          go vet -stdmethods=false $(go list ./...)
-          go install mvdan.cc/gofumpt@v0.2.0
-          test -z "$(gofumpt -l -extra .)"
       - name: Unit Test
-        run: go test -race -covermode=atomic -coverprofile=coverage.txt ./...
-#
-#      - name: Codecov
-#        run: bash <(curl -s https://codecov.io/bash)
+        run: go test -race -covermode=atomic  ./...
diff --git a/pkg/app/client/client_test.go b/pkg/app/client/client_test.go
index 2798f12..23b1eea 100644
--- a/pkg/app/client/client_test.go
+++ b/pkg/app/client/client_test.go
@@ -48,7 +48,6 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"math/rand"
 	"net"
 	"net/http"
 	"net/http/httptest"
@@ -70,7 +69,6 @@ import (
 	"github.com/cloudwego/hertz/pkg/common/test/assert"
 	"github.com/cloudwego/hertz/pkg/network"
 	"github.com/cloudwego/hertz/pkg/network/dialer"
-	"github.com/cloudwego/hertz/pkg/network/netpoll"
 	"github.com/cloudwego/hertz/pkg/network/standard"
 	"github.com/cloudwego/hertz/pkg/protocol"
 	"github.com/cloudwego/hertz/pkg/protocol/consts"
@@ -1897,24 +1895,13 @@ func (m *mockDialer) DialConnection(network, address string, timeout time.Durati
 	return m.Dialer.DialConnection(m.network, m.address, m.timeout, tlsConfig)
 }
 
-func newMockDialerWithCustomFunc(network, address string, timeout time.Duration, f func(network, address string, timeout time.Duration, tlsConfig *tls.Config)) network.Dialer {
-	dialer := standard.NewDialer()
-	if rand.Intn(2) == 0 {
-		dialer = netpoll.NewDialer()
-	}
-	return &mockDialer{
-		Dialer:           dialer,
-		customDialerFunc: f,
-		network:          network,
-		address:          address,
-		timeout:          timeout,
-	}
-}
-
 func TestClientRetry(t *testing.T) {
 	t.Parallel()
 	client, err := NewClient(
-		WithDialTimeout(2*time.Second),
+		// Default dial function performs different in different os. So unit the performance of dial function.
+		WithDialFunc(func(addr string) (network.Conn, error) {
+			return nil, fmt.Errorf("dial tcp %s: i/o timeout", addr)
+		}),
 		WithRetryConfig(
 			retry.WithMaxAttemptTimes(3),
 			retry.WithInitDelay(100*time.Millisecond),
@@ -1943,7 +1930,9 @@ func TestClientRetry(t *testing.T) {
 	}
 
 	client2, err := NewClient(
-		WithDialTimeout(2*time.Second),
+		WithDialFunc(func(addr string) (network.Conn, error) {
+			return nil, fmt.Errorf("dial tcp %s: i/o timeout", addr)
+		}),
 		WithRetryConfig(
 			retry.WithMaxAttemptTimes(2),
 			retry.WithInitDelay(500*time.Millisecond),
@@ -1972,7 +1961,9 @@ func TestClientRetry(t *testing.T) {
 	}
 
 	client3, err := NewClient(
-		WithDialTimeout(2*time.Second),
+		WithDialFunc(func(addr string) (network.Conn, error) {
+			return nil, fmt.Errorf("dial tcp %s: i/o timeout", addr)
+		}),
 		WithRetryConfig(
 			retry.WithMaxAttemptTimes(2),
 			retry.WithInitDelay(100*time.Millisecond),
@@ -2002,7 +1993,9 @@ func TestClientRetry(t *testing.T) {
 	}
 
 	client4, err := NewClient(
-		WithDialTimeout(2*time.Second),
+		WithDialFunc(func(addr string) (network.Conn, error) {
+			return nil, fmt.Errorf("dial tcp %s: i/o timeout", addr)
+		}),
 		WithRetryConfig(
 			retry.WithMaxAttemptTimes(2),
 			retry.WithInitDelay(1*time.Second),
@@ -2045,12 +2038,12 @@ func TestClientDialerName(t *testing.T) {
 		t.Errorf("expected 'netpoll', but get %s", dName)
 	}
 
-	client, _ = NewClient(WithDialer(netpoll.NewDialer()))
+	client, _ = NewClient(WithDialer(&mockDialer{}))
 	dName, err = client.GetDialerName()
 	if err != nil {
 		t.Fatalf("unexpected error: %v", err)
 	}
-	if dName != "netpoll" {
+	if dName != "client" {
 		t.Errorf("expected 'standard', but get %s", dName)
 	}
 
diff --git a/pkg/app/client/client_unix_test.go b/pkg/app/client/client_unix_test.go
new file mode 100644
index 0000000..94869e8
--- /dev/null
+++ b/pkg/app/client/client_unix_test.go
@@ -0,0 +1,43 @@
+// Copyright 2023 CloudWeGo Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
+// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris
+
+package client
+
+import (
+	"crypto/tls"
+	"math/rand"
+	"time"
+
+	"github.com/cloudwego/hertz/pkg/network"
+	"github.com/cloudwego/hertz/pkg/network/netpoll"
+	"github.com/cloudwego/hertz/pkg/network/standard"
+)
+
+func newMockDialerWithCustomFunc(network, address string, timeout time.Duration, f func(network, address string, timeout time.Duration, tlsConfig *tls.Config)) network.Dialer {
+	dialer := standard.NewDialer()
+	if rand.Intn(2) == 0 {
+		dialer = netpoll.NewDialer()
+	}
+	return &mockDialer{
+		Dialer:           dialer,
+		customDialerFunc: f,
+		network:          network,
+		address:          address,
+		timeout:          timeout,
+	}
+}
diff --git a/pkg/app/client/client_windows_test.go b/pkg/app/client/client_windows_test.go
new file mode 100644
index 0000000..63e60fd
--- /dev/null
+++ b/pkg/app/client/client_windows_test.go
@@ -0,0 +1,38 @@
+// Copyright 2023 CloudWeGo Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+//go:build windows
+// +build windows
+
+package client
+
+import (
+	"crypto/tls"
+	"time"
+
+	"github.com/cloudwego/hertz/pkg/network"
+	"github.com/cloudwego/hertz/pkg/network/standard"
+)
+
+func newMockDialerWithCustomFunc(network, address string, timeout time.Duration, f func(network, address string, timeout time.Duration, tlsConfig *tls.Config)) network.Dialer {
+	dialer := standard.NewDialer()
+	return &mockDialer{
+		Dialer:           dialer,
+		customDialerFunc: f,
+		network:          network,
+		address:          address,
+		timeout:          timeout,
+	}
+}
diff --git a/pkg/app/server/hertz_test.go b/pkg/app/server/hertz_test.go
index de6a593..9df769a 100644
--- a/pkg/app/server/hertz_test.go
+++ b/pkg/app/server/hertz_test.go
@@ -25,13 +25,9 @@ import (
 	"io"
 	"net"
 	"net/http"
-	"os"
-	"os/exec"
-	"strconv"
 	"strings"
 	"sync"
 	"sync/atomic"
-	"syscall"
 	"testing"
 	"time"
 
@@ -49,7 +45,6 @@ import (
 	"github.com/cloudwego/hertz/pkg/protocol/consts"
 	"github.com/cloudwego/hertz/pkg/protocol/http1/req"
 	"github.com/cloudwego/hertz/pkg/protocol/http1/resp"
-	"golang.org/x/sys/unix"
 )
 
 func TestHertz_Run(t *testing.T) {
@@ -147,64 +142,6 @@ func TestHertz_GracefulShutdown(t *testing.T) {
 	cancel()
 }
 
-func TestHertz_Spin(t *testing.T) {
-	engine := New(WithHostPorts("127.0.0.1:6668"))
-	engine.GET("/test", func(c context.Context, ctx *app.RequestContext) {
-		time.Sleep(time.Second * 2)
-		path := ctx.Request.URI().PathOriginal()
-		ctx.SetBodyString(string(path))
-	})
-	engine.GET("/test2", func(c context.Context, ctx *app.RequestContext) {})
-
-	testint := uint32(0)
-	engine.Engine.OnShutdown = append(engine.OnShutdown, func(ctx context.Context) {
-		atomic.StoreUint32(&testint, 1)
-	})
-
-	go engine.Spin()
-	time.Sleep(time.Millisecond)
-
-	hc := http.Client{Timeout: time.Second}
-	var err error
-	var resp *http.Response
-	ch := make(chan struct{})
-	ch2 := make(chan struct{})
-	go func() {
-		ticker := time.NewTicker(time.Millisecond * 100)
-		defer ticker.Stop()
-		for range ticker.C {
-			_, err := hc.Get("http://127.0.0.1:6668/test2")
-			t.Logf("[%v]begin listening\n", time.Now())
-			if err != nil {
-				t.Logf("[%v]listening closed: %v", time.Now(), err)
-				ch2 <- struct{}{}
-				break
-			}
-		}
-	}()
-	go func() {
-		t.Logf("[%v]begin request\n", time.Now())
-		resp, err = http.Get("http://127.0.0.1:6668/test")
-		t.Logf("[%v]end request\n", time.Now())
-		ch <- struct{}{}
-	}()
-
-	time.Sleep(time.Second * 1)
-	pid := strconv.Itoa(os.Getpid())
-	cmd := exec.Command("kill", "-SIGHUP", pid)
-	t.Logf("[%v]begin SIGHUP\n", time.Now())
-	if err := cmd.Run(); err != nil {
-		t.Fatal(err)
-	}
-	t.Logf("[%v]end SIGHUP\n", time.Now())
-	<-ch
-	assert.Nil(t, err)
-	assert.NotNil(t, resp)
-	assert.DeepEqual(t, uint32(1), atomic.LoadUint32(&testint))
-
-	<-ch2
-}
-
 func TestLoadHTMLGlob(t *testing.T) {
 	engine := New(WithMaxRequestBodySize(15), WithHostPorts("127.0.0.1:8890"))
 	engine.Delims("{[{", "}]}")
@@ -220,7 +157,9 @@ func TestLoadHTMLGlob(t *testing.T) {
 	assert.DeepEqual(t, consts.StatusOK, resp.StatusCode)
 	b := make([]byte, 100)
 	n, _ := resp.Body.Read(b)
-	assert.DeepEqual(t, "<html>\n<h1>\n    Main website\n</h1>\n</html>", string(b[0:n]))
+	const expected = `<html><h1>Main website</h1></html>`
+
+	assert.DeepEqual(t, expected, string(b[0:n]))
 }
 
 func TestLoadHTMLFiles(t *testing.T) {
@@ -415,8 +354,6 @@ func TestEnoughBodySize(t *testing.T) {
 }
 
 func TestRequestCtxHijack(t *testing.T) {
-	t.Parallel()
-
 	hijackStartCh := make(chan struct{})
 	hijackStopCh := make(chan struct{})
 	engine := New()
@@ -595,44 +532,6 @@ func TestDuplicateReleaseBodyStream(t *testing.T) {
 	wg.Wait()
 }
 
-func TestReusePorts(t *testing.T) {
-	cfg := &net.ListenConfig{Control: func(network, address string, c syscall.RawConn) error {
-		return c.Control(func(fd uintptr) {
-			syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, unix.SO_REUSEADDR, 1)
-			syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, unix.SO_REUSEPORT, 1)
-		})
-	}}
-	ha := New(WithHostPorts("localhost:10093"), WithListenConfig(cfg), WithTransport(standard.NewTransporter))
-	hb := New(WithHostPorts("localhost:10093"), WithListenConfig(cfg), WithTransport(standard.NewTransporter))
-	hc := New(WithHostPorts("localhost:10093"), WithListenConfig(cfg))
-	hd := New(WithHostPorts("localhost:10093"), WithListenConfig(cfg))
-	ha.GET("/ping", func(c context.Context, ctx *app.RequestContext) {
-		ctx.JSON(consts.StatusOK, utils.H{"ping": "pong"})
-	})
-	hc.GET("/ping", func(c context.Context, ctx *app.RequestContext) {
-		ctx.JSON(consts.StatusOK, utils.H{"ping": "pong"})
-	})
-	hd.GET("/ping", func(c context.Context, ctx *app.RequestContext) {
-		ctx.JSON(consts.StatusOK, utils.H{"ping": "pong"})
-	})
-	hb.GET("/ping", func(c context.Context, ctx *app.RequestContext) {
-		ctx.JSON(consts.StatusOK, utils.H{"ping": "pong"})
-	})
-	go ha.Run()
-	go hb.Run()
-	go hc.Run()
-	go hd.Run()
-	time.Sleep(time.Second)
-
-	client, _ := c.NewClient()
-	for i := 0; i < 1000; i++ {
-		statusCode, body, err := client.Get(context.Background(), nil, "http://localhost:10093/ping")
-		assert.Nil(t, err)
-		assert.DeepEqual(t, consts.StatusOK, statusCode)
-		assert.DeepEqual(t, "{\"ping\":\"pong\"}", string(body))
-	}
-}
-
 func TestServiceRegisterFailed(t *testing.T) {
 	mockRegErr := errors.New("mock register error")
 	var rCount int32
@@ -783,6 +682,10 @@ func TestReuseCtx(t *testing.T) {
 	}
 }
 
+type CloseWithoutResetBuffer interface {
+	CloseNoResetBuffer() error
+}
+
 func TestOnprepare(t *testing.T) {
 	h := New(
 		WithHostPorts("localhost:9229"),
@@ -790,7 +693,11 @@ func TestOnprepare(t *testing.T) {
 			b, err := conn.Peek(3)
 			assert.Nil(t, err)
 			assert.DeepEqual(t, string(b), "GET")
-			conn.Close()
+			if c, ok := conn.(CloseWithoutResetBuffer); ok {
+				c.CloseNoResetBuffer()
+			} else {
+				conn.Close()
+			}
 			return ctx
 		}))
 	h.GET("/ping", func(ctx context.Context, c *app.RequestContext) {
diff --git a/pkg/app/server/hertz_unix_test.go b/pkg/app/server/hertz_unix_test.go
new file mode 100644
index 0000000..b37ddfb
--- /dev/null
+++ b/pkg/app/server/hertz_unix_test.go
@@ -0,0 +1,136 @@
+// Copyright 2023 CloudWeGo Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
+// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris
+
+package server
+
+import (
+	"context"
+	"net"
+	"net/http"
+	"os"
+	"os/exec"
+	"strconv"
+	"sync/atomic"
+	"syscall"
+	"testing"
+	"time"
+
+	"github.com/cloudwego/hertz/pkg/app"
+	c "github.com/cloudwego/hertz/pkg/app/client"
+	"github.com/cloudwego/hertz/pkg/common/test/assert"
+	"github.com/cloudwego/hertz/pkg/common/utils"
+	"github.com/cloudwego/hertz/pkg/network/standard"
+	"github.com/cloudwego/hertz/pkg/protocol/consts"
+	"golang.org/x/sys/unix"
+)
+
+func TestReusePorts(t *testing.T) {
+	cfg := &net.ListenConfig{Control: func(network, address string, c syscall.RawConn) error {
+		return c.Control(func(fd uintptr) {
+			syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, unix.SO_REUSEADDR, 1)
+			syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, unix.SO_REUSEPORT, 1)
+		})
+	}}
+	ha := New(WithHostPorts("localhost:10093"), WithListenConfig(cfg), WithTransport(standard.NewTransporter))
+	hb := New(WithHostPorts("localhost:10093"), WithListenConfig(cfg), WithTransport(standard.NewTransporter))
+	hc := New(WithHostPorts("localhost:10093"), WithListenConfig(cfg))
+	hd := New(WithHostPorts("localhost:10093"), WithListenConfig(cfg))
+	ha.GET("/ping", func(c context.Context, ctx *app.RequestContext) {
+		ctx.JSON(consts.StatusOK, utils.H{"ping": "pong"})
+	})
+	hc.GET("/ping", func(c context.Context, ctx *app.RequestContext) {
+		ctx.JSON(consts.StatusOK, utils.H{"ping": "pong"})
+	})
+	hd.GET("/ping", func(c context.Context, ctx *app.RequestContext) {
+		ctx.JSON(consts.StatusOK, utils.H{"ping": "pong"})
+	})
+	hb.GET("/ping", func(c context.Context, ctx *app.RequestContext) {
+		ctx.JSON(consts.StatusOK, utils.H{"ping": "pong"})
+	})
+	go ha.Run()
+	go hb.Run()
+	go hc.Run()
+	go hd.Run()
+	time.Sleep(time.Second)
+
+	client, _ := c.NewClient()
+	for i := 0; i < 1000; i++ {
+		statusCode, body, err := client.Get(context.Background(), nil, "http://localhost:10093/ping")
+		assert.Nil(t, err)
+		assert.DeepEqual(t, consts.StatusOK, statusCode)
+		assert.DeepEqual(t, "{\"ping\":\"pong\"}", string(body))
+	}
+}
+
+func TestHertz_Spin(t *testing.T) {
+	engine := New(WithHostPorts("127.0.0.1:6668"))
+	engine.GET("/test", func(c context.Context, ctx *app.RequestContext) {
+		time.Sleep(time.Second * 2)
+		path := ctx.Request.URI().PathOriginal()
+		ctx.SetBodyString(string(path))
+	})
+	engine.GET("/test2", func(c context.Context, ctx *app.RequestContext) {})
+
+	testint := uint32(0)
+	engine.Engine.OnShutdown = append(engine.OnShutdown, func(ctx context.Context) {
+		atomic.StoreUint32(&testint, 1)
+	})
+
+	go engine.Spin()
+	time.Sleep(time.Millisecond)
+
+	hc := http.Client{Timeout: time.Second}
+	var err error
+	var resp *http.Response
+	ch := make(chan struct{})
+	ch2 := make(chan struct{})
+	go func() {
+		ticker := time.NewTicker(time.Millisecond * 100)
+		defer ticker.Stop()
+		for range ticker.C {
+			_, err := hc.Get("http://127.0.0.1:6668/test2")
+			t.Logf("[%v]begin listening\n", time.Now())
+			if err != nil {
+				t.Logf("[%v]listening closed: %v", time.Now(), err)
+				ch2 <- struct{}{}
+				break
+			}
+		}
+	}()
+	go func() {
+		t.Logf("[%v]begin request\n", time.Now())
+		resp, err = http.Get("http://127.0.0.1:6668/test")
+		t.Logf("[%v]end request\n", time.Now())
+		ch <- struct{}{}
+	}()
+
+	time.Sleep(time.Second * 1)
+	pid := strconv.Itoa(os.Getpid())
+	cmd := exec.Command("kill", "-SIGHUP", pid)
+	t.Logf("[%v]begin SIGHUP\n", time.Now())
+	if err := cmd.Run(); err != nil {
+		t.Fatal(err)
+	}
+	t.Logf("[%v]end SIGHUP\n", time.Now())
+	<-ch
+	assert.Nil(t, err)
+	assert.NotNil(t, resp)
+	assert.DeepEqual(t, uint32(1), atomic.LoadUint32(&testint))
+
+	<-ch2
+}
diff --git a/pkg/app/server/render/html_test.go b/pkg/app/server/render/html_test.go
index 86fa981..aca0966 100644
--- a/pkg/app/server/render/html_test.go
+++ b/pkg/app/server/render/html_test.go
@@ -23,7 +23,7 @@ import (
 )
 
 func TestHTMLDebug_StartChecker_timer(t *testing.T) {
-	render := &HTMLDebug{RefreshInterval: 2 * time.Second}
+	render := &HTMLDebug{RefreshInterval: time.Second}
 	select {
 	case <-render.reloadCh:
 		t.Fatalf("should not be triggered")
@@ -31,17 +31,9 @@ func TestHTMLDebug_StartChecker_timer(t *testing.T) {
 	}
 	render.startChecker()
 	select {
-	case <-time.After(50 * time.Millisecond):
-		t.Fatalf("should be triggered immediately")
+	case <-time.After(render.RefreshInterval + 100*time.Millisecond):
+		t.Fatalf("should be triggered in 1 second")
 	case <-render.reloadCh:
-
-	}
-	select {
-	// some ci servers have poor computing power, so we add some extra time here.
-	case <-time.After(2*time.Second + 50*time.Millisecond):
-		t.Fatalf("should be triggered before 2 seconds")
-	case <-render.reloadCh:
-		t.Logf("paas")
 	}
 }
 
@@ -59,7 +51,7 @@ func TestHTMLDebug_StartChecker_fs_watcher(t *testing.T) {
 	}
 	render.startChecker()
 	f.Write([]byte("hello"))
-
+	f.Sync()
 	select {
 	case <-time.After(50 * time.Millisecond):
 		t.Fatalf("should be triggered immediately")
diff --git a/pkg/common/testdata/template/index.tmpl b/pkg/common/testdata/template/index.tmpl
index 5e5a261..5100785 100644
--- a/pkg/common/testdata/template/index.tmpl
+++ b/pkg/common/testdata/template/index.tmpl
@@ -1,5 +1,1 @@
-<html>
-<h1>
-    {[{ .title }]}
-</h1>
-</html>
\ No newline at end of file
+<html><h1>{[{ .title }]}</h1></html>
\ No newline at end of file
diff --git a/pkg/network/netpoll/connection_test.go b/pkg/network/netpoll/connection_test.go
index 893b328..2661056 100644
--- a/pkg/network/netpoll/connection_test.go
+++ b/pkg/network/netpoll/connection_test.go
@@ -1,18 +1,20 @@
-/*
- * Copyright 2022 CloudWeGo Authors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
+// Copyright 2022 CloudWeGo Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+//go:build !windows
+// +build !windows
 
 package netpoll
 
diff --git a/pkg/network/utils_test.go b/pkg/network/utils_test.go
index 73ef33b..0d71399 100644
--- a/pkg/network/utils_test.go
+++ b/pkg/network/utils_test.go
@@ -18,10 +18,15 @@ package network
 
 import (
 	"os"
+	"runtime"
 	"testing"
 )
 
 func TestUnlinkUdsFile(t *testing.T) {
+	if runtime.GOOS == "windows" {
+		t.SkipNow()
+	}
+
 	tmp := "tmpFile"
 	var err error
 
diff --git a/pkg/protocol/header_test.go b/pkg/protocol/header_test.go
index 0a7baf2..fc58a29 100644
--- a/pkg/protocol/header_test.go
+++ b/pkg/protocol/header_test.go
@@ -572,3 +572,72 @@ func TestRequestHeaderSetNoDefaultContentType(t *testing.T) {
 	b = h.AppendBytes(nil)
 	assert.DeepEqual(t, b, []byte("POST / HTTP/1.1\r\n\r\n"))
 }
+
+func TestRequestHeader_PeekAll(t *testing.T) {
+	t.Parallel()
+	h := &RequestHeader{}
+	h.Add(consts.HeaderConnection, "keep-alive")
+	h.Add("Content-Type", "aaa")
+	h.Add(consts.HeaderHost, "aaabbb")
+	h.Add("User-Agent", "asdfas")
+	h.Add("Content-Length", "1123")
+	h.Add("Cookie", "foobar=baz")
+	h.Add("aaa", "aaa")
+	h.Add("aaa", "bbb")
+
+	expectRequestHeaderAll(t, h, consts.HeaderConnection, [][]byte{[]byte("keep-alive")})
+	expectRequestHeaderAll(t, h, "Content-Type", [][]byte{[]byte("aaa")})
+	expectRequestHeaderAll(t, h, consts.HeaderHost, [][]byte{[]byte("aaabbb")})
+	expectRequestHeaderAll(t, h, "User-Agent", [][]byte{[]byte("asdfas")})
+	expectRequestHeaderAll(t, h, "Content-Length", [][]byte{[]byte("1123")})
+	expectRequestHeaderAll(t, h, "Cookie", [][]byte{[]byte("foobar=baz")})
+	expectRequestHeaderAll(t, h, "aaa", [][]byte{[]byte("aaa"), []byte("bbb")})
+
+	h.DelBytes([]byte("Content-Type"))
+	h.DelBytes([]byte((consts.HeaderHost)))
+	h.DelBytes([]byte("aaa"))
+	expectRequestHeaderAll(t, h, "Content-Type", [][]byte{})
+	expectRequestHeaderAll(t, h, consts.HeaderHost, [][]byte{})
+	expectRequestHeaderAll(t, h, "aaa", [][]byte{})
+}
+
+func expectRequestHeaderAll(t *testing.T, h *RequestHeader, key string, expectedValue [][]byte) {
+	if len(h.PeekAll(key)) != len(expectedValue) {
+		t.Fatalf("Unexpected size for key %q: %d. Expected %d", key, len(h.PeekAll(key)), len(expectedValue))
+	}
+	assert.DeepEqual(t, h.PeekAll(key), expectedValue)
+}
+
+func TestResponseHeader_PeekAll(t *testing.T) {
+	t.Parallel()
+
+	h := &ResponseHeader{}
+	h.Add(consts.HeaderContentType, "aaa/bbb")
+	h.Add(consts.HeaderContentEncoding, "gzip")
+	h.Add(consts.HeaderConnection, "close")
+	h.Add(consts.HeaderContentLength, "1234")
+	h.Add(consts.HeaderServer, "aaaa")
+	h.Add(consts.HeaderSetCookie, "cccc")
+	h.Add("aaa", "aaa")
+	h.Add("aaa", "bbb")
+
+	expectResponseHeaderAll(t, h, consts.HeaderContentType, [][]byte{[]byte("aaa/bbb")})
+	expectResponseHeaderAll(t, h, consts.HeaderContentEncoding, [][]byte{[]byte("gzip")})
+	expectResponseHeaderAll(t, h, consts.HeaderConnection, [][]byte{[]byte("close")})
+	expectResponseHeaderAll(t, h, consts.HeaderContentLength, [][]byte{[]byte("1234")})
+	expectResponseHeaderAll(t, h, consts.HeaderServer, [][]byte{[]byte("aaaa")})
+	expectResponseHeaderAll(t, h, consts.HeaderSetCookie, [][]byte{[]byte("cccc")})
+	expectResponseHeaderAll(t, h, "aaa", [][]byte{[]byte("aaa"), []byte("bbb")})
+
+	h.Del(consts.HeaderContentType)
+	h.Del(consts.HeaderContentEncoding)
+	expectResponseHeaderAll(t, h, consts.HeaderContentType, [][]byte{bytestr.DefaultContentType})
+	expectResponseHeaderAll(t, h, consts.HeaderContentEncoding, [][]byte{})
+}
+
+func expectResponseHeaderAll(t *testing.T, h *ResponseHeader, key string, expectedValue [][]byte) {
+	if len(h.PeekAll(key)) != len(expectedValue) {
+		t.Fatalf("Unexpected size for key %q: %d. Expected %d", key, len(h.PeekAll(key)), len(expectedValue))
+	}
+	assert.DeepEqual(t, h.PeekAll(key), expectedValue)
+}
diff --git a/pkg/protocol/uri_test.go b/pkg/protocol/uri_test.go
index 9588734..e245faf 100644
--- a/pkg/protocol/uri_test.go
+++ b/pkg/protocol/uri_test.go
@@ -44,6 +44,7 @@ package protocol
 import (
 	"path/filepath"
 	"reflect"
+	"runtime"
 	"testing"
 
 	"github.com/cloudwego/hertz/pkg/common/test/assert"
@@ -374,6 +375,67 @@ func TestParsePathWindows(t *testing.T) {
 	testParsePathWindows(t, "/..%5c..%5cfoo", "/foo")
 }
 
+func TestURIPathNormalize(t *testing.T) {
+	if runtime.GOOS == "windows" {
+		t.SkipNow()
+	}
+
+	t.Parallel()
+
+	var u URI
+
+	// double slash
+	testURIPathNormalize(t, &u, "/aa//bb", "/aa/bb")
+
+	// triple slash
+	testURIPathNormalize(t, &u, "/x///y/", "/x/y/")
+
+	// multi slashes
+	testURIPathNormalize(t, &u, "/abc//de///fg////", "/abc/de/fg/")
+
+	// encoded slashes
+	testURIPathNormalize(t, &u, "/xxxx%2fyyy%2f%2F%2F", "/xxxx/yyy/")
+
+	// dotdot
+	testURIPathNormalize(t, &u, "/aaa/..", "/")
+
+	// dotdot with trailing slash
+	testURIPathNormalize(t, &u, "/xxx/yyy/../", "/xxx/")
+
+	// multi dotdots
+	testURIPathNormalize(t, &u, "/aaa/bbb/ccc/../../ddd", "/aaa/ddd")
+
+	// dotdots separated by other data
+	testURIPathNormalize(t, &u, "/a/b/../c/d/../e/..", "/a/c/")
+
+	// too many dotdots
+	testURIPathNormalize(t, &u, "/aaa/../../../../xxx", "/xxx")
+	testURIPathNormalize(t, &u, "/../../../../../..", "/")
+	testURIPathNormalize(t, &u, "/../../../../../../", "/")
+
+	// encoded dotdots
+	testURIPathNormalize(t, &u, "/aaa%2Fbbb%2F%2E.%2Fxxx", "/aaa/xxx")
+
+	// double slash with dotdots
+	testURIPathNormalize(t, &u, "/aaa////..//b", "/b")
+
+	// fake dotdot
+	testURIPathNormalize(t, &u, "/aaa/..bbb/ccc/..", "/aaa/..bbb/")
+
+	// single dot
+	testURIPathNormalize(t, &u, "/a/./b/././c/./d.html", "/a/b/c/d.html")
+	testURIPathNormalize(t, &u, "./foo/", "/foo/")
+	testURIPathNormalize(t, &u, "./../.././../../aaa/bbb/../../../././../", "/")
+	testURIPathNormalize(t, &u, "./a/./.././../b/./foo.html", "/b/foo.html")
+}
+
+func testURIPathNormalize(t *testing.T, u *URI, requestURI, expectedPath string) {
+	u.Parse(nil, []byte(requestURI)) //nolint:errcheck
+	if string(u.Path()) != expectedPath {
+		t.Fatalf("Unexpected path %q. Expected %q. requestURI=%q", u.Path(), expectedPath, requestURI)
+	}
+}
+
 func testParsePathWindows(t *testing.T, path, expectedPath string) {
 	var u URI
 	u.Parse(nil, []byte(path))
diff --git a/pkg/protocol/uri_windows_test.go b/pkg/protocol/uri_windows_test.go
new file mode 100644
index 0000000..507924b
--- /dev/null
+++ b/pkg/protocol/uri_windows_test.go
@@ -0,0 +1,28 @@
+// Copyright 2023 CloudWeGo Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package protocol
+
+import "testing"
+
+func TestURIPathNormalizeIssue86(t *testing.T) {
+	t.Parallel()
+
+	var u URI
+
+	testURIPathNormalize(t, &u, `a`, `/a`)
+	testURIPathNormalize(t, &u, "/../../../../../foo", "/foo")
+	testURIPathNormalize(t, &u, "/..\\..\\..\\..\\..\\", "/")
+	testURIPathNormalize(t, &u, "/..%5c..%5cfoo", "/foo")
+}
diff --git a/pkg/route/engine_test.go b/pkg/route/engine_test.go
index f9f1528..d73bc8c 100644
--- a/pkg/route/engine_test.go
+++ b/pkg/route/engine_test.go
@@ -59,7 +59,6 @@ import (
 	"github.com/cloudwego/hertz/pkg/common/test/assert"
 	"github.com/cloudwego/hertz/pkg/common/test/mock"
 	"github.com/cloudwego/hertz/pkg/network"
-	"github.com/cloudwego/hertz/pkg/network/netpoll"
 	"github.com/cloudwego/hertz/pkg/network/standard"
 	"github.com/cloudwego/hertz/pkg/protocol/consts"
 )
@@ -75,16 +74,16 @@ func TestNew_Engine(t *testing.T) {
 }
 
 func TestNew_Engine_WithTransporter(t *testing.T) {
-	defaultTransporter = netpoll.NewTransporter
+	defaultTransporter = newMockTransporter
 	opt := config.NewOptions([]config.Option{})
 	router := NewEngine(opt)
-	assert.DeepEqual(t, "netpoll", router.GetTransporterName())
+	assert.DeepEqual(t, "route", router.GetTransporterName())
 
-	defaultTransporter = netpoll.NewTransporter
+	defaultTransporter = newMockTransporter
 	opt.TransporterNewer = standard.NewTransporter
 	router = NewEngine(opt)
 	assert.DeepEqual(t, "standard", router.GetTransporterName())
-	assert.DeepEqual(t, "netpoll", GetTransporterName())
+	assert.DeepEqual(t, "route", GetTransporterName())
 }
 
 func TestGetTransporterName(t *testing.T) {
@@ -286,8 +285,8 @@ func TestIdleTimeout03(t *testing.T) {
 			t.Errorf("err should be ErrShortConnection, but got %s", err)
 		}
 		return
-	case <-time.Tick(120 * time.Millisecond):
-		t.Errorf("timeout! should have been finished in 120ms...")
+	case <-time.Tick(200 * time.Millisecond):
+		t.Errorf("timeout! should have been finished in 200ms...")
 	}
 }
 
@@ -419,18 +418,17 @@ func TestRenderHtml(t *testing.T) {
 }
 
 func TestTransporterName(t *testing.T) {
-	SetTransporter(netpoll.NewTransporter)
-	assert.DeepEqual(t, "netpoll", GetTransporterName())
-
 	SetTransporter(standard.NewTransporter)
 	assert.DeepEqual(t, "standard", GetTransporterName())
 
-	SetTransporter(func(options *config.Options) network.Transporter {
-		return &mockTransporter{}
-	})
+	SetTransporter(newMockTransporter)
 	assert.DeepEqual(t, "route", GetTransporterName())
 }
 
+func newMockTransporter(options *config.Options) network.Transporter {
+	return &mockTransporter{}
+}
+
 type mockTransporter struct{}
 
 func (m *mockTransporter) ListenAndServe(onData network.OnData) (err error) {