Repository: cloudwego/hertz
Pull request #1354 (2025-05-15 02:51:20)

chore: release v0.10.0


Patch:
diff --git a/.codecov.yml b/.codecov.yml
index 5cd2fc3..98ed505 100644
--- a/.codecov.yml
+++ b/.codecov.yml
@@ -1,3 +1,6 @@
+github_checks:
+  annotations: false
+
 ignore:
   - "images/.*"
   - "examples/.*"
diff --git a/.gitignore b/.gitignore
index d4c5801..c84307a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,8 @@
 pkg/app/fs.go.hertz.gz
 coverage.txt
 coverage.out
+# OSX trash
+.DS_Store
 
 # test benchmark tmp output
 cpu.out
diff --git a/cmd/hz/meta/const.go b/cmd/hz/meta/const.go
index a696be8..2364334 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.9.1"
+const Version = "v0.9.7"
 
 const DefaultServiceName = "hertz_service"
 
diff --git a/go.mod b/go.mod
index 3469bd7..d35115e 100644
--- a/go.mod
+++ b/go.mod
@@ -3,15 +3,15 @@ module github.com/cloudwego/hertz
 go 1.19
 
 require (
-	github.com/bytedance/gopkg v0.1.0
+	github.com/bytedance/gopkg v0.1.1
 	github.com/bytedance/mockey v1.2.12
 	github.com/bytedance/sonic v1.13.2
-	github.com/cloudwego/netpoll v0.6.4
+	github.com/cloudwego/netpoll v0.7.0
 	github.com/fsnotify/fsnotify v1.5.4
 	github.com/nyaruka/phonenumbers v1.0.55
-	github.com/stretchr/testify v1.8.1
+	github.com/stretchr/testify v1.9.0
 	github.com/tidwall/gjson v1.14.4
-	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
+	golang.org/x/sync v0.8.0
 	golang.org/x/sys v0.24.0
 	google.golang.org/protobuf v1.27.1
 )
@@ -19,7 +19,8 @@ require (
 require (
 	github.com/bytedance/sonic/loader v0.2.4 // indirect
 	github.com/cloudwego/base64x v0.1.5 // indirect
-	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/cloudwego/gopkg v0.1.4 // indirect
+	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/golang/protobuf v1.5.0 // indirect
 	github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 // indirect
 	github.com/jtolds/gls v4.20.0+incompatible // indirect
@@ -27,7 +28,7 @@ require (
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d // indirect
 	github.com/smartystreets/goconvey v1.6.4 // indirect
-	github.com/stretchr/objx v0.5.0 // indirect
+	github.com/stretchr/objx v0.5.2 // indirect
 	github.com/tidwall/match v1.1.1 // indirect
 	github.com/tidwall/pretty v1.2.0 // indirect
 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
diff --git a/go.sum b/go.sum
index a527451..1fba56c 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,5 @@
-github.com/bytedance/gopkg v0.1.0 h1:aAxB7mm1qms4Wz4sp8e1AtKDOeFLtdqvGiUe7aonRJs=
-github.com/bytedance/gopkg v0.1.0/go.mod h1:FtQG3YbQG9L/91pbKSw787yBQPutC+457AvDW77fgUQ=
+github.com/bytedance/gopkg v0.1.1 h1:3azzgSkiaw79u24a+w9arfH8OfnQQ4MHUt9lJFREEaE=
+github.com/bytedance/gopkg v0.1.1/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
 github.com/bytedance/mockey v1.2.12 h1:aeszOmGw8CPX8CRx1DZ/Glzb1yXvhjDh6jdFBNZjsU4=
 github.com/bytedance/mockey v1.2.12/go.mod h1:3ZA4MQasmqC87Tw0w7Ygdy7eHIc2xgpZ8Pona5rsYIk=
 github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
@@ -9,12 +9,15 @@ github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCN
 github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
 github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
 github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
+github.com/cloudwego/gopkg v0.1.4 h1:EoQiCG4sTonTPHxOGE0VlQs+sQR+Hsi2uN0qqwu8O50=
+github.com/cloudwego/gopkg v0.1.4/go.mod h1:FQuXsRWRsSqJLsMVd5SYzp8/Z1y5gXKnVvRrWUOsCMI=
 github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
-github.com/cloudwego/netpoll v0.6.4 h1:z/dA4sOTUQof6zZIO4QNnLBXsDFFFEos9OOGloR6kno=
-github.com/cloudwego/netpoll v0.6.4/go.mod h1:BtM+GjKTdwKoC8IOzD08/+8eEn2gYoiNLipFca6BVXQ=
+github.com/cloudwego/netpoll v0.7.0 h1:bDrxQaNfijRI1zyGgXHQoE/nYegL0nr+ijO1Norelc4=
+github.com/cloudwego/netpoll v0.7.0/go.mod h1:PI+YrmyS7cIr0+SD4seJz3Eo3ckkXdu2ZVKBLhURLNU=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
 github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
 github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -29,6 +32,11 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV
 github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
 github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
 github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
+github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/nyaruka/phonenumbers v1.0.55 h1:bj0nTO88Y68KeUQ/n3Lo2KgK7lM1hF7L9NFuwcCl3yg=
 github.com/nyaruka/phonenumbers v1.0.55/go.mod h1:sDaTZ/KPX5f8qyV9qN+hIm+4ZBARJrupC6LuhshJq1U=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -39,13 +47,16 @@ github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIK
 github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
-github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
 github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
 github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
@@ -54,31 +65,66 @@ github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
 github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
 github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
 github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
 golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
+golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
-golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
-golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
+golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
+golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
 golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
+golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
+golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
 google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/internal/bytesconv/bytesconv.go b/internal/bytesconv/bytesconv.go
index e2d1ca5..898e222 100644
--- a/internal/bytesconv/bytesconv.go
+++ b/internal/bytesconv/bytesconv.go
@@ -43,7 +43,6 @@ package bytesconv
 
 import (
 	"net/http"
-	"sync"
 	"time"
 	"unsafe"
 
@@ -55,8 +54,6 @@ const (
 	lowerhex = "0123456789abcdef"
 )
 
-var hexIntBufPool sync.Pool
-
 func LowercaseBytes(b []byte) {
 	for i := 0; i < len(b); i++ {
 		p := &b[i]
@@ -89,29 +86,30 @@ func S2b(s string) (b []byte) {
 	return
 }
 
-func WriteHexInt(w network.Writer, n int) error {
-	if n < 0 {
-		panic("BUG: int must be positive")
+func EncodedIntHexLen(n uint64) int {
+	if n == 0 {
+		return 1
+	}
+	i := 0
+	for n > 0 {
+		i++
+		n >>= 4
 	}
+	return i
+}
 
-	v := hexIntBufPool.Get()
-	if v == nil {
-		v = make([]byte, maxHexIntChars+1)
+func AppendIntHex(b []byte, n uint64) []byte {
+	if n == 0 {
+		return append(b, '0')
 	}
-	buf := v.([]byte)
-	i := len(buf) - 1
-	for {
-		buf[i] = lowerhex[n&0xf]
-		n >>= 4
-		if n == 0 {
-			break
-		}
+	var tmp [16]byte // 64 / 4 = 16
+	i := len(tmp)
+	for n > 0 {
 		i--
+		tmp[i] = lowerhex[n&0xf]
+		n >>= 4
 	}
-	safeBuf, err := w.Malloc(maxHexIntChars + 1 - i)
-	copy(safeBuf, buf[i:])
-	hexIntBufPool.Put(v)
-	return err
+	return append(b, tmp[i:]...)
 }
 
 func ReadHexInt(r network.Reader) (int, error) {
diff --git a/internal/bytesconv/bytesconv_32.go b/internal/bytesconv/bytesconv_32.go
index 8236be9..c8a3f2b 100644
--- a/internal/bytesconv/bytesconv_32.go
+++ b/internal/bytesconv/bytesconv_32.go
@@ -1,5 +1,4 @@
 //go:build !amd64 && !arm64 && !ppc64
-// +build !amd64,!arm64,!ppc64
 
 /*
  * Copyright 2022 CloudWeGo Authors
diff --git a/internal/bytesconv/bytesconv_64.go b/internal/bytesconv/bytesconv_64.go
index a348082..c50a419 100644
--- a/internal/bytesconv/bytesconv_64.go
+++ b/internal/bytesconv/bytesconv_64.go
@@ -1,5 +1,4 @@
 //go:build amd64 || arm64 || ppc64
-// +build amd64 arm64 ppc64
 
 /*
  * Copyright 2022 CloudWeGo Authors
diff --git a/internal/bytesconv/bytesconv_table_gen.go b/internal/bytesconv/bytesconv_table_gen.go
index eff361b..c703aaa 100644
--- a/internal/bytesconv/bytesconv_table_gen.go
+++ b/internal/bytesconv/bytesconv_table_gen.go
@@ -1,5 +1,4 @@
 //go:build ignore
-// +build ignore
 
 /*
  * Copyright 2022 CloudWeGo Authors
diff --git a/internal/network/standard.go b/internal/network/standard.go
new file mode 100644
index 0000000..8e444d1
--- /dev/null
+++ b/internal/network/standard.go
@@ -0,0 +1,25 @@
+// Copyright 2025 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 network
+
+import (
+	"github.com/cloudwego/hertz/pkg/network"
+)
+
+type StatefulConn interface {
+	network.Conn
+	DetectConnectionClose()
+	AbortBlockingRead()
+}
diff --git a/pkg/app/server/binding/internal/decoder/gjson_required.go b/pkg/app/server/binding/internal/decoder/gjson_required.go
index 130d65d..75be3dc 100644
--- a/pkg/app/server/binding/internal/decoder/gjson_required.go
+++ b/pkg/app/server/binding/internal/decoder/gjson_required.go
@@ -14,7 +14,6 @@
 //
 
 //go:build gjson || !(amd64 && (linux || windows || darwin))
-// +build gjson !amd64 !linux,!windows,!darwin
 
 package decoder
 
diff --git a/pkg/app/server/binding/internal/decoder/sonic_required.go b/pkg/app/server/binding/internal/decoder/sonic_required.go
index 61f3d8d..962d1e3 100644
--- a/pkg/app/server/binding/internal/decoder/sonic_required.go
+++ b/pkg/app/server/binding/internal/decoder/sonic_required.go
@@ -14,9 +14,6 @@
 //
 
 //go:build (linux || windows || darwin) && amd64 && !gjson
-// +build linux windows darwin
-// +build amd64
-// +build !gjson
 
 package decoder
 
diff --git a/pkg/app/server/hertz.go b/pkg/app/server/hertz.go
index b4a3f9c..b97fd17 100644
--- a/pkg/app/server/hertz.go
+++ b/pkg/app/server/hertz.go
@@ -73,12 +73,7 @@ func (h *Hertz) Spin() {
 		return
 	}
 
-	hlog.SystemLogger().Infof("Begin graceful shutdown, wait at most num=%d seconds...", h.GetOptions().ExitWaitTimeout/time.Second)
-
-	ctx, cancel := context.WithTimeout(context.Background(), h.GetOptions().ExitWaitTimeout)
-	defer cancel()
-
-	if err := h.Shutdown(ctx); err != nil {
+	if err := h.Shutdown(context.Background()); err != nil {
 		hlog.SystemLogger().Errorf("Shutdown error=%v", err)
 	}
 }
diff --git a/pkg/common/adaptor/handler.go b/pkg/common/adaptor/handler.go
new file mode 100644
index 0000000..2af3c4f
--- /dev/null
+++ b/pkg/common/adaptor/handler.go
@@ -0,0 +1,215 @@
+/*
+ * Copyright 2025 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 adaptor
+
+import (
+	"bufio"
+	"context"
+	"errors"
+	"net"
+	"net/http"
+
+	"github.com/cloudwego/hertz/internal/bytestr"
+	"github.com/cloudwego/hertz/pkg/app"
+	"github.com/cloudwego/hertz/pkg/network"
+	"github.com/cloudwego/hertz/pkg/protocol/consts"
+	"github.com/cloudwego/hertz/pkg/protocol/http1/resp"
+)
+
+// HertzHandler converts a http.Handler to an app.HandlerFunc.
+func HertzHandler(h http.Handler) app.HandlerFunc {
+	return func(ctx context.Context, rc *app.RequestContext) {
+		// creating http.Request
+		r := &rc.Request
+		req, err := http.NewRequestWithContext(ctx, methodstr(r.Method()), r.URI().String(), nil)
+		if err != nil {
+			rc.SetStatusCode(consts.StatusInternalServerError)
+			rc.SetBodyString(err.Error())
+			return
+		}
+
+		// request Body & ContentLength
+		stream := r.IsBodyStream()
+		if stream && r.HasMultipartForm() && len(r.BodyBytes()) > 0 {
+			// in this case, r.MultipartForm() called before this handler then we can not rely on stream body.
+			// coz both r.Body() and r.BodyStream() will return nothing due to EOF of stream body.
+			// we fix it by calling r.CloseBodyStream(), then r.Body() correctly returns bytes.
+			// FIXME: This should be taken care of by `protocol.Request`
+			r.CloseBodyStream()
+			stream = false
+		}
+		if stream {
+			// use BodyStream if possible to avoid OOM issue.
+			req.Body = reader2closer(r.BodyStream())
+			req.ContentLength = int64(r.Header.ContentLength())
+		} else {
+			// fast path for default cases with StreamRequestBody=false
+			b := r.Body()
+			req.Body = newBytesRWCloser(b)
+			req.ContentLength = int64(len(b))
+		}
+
+		// request Header
+		r.Header.VisitAll(func(k, v []byte) {
+			req.Header[string(k)] = append(req.Header[string(k)], string(v))
+		})
+
+		// request other properties
+		if s := r.Header.GetProtocol(); s != "" {
+			req.Proto = s
+			req.ProtoMajor, req.ProtoMinor, _ = parseHTTPVersion(s)
+		}
+		req.Close = r.ConnectionClose()
+		req.RemoteAddr = rc.RemoteAddr().String()
+		req.RequestURI = string(r.RequestURI())
+		if tlsconn, ok := rc.GetConn().(network.ConnTLSer); ok {
+			state := tlsconn.ConnectionState()
+			req.TLS = &state
+		}
+
+		// creating http.ResponseWriter
+		//
+		// coz it's server response
+		// no need to copy anything from hertz Response
+		w := &httpResponseWriter{rc: rc}
+		h.ServeHTTP(w, req)
+	}
+}
+
+type httpResponseWriter struct {
+	rc     *app.RequestContext
+	header http.Header
+
+	err error
+
+	wroteHeader bool
+	hijacked    bool
+	skipBody    bool
+}
+
+var errConnHijacked = errors.New("hertz net/http adaptor: conn hijacked")
+
+func (p *httpResponseWriter) Header() http.Header {
+	if p.header != nil {
+		return p.header
+	}
+	p.header = make(map[string][]string)
+	return p.header
+}
+
+func (p *httpResponseWriter) Write(b []byte) (n int, _ error) {
+	if p.hijacked {
+		return 0, errConnHijacked
+	}
+	if !p.wroteHeader {
+		p.WriteHeader(consts.StatusOK)
+	}
+	if p.err != nil {
+		return 0, p.err
+	}
+	if p.skipBody {
+		return len(b), nil
+	}
+	n, p.err = p.rc.Response.GetHijackWriter().Write(b)
+	return n, p.err
+}
+
+func (p *httpResponseWriter) WriteHeader(statusCode int) {
+	if p.wroteHeader || p.hijacked {
+		return
+	}
+	p.wroteHeader = true
+	r := &p.rc.Response
+	// reset and check if user updates Content-Length
+	// if we have no Content-Length, can only use chunked Transfer-Encoding
+	r.Header.InitContentLengthWithValue(-1)
+	for k, vv := range p.header {
+		for _, v := range vv {
+			r.Header.Add(k, v)
+		}
+	}
+	w := p.rc.GetWriter()
+	r.Header.SetStatusCode(statusCode)
+	p.skipBody = r.Header.MustSkipContentLength() ||
+		string(p.rc.Request.Method()) == consts.MethodHead
+	if p.skipBody {
+		// set Content-Length: 0
+		r.Header.SetCanonical(bytestr.StrContentLength, []byte("0"))
+		// skip all further writes,
+		// must be set for hertz request loop or it would write header and body after handler returns
+		r.HijackWriter(noopWriter{})
+		p.err = resp.WriteHeader(&r.Header, w)
+		return
+	}
+	if r.Header.ContentLength() < 0 {
+		r.HijackWriter(resp.NewChunkedBodyWriter(r, w))
+	} else {
+		p.err = resp.WriteHeader(&r.Header, w)
+		// use Writer directly instead of keep buffering data in resp.BodyBuffer()
+		// you never know how much data would be written to response
+		r.HijackWriter(writer2writerExt(w))
+	}
+}
+
+var _ http.Flusher = (*httpResponseWriter)(nil)
+
+// Flush implements the [http.Flusher]
+func (p *httpResponseWriter) Flush() {
+	_ = p.rc.GetWriter().Flush()
+}
+
+var _ http.Hijacker = (*httpResponseWriter)(nil)
+
+// Hijack implements the [net/http.Hijacker]
+func (p *httpResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
+	if p.hijacked {
+		return nil, nil, errConnHijacked
+	}
+	p.hijacked = true
+
+	conn := p.rc.GetConn()
+
+	// reset timeout if any
+	_ = conn.SetReadTimeout(0)
+	_ = conn.SetWriteTimeout(0)
+
+	// make sure after handler returns:
+	// * hertz won't reuse the conn
+	// * hertz won't write any extra bytes to underlying conn
+	p.rc.Response.Header.SetConnectionClose(true)
+	p.rc.Response.HijackWriter(noopHijackWriter{})
+
+	return conn, bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn)), nil
+}
+
+type noopHijackWriter struct{}
+
+var _ network.ExtWriter = noopHijackWriter{}
+
+func (noopHijackWriter) Write(b []byte) (int, error) {
+	return 0, errConnHijacked
+}
+func (noopHijackWriter) Flush() error    { return errConnHijacked }
+func (noopHijackWriter) Finalize() error { return nil }
+
+type noopWriter struct{}
+
+var _ network.ExtWriter = noopWriter{}
+
+func (noopWriter) Write(b []byte) (int, error) { return len(b), nil }
+func (noopWriter) Flush() error                { return nil }
+func (noopWriter) Finalize() error             { return nil }
diff --git a/pkg/common/adaptor/request.go b/pkg/common/adaptor/request.go
index ef3d787..7213ef6 100644
--- a/pkg/common/adaptor/request.go
+++ b/pkg/common/adaptor/request.go
@@ -24,6 +24,8 @@ import (
 )
 
 // GetCompatRequest only support basic function of Request, not for all.
+//
+// Deprecated: use HertzHandler instead
 func GetCompatRequest(req *protocol.Request) (*http.Request, error) {
 	r, err := http.NewRequest(string(req.Method()), req.URI().String(), bytes.NewReader(req.Body()))
 	if err != nil {
diff --git a/pkg/common/adaptor/response.go b/pkg/common/adaptor/response.go
index 4dbc0fa..2c0364d 100644
--- a/pkg/common/adaptor/response.go
+++ b/pkg/common/adaptor/response.go
@@ -68,6 +68,8 @@ func (c *compatResponse) WriteHeader(statusCode int) {
 }
 
 // GetCompatResponseWriter only support basic function of ResponseWriter, not for all.
+//
+// Deprecated: use HertzHandler instead
 func GetCompatResponseWriter(resp *protocol.Response) http.ResponseWriter {
 	c := &compatResponse{
 		h: resp,
diff --git a/pkg/common/adaptor/utils.go b/pkg/common/adaptor/utils.go
new file mode 100644
index 0000000..a47b888
--- /dev/null
+++ b/pkg/common/adaptor/utils.go
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2025 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 adaptor
+
+import (
+	"bytes"
+	"fmt"
+	"io"
+	"strconv"
+	"strings"
+	"unsafe"
+
+	"github.com/cloudwego/hertz/pkg/network"
+	"github.com/cloudwego/hertz/pkg/protocol/consts"
+)
+
+// methodstr tries to return consts without allocation
+func methodstr(m []byte) string {
+	if len(m) == 0 {
+		return "GET"
+	}
+	switch m[0] {
+	case 'G':
+		if string(m) == consts.MethodGet {
+			return consts.MethodGet
+		}
+	case 'P':
+		switch string(m) {
+		case consts.MethodPost:
+			return consts.MethodPost
+
+		case consts.MethodPut:
+			return consts.MethodPut
+
+		case consts.MethodPatch:
+			return consts.MethodPatch
+		}
+	case 'H':
+		if string(m) == consts.MethodHead {
+			return consts.MethodHead
+		}
+	case 'D':
+		if string(m) == consts.MethodDelete {
+			return consts.MethodDelete
+		}
+	}
+	return string(m)
+}
+
+type bytesRWCloser struct {
+	bytes.Reader
+}
+
+// newBytesRWCloser adds noop Close method to bytes.Reader without allocation
+func newBytesRWCloser(b []byte) io.ReadCloser {
+	rd := bytes.NewReader(b)
+	return (*bytesRWCloser)(unsafe.Pointer(rd))
+}
+
+// Close implements the [io.Closer] interface.
+func (bytesRWCloser) Close() error { return nil }
+
+func writer2writerExt(w network.Writer) network.ExtWriter {
+	return extWriter{w}
+}
+
+type extWriter struct {
+	network.Writer
+}
+
+func (w extWriter) Write(b []byte) (int, error) {
+	buf, err := w.Writer.Malloc(len(b))
+	if err != nil {
+		return 0, err
+	}
+	return copy(buf, b), nil
+}
+
+func (w extWriter) Finalize() error {
+	return w.Flush()
+}
+
+func reader2closer(r io.Reader) io.ReadCloser {
+	rc, ok := r.(io.ReadCloser)
+	if ok {
+		return rc
+	}
+	return io.NopCloser(r)
+}
+
+func parseHTTPVersion(s string) (major, minor int, _ error) {
+	v := strings.TrimPrefix(s, "HTTP/")
+	if len(v) == len(s) {
+		return 1, 1, fmt.Errorf("invalid http version: %q", s)
+	}
+	switch v {
+	case "1.0":
+		return 1, 0, nil
+
+	case "1.1":
+		return 1, 1, nil
+
+	default:
+		a, b, ok := strings.Cut(v, ".")
+		if ok {
+			major, err1 := strconv.Atoi(a)
+			minor, err2 := strconv.Atoi(b)
+			if err1 == nil && err2 == nil {
+				return major, minor, nil
+			}
+		}
+	}
+	return 1, 1, fmt.Errorf("invalid http version: %q", s)
+}
diff --git a/pkg/common/json/sonic.go b/pkg/common/json/sonic.go
index 20d6e04..c994db7 100644
--- a/pkg/common/json/sonic.go
+++ b/pkg/common/json/sonic.go
@@ -13,7 +13,7 @@
 // limitations under the License.
 //
 
-//go:build !stdjson
+//go:build (amd64 || arm64) && !stdjson
 
 package json
 
diff --git a/pkg/common/json/std.go b/pkg/common/json/std.go
index 9d4bfd3..8847de0 100644
--- a/pkg/common/json/std.go
+++ b/pkg/common/json/std.go
@@ -13,7 +13,7 @@
 // limitations under the License.
 //
 
-//go:build stdjson
+//go:build !(amd64 || arm64) || stdjson
 
 package json
 
diff --git a/pkg/network/dialer/dialer.go b/pkg/network/dialer/dialer.go
index aea05e0..1b856a4 100644
--- a/pkg/network/dialer/dialer.go
+++ b/pkg/network/dialer/dialer.go
@@ -22,9 +22,11 @@ import (
 	"time"
 
 	"github.com/cloudwego/hertz/pkg/network"
+	"github.com/cloudwego/hertz/pkg/network/standard"
 )
 
-var defaultDialer network.Dialer
+// will be netpoll.NewDialer() if available, see netpoll.go
+var defaultDialer network.Dialer = standard.NewDialer()
 
 // SetDialer is used to set the global default dialer.
 // Deprecated: use WithDialer instead.
diff --git a/pkg/network/dialer/default.go b/pkg/network/dialer/netpoll.go
similarity index 93%
rename from pkg/network/dialer/default.go
rename to pkg/network/dialer/netpoll.go
index 35c48a0..e1e1154 100644
--- a/pkg/network/dialer/default.go
+++ b/pkg/network/dialer/netpoll.go
@@ -1,5 +1,4 @@
-//go:build !windows
-// +build !windows
+//go:build (amd64 || arm64) && (linux || darwin)
 
 /*
  * Copyright 2022 CloudWeGo Authors
diff --git a/pkg/network/netpoll/transport.go b/pkg/network/netpoll/transport.go
index badf6cf..64fee41 100644
--- a/pkg/network/netpoll/transport.go
+++ b/pkg/network/netpoll/transport.go
@@ -14,7 +14,6 @@
 //
 
 //go:build !windows
-// +build !windows
 
 package netpoll
 
diff --git a/pkg/network/standard/connection.go b/pkg/network/standard/connection.go
index 13c10b4..de1ab98 100644
--- a/pkg/network/standard/connection.go
+++ b/pkg/network/standard/connection.go
@@ -17,6 +17,7 @@
 package standard
 
 import (
+	"context"
 	"crypto/tls"
 	"errors"
 	"io"
@@ -26,6 +27,7 @@ import (
 	"syscall"
 	"time"
 
+	internalNetwork "github.com/cloudwego/hertz/internal/network"
 	errs "github.com/cloudwego/hertz/pkg/common/errors"
 	"github.com/cloudwego/hertz/pkg/common/hlog"
 	"github.com/cloudwego/hertz/pkg/network"
@@ -638,3 +640,55 @@ func newTLSConn(c net.Conn, size int) network.Conn {
 		},
 	}
 }
+
+var earliestTime = time.Unix(1, 0)
+
+var _ internalNetwork.StatefulConn = &statefulConn{}
+
+func NewStatefulConn(c network.Conn, onReadError func(err error)) internalNetwork.StatefulConn {
+	return &statefulConn{
+		Conn:        c,
+		onReadError: onReadError,
+		ch:          make(chan struct{}, 1),
+	}
+}
+
+type statefulConn struct {
+	network.Conn
+	onReadError func(err error)
+	ch          chan struct{}
+}
+
+// DetectConnectionClose starts a blocking read to detect connection error
+func (c *statefulConn) DetectConnectionClose() {
+	c.Conn.SetReadDeadline(time.Time{}) // blocking read
+	go func() {
+		// start a blocking read, if return
+		// 1. n != 0, err == nil: pipeline request
+		// 2. err != nil: execute onReadError
+		_, err := c.Conn.Peek(1) // peek 1 byte to trigger Read Syscall
+		if err != nil {
+			c.onReadError(err)
+		}
+		c.ch <- struct{}{}
+	}()
+}
+
+// AbortBlockingRead cancels the blocking read and waits for the DetectConnectionClose exit.
+// Only call AbortBlockingRead if already called DetectConnectionClose.
+func (c *statefulConn) AbortBlockingRead() {
+	c.Conn.SetReadDeadline(earliestTime) // cancel the blocking read
+	<-c.ch                               // wait until the blocking read returns
+}
+
+func newOnReadErrorFunc(cancelCtx context.CancelFunc) func(err error) {
+	return func(err error) {
+		// 1. timeout error, triggered by `abortBlockingRead` when response is written, ignore.
+		// 2. other connection error (e.g. EOF), cancel the context
+		if ne, ok := err.(net.Error); ok && ne.Timeout() {
+			// ignore the error triggered by AbortBlockingRead, which should be "i/o timeout"
+		} else {
+			cancelCtx()
+		}
+	}
+}
diff --git a/pkg/network/standard/transport.go b/pkg/network/standard/transport.go
index 10ee991..1b08509 100644
--- a/pkg/network/standard/transport.go
+++ b/pkg/network/standard/transport.go
@@ -38,16 +38,17 @@ type transport struct {
 	// and/or multi-KB headers (for example, BIG cookies).
 	//
 	// Default buffer size is used if not set.
-	readBufferSize   int
-	network          string
-	addr             string
-	keepAliveTimeout time.Duration
-	readTimeout      time.Duration
-	handler          network.OnData
-	tls              *tls.Config
-	listenConfig     *net.ListenConfig
-	OnAccept         func(conn net.Conn) context.Context
-	OnConnect        func(ctx context.Context, conn network.Conn) context.Context
+	readBufferSize           int
+	network                  string
+	addr                     string
+	keepAliveTimeout         time.Duration
+	senseClientDisconnection bool
+	readTimeout              time.Duration
+	handler                  network.OnData
+	tls                      *tls.Config
+	listenConfig             *net.ListenConfig
+	OnAccept                 func(conn net.Conn) context.Context
+	OnConnect                func(ctx context.Context, conn network.Conn) context.Context
 
 	// active connections. it +1 after accept and -1 after handler returns
 	active int32
@@ -101,8 +102,16 @@ func (t *transport) serve() (err error) {
 		if t.OnConnect != nil {
 			ctx = t.OnConnect(ctx, c)
 		}
+
 		go func(ctx context.Context, conn network.Conn) {
-			t.handler(ctx, conn)
+			if t.senseClientDisconnection {
+				ctx, cancelCtx := context.WithCancel(ctx)
+				conn = NewStatefulConn(conn, newOnReadErrorFunc(cancelCtx))
+				t.handler(ctx, conn)
+				cancelCtx()
+			} else {
+				t.handler(ctx, conn)
+			}
 			t.updateActive(-1)
 		}(ctx, c)
 	}
@@ -169,14 +178,15 @@ func (t *transport) Shutdown(ctx context.Context) error {
 // For transporter switch
 func NewTransporter(options *config.Options) network.Transporter {
 	return &transport{
-		readBufferSize:   options.ReadBufferSize,
-		network:          options.Network,
-		addr:             options.Addr,
-		keepAliveTimeout: options.KeepAliveTimeout,
-		readTimeout:      options.ReadTimeout,
-		tls:              options.TLS,
-		listenConfig:     options.ListenConfig,
-		OnAccept:         options.OnAccept,
-		OnConnect:        options.OnConnect,
+		readBufferSize:           options.ReadBufferSize,
+		network:                  options.Network,
+		addr:                     options.Addr,
+		keepAliveTimeout:         options.KeepAliveTimeout,
+		readTimeout:              options.ReadTimeout,
+		senseClientDisconnection: options.SenseClientDisconnection,
+		tls:                      options.TLS,
+		listenConfig:             options.ListenConfig,
+		OnAccept:                 options.OnAccept,
+		OnConnect:                options.OnConnect,
 	}
 }
diff --git a/pkg/network/writer.go b/pkg/network/writer.go
index ed6c6ac..483c96d 100644
--- a/pkg/network/writer.go
+++ b/pkg/network/writer.go
@@ -106,6 +106,8 @@ func NewWriter(w io.Writer) Writer {
 
 type ExtWriter interface {
 	io.Writer
+
+	// Flush sends data to peer immediately
 	Flush() error
 
 	// Finalize will be called by framework before the writer is released.
diff --git a/pkg/protocol/client/client.go b/pkg/protocol/client/client.go
index a5b2c88..e0ed457 100644
--- a/pkg/protocol/client/client.go
+++ b/pkg/protocol/client/client.go
@@ -184,7 +184,7 @@ func GetURLDeadline(ctx context.Context, dst []byte, url string, deadline time.T
 func PostURL(ctx context.Context, dst []byte, url string, postArgs *protocol.Args, c Doer, requestOptions ...config.RequestOption) (statusCode int, body []byte, err error) {
 	req := protocol.AcquireRequest()
 	req.Header.SetMethodBytes(bytestr.StrPost)
-	req.Header.SetContentTypeBytes(bytestr.StrPostArgsContentType)
+	req.Header.SetContentTypeBytes(bytestr.MIMEPostForm)
 	req.SetOptions(requestOptions...)
 
 	if postArgs != nil {
diff --git a/pkg/protocol/header.go b/pkg/protocol/header.go
index e68ec5c..6993494 100644
--- a/pkg/protocol/header.go
+++ b/pkg/protocol/header.go
@@ -404,7 +404,7 @@ func (h *RequestHeader) AppendBytes(dst []byte) []byte {
 
 	contentType := h.ContentType()
 	if len(contentType) == 0 && !h.IgnoreBody() && !h.noDefaultContentType {
-		contentType = bytestr.StrPostArgsContentType
+		contentType = bytestr.MIMEPostForm
 	}
 	if len(contentType) > 0 {
 		dst = appendHeaderLine(dst, bytestr.StrContentType, contentType)
@@ -1144,7 +1144,7 @@ func (h *RequestHeader) Peek(key string) []byte {
 // where ... is substituted by the given boundary.
 func (h *RequestHeader) SetMultipartFormBoundary(boundary string) {
 	b := h.bufKV.value[:0]
-	b = append(b, bytestr.StrMultipartFormData...)
+	b = append(b, bytestr.MIMEFormData...)
 	b = append(b, ';', ' ')
 	b = append(b, bytestr.StrBoundary...)
 	b = append(b, '=')
@@ -1202,10 +1202,10 @@ func (h *RequestHeader) InitContentLengthWithValue(contentLength int) {
 // from 'multipart/form-data; boundary=...' Content-Type.
 func (h *RequestHeader) MultipartFormBoundary() []byte {
 	b := h.ContentType()
-	if !bytes.HasPrefix(b, bytestr.StrMultipartFormData) {
+	if !bytes.HasPrefix(b, bytestr.MIMEFormData) {
 		return nil
 	}
-	b = b[len(bytestr.StrMultipartFormData):]
+	b = b[len(bytestr.MIMEFormData):]
 	if len(b) == 0 || b[0] != ';' {
 		return nil
 	}
diff --git a/pkg/protocol/http1/client.go b/pkg/protocol/http1/client.go
index eb57d0d..c49da74 100644
--- a/pkg/protocol/http1/client.go
+++ b/pkg/protocol/http1/client.go
@@ -683,10 +683,24 @@ func (c *HostClient) doNonNilReqResp(req *protocol.Request, resp *protocol.Respo
 	// This is to solve the circular dependency problem of Response and BodyStream
 	shouldCloseConn := false
 
-	if !c.ResponseBodyStream {
-		err = respI.ReadHeaderAndLimitBody(resp, zr, c.MaxResponseBodySize)
+	if err = respI.ReadHeaders(resp, zr); err != nil {
+		_ = zr.Release()
+		c.closeConn(cc)
+		return true, err
+	}
+
+	stream := c.ResponseBodyStream
+
+	// if it's server-sent event response,
+	// we should set stream=true or it may block till timeout
+	if !stream && resp.Header.ContentLength() < 0 &&
+		bytes.HasPrefix(resp.Header.ContentType(), bytestr.MIMETextEventStream) {
+		stream = true
+	}
+	if !stream {
+		err = respI.ReadRespBody(resp, zr, c.MaxResponseBodySize)
 	} else {
-		err = respI.ReadBodyStream(resp, zr, c.MaxResponseBodySize, func(shouldClose bool) error {
+		err = respI.ReadRespBodyStream(resp, zr, c.MaxResponseBodySize, func(shouldClose bool) error {
 			if shouldCloseConn || shouldClose {
 				c.closeConn(cc)
 			} else {
@@ -714,7 +728,7 @@ func (c *HostClient) doNonNilReqResp(req *protocol.Request, resp *protocol.Respo
 	}
 
 	// In stream mode, we still can close/release the connection immediately if there is no content on the wire.
-	if c.ResponseBodyStream && resp.BodyStream() != protocol.NoResponseBody {
+	if stream && resp.BodyStream() != protocol.NoResponseBody {
 		return false, nil
 	}
 
diff --git a/pkg/protocol/http1/ext/common.go b/pkg/protocol/http1/ext/common.go
index 2f2280a..90cd3b9 100644
--- a/pkg/protocol/http1/ext/common.go
+++ b/pkg/protocol/http1/ext/common.go
@@ -296,27 +296,31 @@ func round2(n int) int {
 	return 1 << x
 }
 
-func WriteChunk(w network.Writer, b []byte, withFlush bool) (err error) {
+func WriteChunk(w network.Writer, b []byte, withFlush bool) error {
 	n := len(b)
-	if err = bytesconv.WriteHexInt(w, n); err != nil {
-		return err
-	}
 
-	w.WriteBinary(bytestr.StrCRLF) //nolint:errcheck
-	if _, err = w.WriteBinary(b); err != nil {
+	sz := bytesconv.EncodedIntHexLen(uint64(n)) + 2 // len + CRLF
+	if n > 0 {
+		sz += n + 2 // data + CRLF
+	}
+	wb, err := w.Malloc(sz)
+	if err != nil {
 		return err
 	}
 
-	// If it is the end of chunk, write CRLF after writing trailer
+	wb = bytesconv.AppendIntHex(wb[:0], uint64(n))
+	wb = append(wb, '\r', '\n')
 	if n > 0 {
-		w.WriteBinary(bytestr.StrCRLF) //nolint:errcheck
+		wb = append(wb, b...)
+		wb = append(wb, '\r', '\n')
+	}
+	if len(wb) != sz {
+		panic("[BUG] len mismatch")
 	}
-
 	if !withFlush {
 		return nil
 	}
-	err = w.Flush()
-	return
+	return w.Flush()
 }
 
 func isOnlyCRLF(b []byte) bool {
diff --git a/pkg/protocol/http1/resp/header.go b/pkg/protocol/http1/resp/header.go
index ff12cb2..6d4e3b4 100644
--- a/pkg/protocol/http1/resp/header.go
+++ b/pkg/protocol/http1/resp/header.go
@@ -87,12 +87,14 @@ func ReadHeader(h *protocol.ResponseHeader, r network.Reader) error {
 
 // WriteHeader writes response header to w.
 func WriteHeader(h *protocol.ResponseHeader, w network.Writer) error {
+	// Data may become invalid after the next call of ResponseHeader.
+	// copy before WriteHeader returns
 	header := h.Header()
-	h.SetHeaderLength(len(header))
-	_, err := w.WriteBinary(header)
+	b, err := w.Malloc(len(header))
 	if err != nil {
 		return err
 	}
+	h.SetHeaderLength(copy(b, header))
 	return nil
 }
 
diff --git a/pkg/protocol/http1/resp/response.go b/pkg/protocol/http1/resp/response.go
index 0366336..6fa6e12 100644
--- a/pkg/protocol/http1/resp/response.go
+++ b/pkg/protocol/http1/resp/response.go
@@ -89,13 +89,8 @@ func GetHTTP1Response(resp *protocol.Response) fmt.Stringer {
 	return &h1Response{resp}
 }
 
-// ReadHeaderAndLimitBody reads response from the given r, limiting the body size.
-//
-// If maxBodySize > 0 and the body size exceeds maxBodySize,
-// then ErrBodyTooLarge is returned.
-//
-// io.EOF is returned if r is closed before reading the first header byte.
-func ReadHeaderAndLimitBody(resp *protocol.Response, r network.Reader, maxBodySize int) error {
+// ReadHeaders reads http header into *protocol.Response
+func ReadHeaders(resp *protocol.Response, r network.Reader) error {
 	resp.ResetBody()
 	err := ReadHeader(&resp.Header, r)
 	if err != nil {
@@ -107,45 +102,87 @@ func ReadHeaderAndLimitBody(resp *protocol.Response, r network.Reader, maxBodySi
 			return err
 		}
 	}
+	return nil
+}
+
+// ReadHeaderAndLimitBody ...
+func ReadHeaderAndLimitBody(resp *protocol.Response, r network.Reader, maxBodySize int) error {
+	if err := ReadHeaders(resp, r); err != nil {
+		return err
+	}
+	return ReadRespBody(resp, r, maxBodySize)
+}
 
-	if !resp.MustSkipBody() {
-		bodyBuf := resp.BodyBuffer()
-		bodyBuf.Reset()
-		bodyBuf.B, err = ext.ReadBody(r, resp.Header.ContentLength(), maxBodySize, bodyBuf.B)
-		if err != nil {
+// ReadRespBody reads response body from the given r, limiting the body size.
+//
+// If maxBodySize > 0 and the body size exceeds maxBodySize,
+// then ErrBodyTooLarge is returned.
+//
+// io.EOF is returned if r is closed before reading the first header byte.
+func ReadRespBody(resp *protocol.Response, r network.Reader, maxBodySize int) (err error) {
+	if resp.MustSkipBody() {
+		return nil
+	}
+	bodyBuf := resp.BodyBuffer()
+	bodyBuf.Reset()
+	bodyBuf.B, err = ext.ReadBody(r, resp.Header.ContentLength(), maxBodySize, bodyBuf.B)
+	if err != nil {
+		return err
+	}
+	if resp.Header.ContentLength() == -1 {
+		err = ext.ReadTrailer(resp.Header.Trailer(), r)
+		if err != nil && err != io.EOF {
 			return err
 		}
-		if resp.Header.ContentLength() == -1 {
-			err = ext.ReadTrailer(resp.Header.Trailer(), r)
-			if err != nil && err != io.EOF {
-				return err
-			}
-		}
-		resp.Header.SetContentLength(len(bodyBuf.B))
 	}
-
+	resp.Header.SetContentLength(len(bodyBuf.B))
 	return nil
 }
 
 type clientRespStream struct {
+	mu sync.Mutex
+
 	r             io.Reader
 	closeCallback func(shouldClose bool) error
 }
 
+// ForceClose closes underlying conn. It enables `Read` call to return instead of blocking.
+//
+// This method is ONLY used by hertz internally.
+// Normally, users call `Close` when the body is no longer used.
+func (c *clientRespStream) ForceClose() (err error) {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+	if c.closeCallback != nil {
+		err = c.closeCallback(true)
+		c.closeCallback = nil
+	}
+	// NOTE: DO NOT put back to pool here,
+	// user may still use clientRespStream and call Close() like `defer body.Close()`
+	return
+}
+
+// Close closes response stream gracefully.
+//
+// NOTE:
+// * Since Close() will put it back to pool, MUST ensure it only be called when no longer use.
+// * MUST NOT call Close() and `Read()` concurrently to avoid race issue
 func (c *clientRespStream) Close() (err error) {
+	c.mu.Lock()
+	defer c.mu.Unlock()
 	runtime.SetFinalizer(c, nil)
-	// If error happened in release, the connection may be in abnormal state.
+	// If error happened in ReleaseBodyStream, the connection may be in abnormal state.
 	// Close it in the callback in order to avoid other unexpected problems.
 	err = ext.ReleaseBodyStream(c.r)
-	shouldClose := false
-	if err != nil {
-		shouldClose = true
-		hlog.Warnf("connection will be closed instead of recycled because an error occurred during the stream body release: %s", err.Error())
-	}
 	if c.closeCallback != nil {
-		err = c.closeCallback(shouldClose)
+		if err != nil {
+			hlog.SystemLogger().Warnf("error occurred during the stream body close: %s", err)
+		}
+		err = c.closeCallback(err != nil)
 	}
-	c.reset()
+	c.r = nil
+	c.closeCallback = nil
+	clientRespStreamPool.Put(c)
 	return
 }
 
@@ -153,12 +190,6 @@ func (c *clientRespStream) Read(p []byte) (n int, err error) {
 	return c.r.Read(p)
 }
 
-func (c *clientRespStream) reset() {
-	c.closeCallback = nil
-	c.r = nil
-	clientRespStreamPool.Put(c)
-}
-
 var clientRespStreamPool = sync.Pool{
 	New: func() interface{} {
 		return &clientRespStream{}
@@ -173,25 +204,24 @@ func convertClientRespStream(bs io.Reader, fn func(shouldClose bool) error) *cli
 	return clientStream
 }
 
-// ReadBodyStream reads response body in stream
-func ReadBodyStream(resp *protocol.Response, r network.Reader, maxBodySize int, closeCallBack func(shouldClose bool) error) error {
-	resp.ResetBody()
-	err := ReadHeader(&resp.Header, r)
-	if err != nil {
+// ReadHeaderBodyStream ...
+func ReadHeaderBodyStream(resp *protocol.Response, r network.Reader,
+	maxBodySize int, closeCallBack func(shouldClose bool) error) error {
+	if err := ReadHeaders(resp, r); err != nil {
 		return err
 	}
+	return ReadRespBodyStream(resp, r, maxBodySize, closeCallBack)
+}
 
-	if resp.Header.StatusCode() == consts.StatusContinue {
-		// Read the next response according to http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html .
-		if err = ReadHeader(&resp.Header, r); err != nil {
-			return err
-		}
-	}
+// Deprecated: use ReadHeaderBodyStream
+var ReadBodyStream = ReadHeaderBodyStream
 
+// ReadRespBodyStream reads response body in stream
+func ReadRespBodyStream(resp *protocol.Response, r network.Reader,
+	maxBodySize int, closeCallBack func(shouldClose bool) error) (err error) {
 	if resp.MustSkipBody() {
 		return nil
 	}
-
 	bodyBuf := resp.BodyBuffer()
 	bodyBuf.Reset()
 	bodyBuf.B, err = ext.ReadBodyWithStreaming(r, resp.Header.ContentLength(), maxBodySize, bodyBuf.B)
@@ -211,7 +241,6 @@ func ReadBodyStream(resp *protocol.Response, r network.Reader, maxBodySize int,
 		resp.Reset()
 		return err
 	}
-
 	bodyStream := ext.AcquireBodyStream(bodyBuf, r, resp.Header.Trailer(), resp.Header.ContentLength())
 	resp.ConstructBodyStream(bodyBuf, convertClientRespStream(bodyStream, closeCallBack))
 	return nil
diff --git a/pkg/protocol/http1/resp/writer.go b/pkg/protocol/http1/resp/writer.go
index 7b50cd4..02a3b7c 100644
--- a/pkg/protocol/http1/resp/writer.go
+++ b/pkg/protocol/http1/resp/writer.go
@@ -17,6 +17,7 @@
 package resp
 
 import (
+	"errors"
 	"runtime"
 	"sync"
 
@@ -36,32 +37,52 @@ func init() {
 }
 
 type chunkedBodyWriter struct {
-	sync.Once
-	finalizeErr error
+	r *protocol.Response
+	w network.Writer
+
+	err         error
+	finalized   bool
 	wroteHeader bool
-	r           *protocol.Response
-	w           network.Writer
 }
 
-// Write will encode chunked p before writing
-// It will only return the length of p and a nil error if the writing is successful or 0, error otherwise.
-//
-// NOTE: Write will use the user buffer to flush.
-// Before flush successfully, the buffer b should be valid.
+var errChunkedFinished = errors.New("chunked response is finished; no more data will be written.")
+
+// Write implements network.ExtWriter.Write / io.Writer.Write
 func (c *chunkedBodyWriter) Write(p []byte) (n int, err error) {
-	if !c.wroteHeader {
-		c.r.Header.SetContentLength(-1)
-		if err = WriteHeader(&c.r.Header, c.w); err != nil {
-			return
-		}
-		c.wroteHeader = true
+	if c.finalized {
+		return 0, errChunkedFinished
+	}
+	if c.err != nil {
+		return 0, c.err
+	}
+	if err := c.writeHeader(); err != nil {
+		return 0, err
 	}
-	if err = ext.WriteChunk(c.w, p, false); err != nil {
-		return
+	if err := c.writeChunk(p); err != nil {
+		return 0, err
 	}
 	return len(p), nil
 }
 
+func (c *chunkedBodyWriter) writeHeader() error {
+	if c.wroteHeader {
+		return nil
+	}
+	c.wroteHeader = true
+	c.r.Header.SetContentLength(-1)
+	if c.err = WriteHeader(&c.r.Header, c.w); c.err != nil {
+		return c.err
+	}
+	return nil
+}
+
+func (c *chunkedBodyWriter) writeChunk(b []byte) error {
+	if c.err = ext.WriteChunk(c.w, b, false); c.err != nil {
+		return c.err
+	}
+	return nil
+}
+
 func (c *chunkedBodyWriter) Flush() error {
 	return c.w.Flush()
 }
@@ -69,37 +90,36 @@ func (c *chunkedBodyWriter) Flush() error {
 // Finalize will write the ending chunk as well as trailer and flush the writer.
 // Warning: do not call this method by yourself, unless you know what you are doing.
 func (c *chunkedBodyWriter) Finalize() error {
-	c.Do(func() {
-		// in case no actual data from user
-		if !c.wroteHeader {
-			c.r.Header.SetContentLength(-1)
-			if c.finalizeErr = WriteHeader(&c.r.Header, c.w); c.finalizeErr != nil {
-				return
-			}
-			c.wroteHeader = true
-		}
-		c.finalizeErr = ext.WriteChunk(c.w, nil, true)
-		if c.finalizeErr != nil {
-			return
-		}
-		c.finalizeErr = ext.WriteTrailer(c.r.Header.Trailer(), c.w)
-	})
-	return c.finalizeErr
+	if c.finalized || c.err != nil {
+		return c.err
+	}
+	// zero-len chunk
+	if _, err := c.Write(nil); err != nil {
+		return err
+	}
+	c.finalized = true
+	// trailer which ends with \r\n
+	_, c.err = c.w.WriteBinary(c.r.Header.Trailer().Header())
+	if c.err == nil {
+		c.err = c.Flush()
+	}
+	return c.err
 }
 
 func (c *chunkedBodyWriter) release() {
 	c.r = nil
 	c.w = nil
-	c.finalizeErr = nil
+	c.err = nil
+	c.finalized = false
 	c.wroteHeader = false
 	chunkReaderPool.Put(c)
 }
 
+// NewChunkedBodyWriter creates a new chunked body writer.
 func NewChunkedBodyWriter(r *protocol.Response, w network.Writer) network.ExtWriter {
 	extWriter := chunkReaderPool.Get().(*chunkedBodyWriter)
 	extWriter.r = r
 	extWriter.w = w
-	extWriter.Once = sync.Once{}
 	runtime.SetFinalizer(extWriter, (*chunkedBodyWriter).release)
 	return extWriter
 }
diff --git a/pkg/protocol/http1/server.go b/pkg/protocol/http1/server.go
index eb46f65..8152018 100644
--- a/pkg/protocol/http1/server.go
+++ b/pkg/protocol/http1/server.go
@@ -26,10 +26,12 @@ import (
 	"time"
 
 	"github.com/cloudwego/hertz/internal/bytestr"
+	internalNetwork "github.com/cloudwego/hertz/internal/network"
 	internalStats "github.com/cloudwego/hertz/internal/stats"
 	"github.com/cloudwego/hertz/pkg/app"
 	"github.com/cloudwego/hertz/pkg/app/server/render"
 	errs "github.com/cloudwego/hertz/pkg/common/errors"
+	"github.com/cloudwego/hertz/pkg/common/hlog"
 	"github.com/cloudwego/hertz/pkg/common/tracer/stats"
 	"github.com/cloudwego/hertz/pkg/common/tracer/traceinfo"
 	"github.com/cloudwego/hertz/pkg/common/utils"
@@ -132,6 +134,10 @@ func (s Server) Serve(c context.Context, conn network.Conn) (err error) {
 		cc = c
 	)
 
+	// for sensing connection close
+	// only if `conn` is internalNetwork.StatefulConn
+	statefulConn, configuredSenseConnClose := conn.(internalNetwork.StatefulConn)
+
 	if s.EnableTrace {
 		eventsToTrigger = s.eventStackPool.Get().(*eventStack)
 	}
@@ -167,6 +173,7 @@ func (s Server) Serve(c context.Context, conn network.Conn) (err error) {
 
 	ctx.HTMLRender = s.HTMLRender
 	ctx.SetConn(conn)
+
 	ctx.Request.SetIsTLS(s.TLS != nil)
 	ctx.SetEnableTrace(s.EnableTrace)
 
@@ -178,6 +185,7 @@ func (s Server) Serve(c context.Context, conn network.Conn) (err error) {
 
 	for {
 		connRequestNum++
+		senseConnClose := configuredSenseConnClose // use to check if senseConnClose logic should be triggered for each request
 
 		if zr == nil {
 			zr = ctx.GetReader()
@@ -294,6 +302,7 @@ func (s Server) Serve(c context.Context, conn network.Conn) (err error) {
 				} else {
 					err = req.ContinueReadBody(&ctx.Request, zr, s.MaxRequestBodySize, !s.DisablePreParseMultipartForm)
 				}
+
 				if err != nil {
 					writeErrorResponse(zw, ctx, serverName, err)
 					return
@@ -313,6 +322,25 @@ func (s Server) Serve(c context.Context, conn network.Conn) (err error) {
 				internalStats.Record(ti, stats.ServerHandleFinish, err)
 			})
 		}
+
+		// If request sets BodyStream, we don't start connection close detection logic to prevent concurrent Read.
+		senseConnClose = senseConnClose && !ctx.Request.IsBodyStream()
+		if senseConnClose {
+			statefulConn.DetectConnectionClose()
+		}
+
+		if ctx.Request.IsURIParsed() {
+			// ctx.Request.URI() must not be called before ServeHTTP
+			// The only case is concurrency issue when parsing a new request,
+			// and user is reading the old request in background.
+			hlog.SystemLogger().Warnf("%s\n%s\n%s\n%s",
+				"Race detected.",
+				"Please be aware that the protocol.Request passed to handler is only valid before the handler returns.",
+				"DO NOT attempt to keep and access protocol.Request after the handler returns.",
+				"Try build with -race to check the race issue.")
+			return errors.New("race detected")
+		}
+
 		// Handle the request
 		//
 		// NOTE: All middlewares and business handler will be executed in this. And at this point, the request has been parsed
@@ -365,6 +393,13 @@ func (s Server) Serve(c context.Context, conn network.Conn) (err error) {
 			}
 		}
 
+		// Abort the blocking read in DetectConnectionClose
+		if senseConnClose {
+			// this should be sync to wait the blocking read return.
+			// we should make sure the read is not affected by other SetReadDeadline.
+			statefulConn.AbortBlockingRead()
+		}
+
 		// Release the zeroCopyReader before flush to prevent data race
 		if zr != nil {
 			zr.Release() //nolint:errcheck
@@ -407,6 +442,7 @@ func (s Server) Serve(c context.Context, conn network.Conn) (err error) {
 		}
 		// Back to network layer to trigger.
 		// For now, only netpoll network mode has this feature.
+		// FIXME: check
 		if s.IdleTimeout == 0 {
 			return
 		}
diff --git a/pkg/protocol/request.go b/pkg/protocol/request.go
index 1bec09f..d7ddf2c 100644
--- a/pkg/protocol/request.go
+++ b/pkg/protocol/request.go
@@ -464,7 +464,7 @@ func (req *Request) SetFormData(data map[string]string) {
 		req.postArgs.Add(k, v)
 	}
 	req.parsedPostArgs = true
-	req.Header.SetContentTypeBytes(bytestr.StrPostArgsContentType)
+	req.Header.SetContentTypeBytes(bytestr.MIMEPostForm)
 }
 
 // SetFormDataFromValues sets x-www-form-urlencoded params from url values.
@@ -475,7 +475,7 @@ func (req *Request) SetFormDataFromValues(data url.Values) {
 		}
 	}
 	req.parsedPostArgs = true
-	req.Header.SetContentTypeBytes(bytestr.StrPostArgsContentType)
+	req.Header.SetContentTypeBytes(bytestr.MIMEPostForm)
 }
 
 // SetFile sets single file field name and its path for multipart upload.
@@ -638,7 +638,7 @@ func (req *Request) IsBodyStream() bool {
 
 func (req *Request) BodyStream() io.Reader {
 	if req.bodyStream == nil {
-		req.bodyStream = NoBody
+		return NoBody
 	}
 	return req.bodyStream
 }
@@ -684,8 +684,7 @@ func (req *Request) parsePostArgs() {
 		return
 	}
 	req.parsedPostArgs = true
-
-	if !bytes.HasPrefix(req.Header.ContentType(), bytestr.StrPostArgsContentType) {
+	if !bytes.HasPrefix(req.Header.ContentType(), bytestr.MIMEPostForm) {
 		return
 	}
 	req.postArgs.ParseBytes(req.Body())
diff --git a/pkg/protocol/response.go b/pkg/protocol/response.go
index 174ae4f..3d8a80b 100644
--- a/pkg/protocol/response.go
+++ b/pkg/protocol/response.go
@@ -342,7 +342,7 @@ func (resp *Response) SetBody(body []byte) {
 
 func (resp *Response) BodyStream() io.Reader {
 	if resp.bodyStream == nil {
-		resp.bodyStream = NoResponseBody
+		return NoResponseBody
 	}
 	return resp.bodyStream
 }
@@ -393,6 +393,10 @@ func (resp *Response) ConnectionClose() bool {
 	return resp.Header.ConnectionClose()
 }
 
+// CloseBodyStream tries call Close() of underlying body stream.
+//
+// NOTE:
+// * MUST NOT call CloseBodyStream() and BodyStream().Read() concurrently to avoid race issue.
 func (resp *Response) CloseBodyStream() error {
 	if resp.bodyStream == nil {
 		return nil
diff --git a/pkg/protocol/sse/event.go b/pkg/protocol/sse/event.go
new file mode 100644
index 0000000..3989ca6
--- /dev/null
+++ b/pkg/protocol/sse/event.go
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2025 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 sse
+
+import (
+	"fmt"
+	"sync"
+	"time"
+)
+
+const (
+	fieldID = 1 << iota
+	fieldType
+	fieldData
+	fieldRetry
+)
+
+// Event represents a Server-Sent Event (SSE).
+type Event struct {
+	ID   string
+	Type string // aka `event` field, which means event type
+	Data []byte
+
+	// hertz only supports reading and writing the field,
+	// and will not take care of retry policy, please implement on your own.
+	Retry time.Duration
+
+	bitset uint8
+}
+
+var poolEvent = sync.Pool{}
+
+// NewEvent creates a new event.
+//
+// Call `Release` when you're done with the event.
+func NewEvent() *Event {
+	if v := poolEvent.Get(); v != nil {
+		ret := v.(*Event)
+		ret.Reset()
+		return ret
+	}
+	return &Event{}
+}
+
+// Release releases the event back to the pool.
+func (e *Event) Release() {
+	poolEvent.Put(e)
+}
+
+// String returns a string representation of the event.
+func (e *Event) String() string {
+	return fmt.Sprintf("Event{ID:%q, Type:%q, Retry:%s, Data:%q}", e.ID, e.Type, e.Retry, e.Data)
+}
+
+// Reset resets the event fields.
+func (e *Event) Reset() {
+	e.ID = ""
+	e.Type = ""
+	e.Retry = time.Duration(0)
+	e.Data = e.Data[:0]
+	e.bitset = 0
+}
+
+// Clone creates a copy of the event.
+//
+// When it's no longer needed, call `Release` to return it to the pool.
+func (e *Event) Clone() *Event {
+	p := NewEvent()
+	p.ID = e.ID
+	p.Type = e.Type
+	p.Retry = e.Retry
+	p.Data = append(p.Data[:0], e.Data...)
+	p.bitset = e.bitset
+	return p
+}
+
+// IsSetID returns true if the event ID is set.
+//
+// Please use SetID to set the event ID for differentiating notset or empty
+func (e *Event) IsSetID() bool {
+	return e.bitset&fieldID != 0 || len(e.ID) > 0
+}
+
+// IsSetType returns true if the event type is set.
+//
+// Please use SetEvent to set the event type for differentiating notset or empty
+func (e *Event) IsSetType() bool {
+	return e.bitset&fieldType != 0 || len(e.Type) > 0
+}
+
+// IsSetRetry returns true if the retry duration is set.
+//
+// Please use SetRetry to set the event retry duration for differentiating notset or empty
+func (e *Event) IsSetRetry() bool {
+	return e.bitset&fieldRetry != 0 || e.Retry != 0
+}
+
+// IsSetData returns true if the event data is set.
+//
+// Please use SetData to set or AppendData to append the event data for differentiating notset or empty
+func (e *Event) IsSetData() bool {
+	return e.bitset&fieldData != 0 || len(e.Data) > 0
+}
+
+// SetID sets the event ID.
+func (e *Event) SetID(id string) {
+	e.ID = id
+	e.bitset |= fieldID
+}
+
+// SetEvent sets the event type.
+func (e *Event) SetEvent(eventType string) {
+	e.Type = eventType
+	e.bitset |= fieldType
+}
+
+// SetData sets the event data.
+func (e *Event) SetData(data []byte) {
+	e.Data = append(e.Data[:0], data...)
+	e.bitset |= fieldData
+}
+
+// SetDataString sets the event data from a string.
+func (e *Event) SetDataString(data string) {
+	e.Data = append(e.Data[:0], data...)
+	e.bitset |= fieldData
+}
+
+// AppendData appends data to the event data.
+func (e *Event) AppendData(data []byte) {
+	e.Data = append(e.Data, data...)
+	e.bitset |= fieldData
+}
+
+// AppendDataString appends string data to the event data.
+func (e *Event) AppendDataString(data string) {
+	e.Data = append(e.Data, data...)
+	e.bitset |= fieldData
+}
+
+// SetRetry sets the retry duration.
+func (e *Event) SetRetry(retry time.Duration) {
+	e.Retry = retry
+	e.bitset |= fieldRetry
+}
diff --git a/pkg/protocol/sse/reader.go b/pkg/protocol/sse/reader.go
new file mode 100644
index 0000000..b879fee
--- /dev/null
+++ b/pkg/protocol/sse/reader.go
@@ -0,0 +1,214 @@
+/*
+ * Copyright 2025 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 sse
+
+import (
+	"bufio"
+	"bytes"
+	"context"
+	"errors"
+	"io"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/cloudwego/hertz/internal/bytestr"
+	"github.com/cloudwego/hertz/pkg/protocol"
+)
+
+// errNotSSEContentType is returned when the response's content type is not text/event-stream.
+var errNotSSEContentType = errors.New("Content-Type returned by server is NOT text/event-stream")
+
+// Reader represents a reader for Server-Sent Events (SSE).
+//
+// It is used to parse the response body and extract individual events.
+type Reader struct {
+	resp   *protocol.Response
+	r      io.Reader
+	s      *bufio.Scanner
+	events int32
+
+	lastEventID string
+}
+
+// NewReader creates a new SSE reader from the given response.
+//
+// It returns an error if the response's content type is not text/event-stream.
+func NewReader(resp *protocol.Response) (*Reader, error) {
+	if !bytes.HasPrefix(resp.Header.ContentType(), bytestr.MIMETextEventStream) {
+		return nil, errNotSSEContentType
+	}
+	r := &Reader{resp: resp}
+	if resp.IsBodyStream() {
+		r.r = resp.BodyStream()
+	} else {
+		r.r = bytes.NewReader(resp.Body())
+	}
+	r.s = bufio.NewScanner(r.r)
+	return r, nil
+}
+
+type forceCloseIf interface {
+	ForceClose() error // implemented by *clientRespStream
+}
+
+// ForEach iterates over all SSE events in the response body,
+// invoking the provided handler function for each event.
+//
+// `f` MUST NOT keep the Event reference after the func returns.
+// Use (*Event).Clone to create a copy instead.
+//
+// Iteration stops if the handler returns an error or if reading fails.
+// `ctx` is used to cancel the iteration if ctx.Done() != nil
+//
+// It returns nil if all events are processed successfully.
+func (r *Reader) ForEach(ctx context.Context, f func(e *Event) error) error {
+	if ctx.Done() != nil {
+		ch := make(chan struct{})
+		defer close(ch)
+		go func() {
+			select {
+			case <-ctx.Done():
+				// force close the underlying connection to release resource
+				// or r.Read may block until remote server ends
+				if s, ok := r.r.(forceCloseIf); ok {
+					s.ForceClose()
+				}
+			case <-ch:
+				return
+			}
+		}()
+	}
+	e := NewEvent()
+	defer e.Release()
+	for {
+		if err := ctx.Err(); err != nil {
+			return err
+		}
+		if err := r.Read(e); err != nil {
+			if err == io.EOF {
+				return nil
+			}
+			if er := ctx.Err(); er != nil {
+				err = er
+			}
+			return err
+		}
+		if err := f(e); err != nil {
+			return err
+		}
+	}
+}
+
+// LastEventID returns the last event ID read by the reader.
+func (r *Reader) LastEventID() string {
+	return r.lastEventID
+}
+
+func (r *Reader) onEventRead(e *Event) {
+	r.events++
+	if e.IsSetID() {
+		r.lastEventID = e.ID
+	}
+}
+
+// Read reads a single SSE event from the response body.
+//
+// It populates the provided Event struct with the parsed data.
+// Returns nil if an event was successfully read, or an error otherwise.
+func (r *Reader) Read(e *Event) error {
+	e.Reset()
+	for i := 0; r.s.Scan(); i++ {
+		line := r.s.Bytes()
+
+		// Trim UTF8 BOM
+		if i == 0 && r.events == 0 && bytes.HasPrefix(line, []byte{0xEF, 0xBB, 0xBF}) {
+			line = line[3:]
+		}
+
+		if len(line) == 0 {
+			// Empty line marks the end of an event
+			if e.bitset != 0 {
+				r.onEventRead(e)
+				return nil
+			}
+			continue // Skip empty lines at the beginning
+		}
+
+		if line[0] == ':' {
+			// Comment which starts with colon
+			continue
+		}
+
+		// Parse field
+		var f, v []byte
+		i := bytes.IndexByte(line, ':')
+		if i < 0 {
+			// No colon, the entire line is the field name with an empty value
+			f = line
+		} else {
+			f = line[:i]
+			// If the colon is followed by a space, remove it
+			if i+1 < len(line) && line[i+1] == ' ' {
+				v = line[i+2:]
+			} else {
+				v = line[i+1:]
+			}
+		}
+
+		// Process the field
+		switch string(f) {
+		case "event":
+			e.SetEvent(sseEventType(v))
+		case "data":
+			if len(e.Data) > 0 {
+				// If we already have data, append a newline before the new data
+				e.Data = append(e.Data, '\n')
+			}
+			e.AppendData(v)
+		case "id":
+			id := string(v)
+			// Ignore if it contains Null
+			if !strings.Contains(id, "\u0000") {
+				e.SetID(id)
+			}
+		case "retry":
+			if retry, err := strconv.ParseInt(string(v), 10, 64); err == nil {
+				e.SetRetry(time.Duration(retry) * time.Millisecond)
+			}
+		default:
+			// As per spec, ignore if it's not defined.
+		}
+	}
+	// Check if scanner encountered an error
+	if err := r.s.Err(); err != nil {
+		return err
+	}
+	if e.bitset == 0 {
+		return io.EOF
+	}
+	r.onEventRead(e)
+	return nil
+}
+
+// Close closes the underlying response body.
+//
+// NOTE:
+// * MUST NOT call Close() and Read() / ForEach() concurrently to avoid race issue.
+func (r *Reader) Close() error {
+	return r.resp.CloseBodyStream()
+}
diff --git a/pkg/network/dialer/default_windows.go b/pkg/protocol/sse/request.go
similarity index 77%
rename from pkg/network/dialer/default_windows.go
rename to pkg/protocol/sse/request.go
index e2ff5b1..6e9082d 100644
--- a/pkg/network/dialer/default_windows.go
+++ b/pkg/protocol/sse/request.go
@@ -1,5 +1,5 @@
 /*
- * Copyright 2022 CloudWeGo Authors
+ * Copyright 2025 CloudWeGo Authors
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,12 +14,4 @@
  * limitations under the License.
  */
 
-package dialer
-
-import (
-	"github.com/cloudwego/hertz/pkg/network/standard"
-)
-
-func init() {
-	defaultDialer = standard.NewDialer()
-}
+package sse
diff --git a/pkg/route/default_windows.go b/pkg/protocol/sse/request_test.go
similarity index 76%
rename from pkg/route/default_windows.go
rename to pkg/protocol/sse/request_test.go
index 71deb19..6e9082d 100644
--- a/pkg/route/default_windows.go
+++ b/pkg/protocol/sse/request_test.go
@@ -1,5 +1,5 @@
 /*
- * Copyright 2022 CloudWeGo Authors
+ * Copyright 2025 CloudWeGo Authors
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,12 +14,4 @@
  * limitations under the License.
  */
 
-package route
-
-import (
-	"github.com/cloudwego/hertz/pkg/network/standard"
-)
-
-func init() {
-	defaultTransporter = standard.NewTransporter
-}
+package sse
diff --git a/pkg/protocol/sse/utils.go b/pkg/protocol/sse/utils.go
new file mode 100644
index 0000000..74062b8
--- /dev/null
+++ b/pkg/protocol/sse/utils.go
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2025 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 sse
+
+import (
+	"bytes"
+
+	"github.com/cloudwego/hertz/internal/bytestr"
+	"github.com/cloudwego/hertz/pkg/protocol"
+)
+
+const LastEventIDHeader = "Last-Event-ID"
+
+// GetLastEventID returns the value of the Last-Event-ID header.
+func GetLastEventID(req *protocol.Request) string {
+	return string(req.Header.Peek(LastEventIDHeader))
+}
+
+// SetLastEventID sets the Last-Event-ID header.
+func SetLastEventID(req *protocol.Request, id string) {
+	req.Header.Set(LastEventIDHeader, id)
+}
+
+// AddAcceptMIME adds `text/event-stream` to http `Accept` header.
+//
+// This is NOT required as per spec:
+// * User agents MAY set (`Accept`, `text/event-stream`) in request's header list.
+func AddAcceptMIME(req *protocol.Request) {
+	v := req.Header.Peek("Accept")
+	if len(v) > 0 {
+		if bytes.Contains(v, bytestr.MIMETextEventStream) {
+			return
+		}
+		// for better compatibility, only use one Accept header value
+		// append `text/event-stream` to the end of the value
+		req.Header.Set("Accept", string(v)+", "+string(bytestr.MIMETextEventStream))
+	} else {
+		req.Header.Set("Accept", string(bytestr.MIMETextEventStream))
+	}
+}
+
+func sseEventType(v []byte) string {
+	switch string(v) {
+	case "message":
+		return "message"
+	}
+	return string(v)
+}
+
+func hasCRLF(s string) bool {
+	for i := len(s) - 1; i >= 0; i-- {
+		switch s[i] {
+		case '\r', '\n':
+			return true
+		}
+	}
+	return false
+}
diff --git a/pkg/protocol/sse/writer.go b/pkg/protocol/sse/writer.go
new file mode 100644
index 0000000..511eecb
--- /dev/null
+++ b/pkg/protocol/sse/writer.go
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2025 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 sse
+
+import (
+	"bytes"
+	"errors"
+	"strconv"
+	"strings"
+	"sync"
+
+	"github.com/cloudwego/hertz/internal/bytestr"
+	"github.com/cloudwego/hertz/pkg/app"
+	"github.com/cloudwego/hertz/pkg/common/bytebufferpool"
+	"github.com/cloudwego/hertz/pkg/network"
+	"github.com/cloudwego/hertz/pkg/protocol/http1/resp"
+)
+
+// Writer represents a writer for Server-Sent Events (SSE).
+//
+// It is used to write individual events to the response body.
+type Writer struct {
+	w network.ExtWriter
+
+	mu sync.Mutex
+}
+
+// NewWriter creates a new SSE writer.
+func NewWriter(c *app.RequestContext) *Writer {
+	c.Response.Header.Set("Cache-Control", "no-cache")
+	c.Response.Header.SetContentType(string(bytestr.MIMETextEventStream))
+	w := c.Response.GetHijackWriter()
+	if w == nil {
+		w = resp.NewChunkedBodyWriter(&c.Response, c.GetWriter())
+		c.Response.HijackWriter(w)
+	}
+	return &Writer{w: w}
+}
+
+var (
+	errIDContainsCRLR   = errors.New(`id field contains '\r' or '\n'`)
+	errTypeContainsCRLR = errors.New(`event field contains '\r' or '\n'`)
+)
+
+// WriteEvent writes a single SSE event to the response body.
+//
+// If id, eventType, or data are zero-length, they will be ignored.
+// It returns an error if the event contains invalid characters or if the underlying writer fails.
+func (w *Writer) WriteEvent(id, eventType string, data []byte) error {
+	return w.Write(&Event{
+		ID:   id,
+		Type: eventType,
+		Data: data,
+	})
+}
+
+// WriteKeepAlive writes a comment line with "keep-alive" to the response body.
+//
+// It keeps the underlying connection alive, which is useful when using proxy servers.
+func (w *Writer) WriteKeepAlive() error {
+	return w.WriteComment("keep-alive")
+}
+
+// WriteComment writes comment lines to the response body.
+//
+// Client-side will ignore lines starting with a U+003A COLON character (:)
+// see: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation
+func (w *Writer) WriteComment(s string) error {
+	p := bytebufferpool.Get()
+	defer bytebufferpool.Put(p)
+
+	buf := p.B[:0]
+	for len(s) > 0 {
+		i := strings.IndexByte(s, '\n')
+		if i >= 0 {
+			buf = append(buf, ':')
+			buf = append(buf, s[:i+1]...) // it contains '\n' already
+			s = s[i+1:]
+		} else {
+			buf = append(append(append(buf, ':'), s...), '\n')
+			s = ""
+		}
+	}
+	if len(buf) == 0 {
+		buf = append(buf, ':')
+	}
+	p.B = buf
+	w.mu.Lock()
+	defer w.mu.Unlock()
+	if _, err := w.w.Write(p.B); err != nil {
+		return err
+	}
+	return w.w.Flush()
+}
+
+// Write writes a single SSE event to the response body.
+//
+// It returns an error if the event contains invalid characters or underlying writer fails.
+func (w *Writer) Write(e *Event) error {
+	p := bytebufferpool.Get()
+	defer bytebufferpool.Put(p)
+
+	buf := p.B[:0]
+
+	if e.IsSetID() {
+		if hasCRLF(e.ID) {
+			return errIDContainsCRLR
+		}
+		buf = append(append(append(buf, "id: "...), e.ID...), '\n')
+	}
+
+	if e.IsSetType() {
+		if e.Type == "message" {
+			buf = append(buf, "event: message\n"...) // fast path for message
+		} else {
+			if hasCRLF(e.Type) {
+				return errTypeContainsCRLR
+			}
+			buf = append(append(append(buf, "event: "...), e.Type...), '\n')
+		}
+	}
+
+	if e.IsSetRetry() {
+		buf = append(buf, "retry: "...)
+		buf = strconv.AppendInt(buf, e.Retry.Milliseconds(), 10)
+		buf = append(buf, '\n')
+	}
+
+	if e.IsSetData() {
+		data := e.Data
+		for len(data) > 0 {
+			i := bytes.IndexByte(data, '\n')
+			if i >= 0 {
+				buf = append(buf, "data: "...)
+				buf = append(buf, data[:i+1]...) // it contains '\n' already
+				data = data[i+1:]
+			} else {
+				buf = append(append(append(buf, "data: "...), data...), '\n')
+				data = nil
+			}
+		}
+	}
+	p.B = append(buf, '\n') // end of event
+	w.mu.Lock()
+	defer w.mu.Unlock()
+	if _, err := w.w.Write(p.B); err != nil {
+		return err
+	}
+	return w.w.Flush()
+}
+
+func (w *Writer) Close() error {
+	w.mu.Lock()
+	defer w.mu.Unlock()
+	return w.w.Finalize()
+}
diff --git a/pkg/protocol/uri_unix.go b/pkg/protocol/uri_unix.go
index d3726d8..5821067 100644
--- a/pkg/protocol/uri_unix.go
+++ b/pkg/protocol/uri_unix.go
@@ -1,5 +1,4 @@
 //go:build !windows
-// +build !windows
 
 /*
  * Copyright 2022 CloudWeGo Authors
diff --git a/pkg/protocol/uri_windows.go b/pkg/protocol/uri_windows.go
index a40daef..0c3092c 100644
--- a/pkg/protocol/uri_windows.go
+++ b/pkg/protocol/uri_windows.go
@@ -1,5 +1,4 @@
 //go:build windows
-// +build windows
 
 /*
  * Copyright 2022 CloudWeGo Authors
diff --git a/pkg/route/engine.go b/pkg/route/engine.go
index 7c87826..6589cf9 100644
--- a/pkg/route/engine.go
+++ b/pkg/route/engine.go
@@ -82,6 +82,7 @@ import (
 const unknownTransporterName = "unknown"
 
 var (
+	// will be netpoll.NewTransporter if available, see: netpoll.go
 	defaultTransporter = standard.NewTransporter
 
 	errInitFailed       = errs.NewPrivate("engine has been init already")
@@ -312,9 +313,17 @@ func (engine *Engine) Shutdown(ctx context.Context) (err error) {
 		return
 	}
 
+	opt := engine.GetOptions()
+	hlog.SystemLogger().Infof("Begin graceful shutdown, wait at most %s ...", opt.ExitWaitTimeout)
+
+	ctx, cancel := context.WithTimeout(ctx, opt.ExitWaitTimeout)
+	defer cancel()
+
 	ch := make(chan struct{})
-	// trigger hooks if any
-	go engine.executeOnShutdownHooks(ctx, ch)
+	go func() {
+		defer close(ch)
+		engine.executeOnShutdownHooks(ctx)
+	}()
 
 	defer func() {
 		// ensure that the hook is executed until wait timeout or finish
@@ -328,7 +337,7 @@ func (engine *Engine) Shutdown(ctx context.Context) (err error) {
 		}
 	}()
 
-	if opt := engine.options; opt != nil && opt.Registry != nil {
+	if opt.Registry != nil {
 		if err = opt.Registry.Deregister(opt.RegistryInfo); err != nil {
 			hlog.SystemLogger().Errorf("Deregister error=%v", err)
 			return err
@@ -343,7 +352,7 @@ func (engine *Engine) Shutdown(ctx context.Context) (err error) {
 	return
 }
 
-func (engine *Engine) executeOnShutdownHooks(ctx context.Context, ch chan struct{}) {
+func (engine *Engine) executeOnShutdownHooks(ctx context.Context) {
 	wg := sync.WaitGroup{}
 	for i := range engine.OnShutdown {
 		wg.Add(1)
@@ -353,7 +362,6 @@ func (engine *Engine) executeOnShutdownHooks(ctx context.Context, ch chan struct
 		}(i)
 	}
 	wg.Wait()
-	ch <- struct{}{}
 }
 
 func (engine *Engine) Run() (err error) {
@@ -572,6 +580,7 @@ func (engine *Engine) ServeStream(ctx context.Context, conn network.StreamConn)
 }
 
 func (engine *Engine) initBinderAndValidator(opt *config.Options) {
+	var isCustomValidator bool
 	// init validator
 	if opt.CustomValidator != nil {
 		customValidator, ok := opt.CustomValidator.(binding.StructValidator)
@@ -579,6 +588,7 @@ func (engine *Engine) initBinderAndValidator(opt *config.Options) {
 			panic("customized validator does not implement binding.StructValidator")
 		}
 		engine.validator = customValidator
+		isCustomValidator = true
 	} else {
 		engine.validator = binding.NewValidator(binding.NewValidateConfig())
 		if opt.ValidateConfig != nil {
@@ -587,6 +597,7 @@ func (engine *Engine) initBinderAndValidator(opt *config.Options) {
 				panic("opt.ValidateConfig is not the '*binding.ValidateConfig' type")
 			}
 			engine.validator = binding.NewValidator(vConf)
+			isCustomValidator = true
 		}
 	}
 
@@ -607,7 +618,8 @@ func (engine *Engine) initBinderAndValidator(opt *config.Options) {
 		if !ok {
 			panic("opt.BindConfig is not the '*binding.BindConfig' type")
 		}
-		if bConf.Validator == nil {
+		// optimize: user customized validator has the highest priority
+		if isCustomValidator || bConf.Validator == nil {
 			bConf.Validator = engine.validator
 		}
 		engine.binder = binding.NewDefaultBinder(bConf)
diff --git a/pkg/route/default.go b/pkg/route/netpoll.go
similarity index 93%
rename from pkg/route/default.go
rename to pkg/route/netpoll.go
index d357715..6f614e8 100644
--- a/pkg/route/default.go
+++ b/pkg/route/netpoll.go
@@ -13,8 +13,7 @@
 // limitations under the License.
 //
 
-//go:build !windows
-// +build !windows
+//go:build (amd64 || arm64) && (linux || darwin)
 
 package route
 
diff --git a/version.go b/version.go
index 58c3022..91da369 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.9.7"
+	Version = "v0.10.0"
 )


Test patch:
diff --git a/internal/bytesconv/bytesconv_32_test.go b/internal/bytesconv/bytesconv_32_test.go
index 384127f..7b8ebdd 100644
--- a/internal/bytesconv/bytesconv_32_test.go
+++ b/internal/bytesconv/bytesconv_32_test.go
@@ -1,5 +1,4 @@
 //go:build !amd64 && !arm64 && !ppc64
-// +build !amd64,!arm64,!ppc64
 
 /*
  * Copyright 2022 CloudWeGo Authors
@@ -51,22 +50,6 @@ import (
 	"github.com/cloudwego/hertz/pkg/common/test/assert"
 )
 
-func TestWriteHexInt(t *testing.T) {
-	t.Parallel()
-
-	for _, v := range []struct {
-		s string
-		n int
-	}{
-		{"0", 0},
-		{"1", 1},
-		{"123", 0x123},
-		{"7fffffff", 0x7fffffff},
-	} {
-		testWriteHexInt(t, v.n, v.s)
-	}
-}
-
 func TestReadHexInt(t *testing.T) {
 	t.Parallel()
 
diff --git a/internal/bytesconv/bytesconv_64_test.go b/internal/bytesconv/bytesconv_64_test.go
index 6f96f8b..c724676 100644
--- a/internal/bytesconv/bytesconv_64_test.go
+++ b/internal/bytesconv/bytesconv_64_test.go
@@ -1,5 +1,4 @@
 //go:build amd64 || arm64 || ppc64
-// +build amd64 arm64 ppc64
 
 /*
  * Copyright 2022 CloudWeGo Authors
@@ -51,22 +50,6 @@ import (
 	"github.com/cloudwego/hertz/pkg/common/test/assert"
 )
 
-func TestWriteHexInt(t *testing.T) {
-	t.Parallel()
-
-	for _, v := range []struct {
-		s string
-		n int
-	}{
-		{"0", 0},
-		{"1", 1},
-		{"123", 0x123},
-		{"7fffffffffffffff", 0x7fffffffffffffff},
-	} {
-		testWriteHexInt(t, v.n, v.s)
-	}
-}
-
 func TestReadHexInt(t *testing.T) {
 	t.Parallel()
 
diff --git a/internal/bytesconv/bytesconv_test.go b/internal/bytesconv/bytesconv_test.go
index 316c917..b63f337 100644
--- a/internal/bytesconv/bytesconv_test.go
+++ b/internal/bytesconv/bytesconv_test.go
@@ -22,10 +22,8 @@ import (
 	"testing"
 	"time"
 
-	"github.com/cloudwego/hertz/pkg/common/bytebufferpool"
 	"github.com/cloudwego/hertz/pkg/common/test/assert"
 	"github.com/cloudwego/hertz/pkg/common/test/mock"
-	"github.com/cloudwego/hertz/pkg/network"
 )
 
 func TestAppendDate(t *testing.T) {
@@ -103,18 +101,36 @@ func TestS2b(t *testing.T) {
 	}
 }
 
-// common test function for 32bit and 64bit
-func testWriteHexInt(t *testing.T, n int, expectedS string) {
-	w := bytebufferpool.Get()
-	zw := network.NewWriter(w)
-	if err := WriteHexInt(zw, n); err != nil {
-		t.Errorf("unexpected error when writing hex %x: %v", n, err)
+func TestAppendIntHex(t *testing.T) {
+	testCases := []struct {
+		b        []byte
+		n        uint64
+		expected string
+	}{
+		{[]byte{}, 0, "0"},
+		{[]byte{}, 1, "1"},
+		{[]byte{}, 10, "a"},
+		{[]byte{}, 15, "f"},
+		{[]byte{}, 16, "10"},
+		{[]byte{}, 255, "ff"},
+		{[]byte{}, 256, "100"},
+		{[]byte{}, 123456789, "75bcd15"},
+		{[]byte{}, 0xffffffffffffffff, "ffffffffffffffff"},
+		{[]byte("pre-"), 255, "pre-ff"},
+		{[]byte("start"), 0, "start0"},
 	}
-	if err := zw.Flush(); err != nil {
-		t.Fatalf("unexpected error when flushing hex %x: %v", n, err)
+	for _, tc := range testCases {
+		result := AppendIntHex(tc.b, tc.n)
+		if string(result) != tc.expected {
+			t.Fatalf("AppendIntHex(%q, %d) = %q; want %q", tc.b, tc.n, result, tc.expected)
+		}
+
+		actualLen := EncodedIntHexLen(tc.n)
+		expectedLen := len(result) - len(tc.b)
+		if actualLen != expectedLen {
+			t.Fatalf("EncodedIntHexLen(%d) = %d; want %d", tc.n, actualLen, expectedLen)
+		}
 	}
-	s := B2s(w.B)
-	assert.DeepEqual(t, s, expectedS)
 }
 
 // common test function for 32bit and 64bit
diff --git a/internal/bytestr/bytes.go b/internal/bytestr/bytes.go
index bbf3dcb..bdf530f 100644
--- a/internal/bytestr/bytes.go
+++ b/internal/bytestr/bytes.go
@@ -99,22 +99,26 @@ var (
 	StrCookieSameSiteNone   = []byte("None")
 	StrCookiePartitioned    = []byte("Partitioned")
 
-	StrClose               = []byte("close")
-	StrGzip                = []byte("gzip")
-	StrDeflate             = []byte("deflate")
-	StrKeepAlive           = []byte("keep-alive")
-	StrUpgrade             = []byte("Upgrade")
-	StrChunked             = []byte("chunked")
-	StrIdentity            = []byte("identity")
-	Str100Continue         = []byte("100-continue")
-	StrPostArgsContentType = []byte("application/x-www-form-urlencoded")
-	StrMultipartFormData   = []byte("multipart/form-data")
-	StrBoundary            = []byte("boundary")
-	StrBytes               = []byte("bytes")
-	StrTextSlash           = []byte("text/")
-	StrApplicationSlash    = []byte("application/")
-	StrBasicSpace          = []byte("Basic ")
+	StrClose            = []byte("close")
+	StrGzip             = []byte("gzip")
+	StrDeflate          = []byte("deflate")
+	StrKeepAlive        = []byte("keep-alive")
+	StrUpgrade          = []byte("Upgrade")
+	StrChunked          = []byte("chunked")
+	StrIdentity         = []byte("identity")
+	Str100Continue      = []byte("100-continue")
+	StrBoundary         = []byte("boundary")
+	StrBytes            = []byte("bytes")
+	StrTextSlash        = []byte("text/")
+	StrApplicationSlash = []byte("application/")
+	StrBasicSpace       = []byte("Basic ")
 
 	// http2
 	StrClientPreface = []byte(consts.ClientPreface)
 )
+
+var ( // content types
+	MIMEPostForm        = []byte("application/x-www-form-urlencoded")
+	MIMEFormData        = []byte("multipart/form-data")
+	MIMETextEventStream = []byte("text/event-stream") // for server-sent events
+)
diff --git a/internal/testutils/testutils.go b/internal/testutils/testutils.go
index 4de5835..917d34b 100644
--- a/internal/testutils/testutils.go
+++ b/internal/testutils/testutils.go
@@ -20,6 +20,7 @@ import (
 	"net"
 	"path"
 	"reflect"
+	"time"
 	"unsafe"
 
 	"github.com/cloudwego/hertz/pkg/network"
@@ -95,3 +96,17 @@ func GetListenerAddr(v interface{}) string {
 func GetURL(v interface{}, p string) string {
 	return "http://" + path.Join(GetListenerAddr(v), p)
 }
+
+type RouteEngine interface {
+	IsRunning() bool
+}
+
+func WaitEngineRunning(e RouteEngine) {
+	for i := 0; i < 100; i++ {
+		if e.IsRunning() {
+			return
+		}
+		time.Sleep(10 * time.Millisecond)
+	}
+	panic("not running")
+}
diff --git a/internal/testutils/testutils_test.go b/internal/testutils/testutils_test.go
index 756eded..8317d6b 100644
--- a/internal/testutils/testutils_test.go
+++ b/internal/testutils/testutils_test.go
@@ -20,6 +20,7 @@ import (
 	"context"
 	"io"
 	"net/http"
+	"sync/atomic"
 	"testing"
 	"time"
 
@@ -61,3 +62,20 @@ func TestGetListener(t *testing.T) {
 		t.Fatal(s, "!=", msg)
 	}
 }
+
+type routeEngine struct {
+	Running atomic.Bool
+}
+
+func (e *routeEngine) IsRunning() bool {
+	return e.Running.Load()
+}
+
+func TestWaitEngineRunning(t *testing.T) {
+	e := &routeEngine{}
+	go func() {
+		time.Sleep(30 * time.Millisecond)
+		e.Running.Store(true)
+	}()
+	WaitEngineRunning(e)
+}
diff --git a/pkg/app/client/client_test.go b/pkg/app/client/client_test.go
index 5e182f6..9e4ffde 100644
--- a/pkg/app/client/client_test.go
+++ b/pkg/app/client/client_test.go
@@ -111,13 +111,7 @@ func nextUnixSock() string {
 }
 
 func waitEngineRunning(e *route.Engine) {
-	for i := 0; i < 100; i++ {
-		if e.IsRunning() {
-			return
-		}
-		time.Sleep(10 * time.Millisecond)
-	}
-	panic("not running")
+	testutils.WaitEngineRunning(e)
 }
 
 func TestCloseIdleConnections(t *testing.T) {
diff --git a/pkg/app/client/client_unix_test.go b/pkg/app/client/client_unix_test.go
index 94869e8..5707f8b 100644
--- a/pkg/app/client/client_unix_test.go
+++ b/pkg/app/client/client_unix_test.go
@@ -14,7 +14,6 @@
 //
 
 //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
-// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris
 
 package client
 
diff --git a/pkg/app/client/client_windows_test.go b/pkg/app/client/client_windows_test.go
index 63e60fd..37d5e18 100644
--- a/pkg/app/client/client_windows_test.go
+++ b/pkg/app/client/client_windows_test.go
@@ -14,7 +14,6 @@
 //
 
 //go:build windows
-// +build windows
 
 package client
 
diff --git a/pkg/app/server/hertz_test.go b/pkg/app/server/hertz_test.go
index 2a71947..f86357b 100644
--- a/pkg/app/server/hertz_test.go
+++ b/pkg/app/server/hertz_test.go
@@ -57,13 +57,7 @@ type routeEngine interface {
 }
 
 func waitEngineRunning(e routeEngine) {
-	for i := 0; i < 100; i++ {
-		if e.IsRunning() {
-			return
-		}
-		time.Sleep(10 * time.Millisecond)
-	}
-	panic("not running")
+	testutils.WaitEngineRunning(e)
 }
 
 func TestHertz_Run(t *testing.T) {
@@ -1234,3 +1228,59 @@ func TestWithDisableDefaultContentType(t *testing.T) {
 	r, _ := hc.Get(testutils.GetURL(h, "")) //nolint:errcheck
 	assert.DeepEqual(t, "", r.Header.Get("Content-Type"))
 }
+
+func TestWithSenseClientDisconnection(t *testing.T) {
+	var closeFlag int32
+	h := New(WithHostPorts("127.0.0.1:0"), WithSenseClientDisconnection(true))
+	h.GET("/ping", func(c context.Context, ctx *app.RequestContext) {
+		assert.DeepEqual(t, "aa", string(ctx.Host()))
+		ch := make(chan struct{})
+		select {
+		case <-c.Done():
+			atomic.StoreInt32(&closeFlag, 1)
+			assert.DeepEqual(t, context.Canceled, c.Err())
+		case <-ch:
+		}
+	})
+	go h.Spin()
+	waitEngineRunning(h)
+
+	con, err := net.Dial("tcp", testutils.GetListenerAddr(h))
+	assert.Nil(t, err)
+	_, err = con.Write([]byte("GET /ping HTTP/1.1\r\nHost: aa\r\n\r\n"))
+	assert.Nil(t, err)
+	time.Sleep(20 * time.Millisecond)
+	assert.DeepEqual(t, atomic.LoadInt32(&closeFlag), int32(0))
+	assert.Nil(t, con.Close())
+	time.Sleep(20 * time.Millisecond)
+	assert.DeepEqual(t, atomic.LoadInt32(&closeFlag), int32(1))
+}
+
+func TestWithSenseClientDisconnectionAndWithOnConnect(t *testing.T) {
+	var closeFlag int32
+	h := New(WithHostPorts("127.0.0.1:0"), WithSenseClientDisconnection(true), WithOnConnect(func(ctx context.Context, conn network.Conn) context.Context {
+		return ctx
+	}))
+	h.GET("/ping", func(c context.Context, ctx *app.RequestContext) {
+		assert.DeepEqual(t, "aa", string(ctx.Host()))
+		ch := make(chan struct{})
+		select {
+		case <-c.Done():
+			atomic.StoreInt32(&closeFlag, 1)
+			assert.DeepEqual(t, context.Canceled, c.Err())
+		case <-ch:
+		}
+	})
+	go h.Spin()
+	waitEngineRunning(h)
+
+	con, err := net.Dial("tcp", testutils.GetListenerAddr(h))
+	assert.Nil(t, err)
+	_, err = con.Write([]byte("GET /ping HTTP/1.1\r\nHost: aa\r\n\r\n"))
+	assert.Nil(t, err)
+	time.Sleep(20 * time.Millisecond)
+	assert.DeepEqual(t, atomic.LoadInt32(&closeFlag), int32(0))
+	assert.Nil(t, con.Close())
+	time.Sleep(20 * time.Millisecond)
+	assert.DeepEqual(t, atomic.LoadInt32(&closeFlag), int32(1))
+}
diff --git a/pkg/app/server/hertz_unix_test.go b/pkg/app/server/hertz_unix_test.go
index 7e14c94..5337666 100644
--- a/pkg/app/server/hertz_unix_test.go
+++ b/pkg/app/server/hertz_unix_test.go
@@ -14,7 +14,6 @@
 //
 
 //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
-// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris
 
 package server
 
@@ -35,7 +34,6 @@ import (
 	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"
 	"github.com/cloudwego/hertz/pkg/network/standard"
 	"github.com/cloudwego/hertz/pkg/protocol/consts"
 	"golang.org/x/sys/unix"
@@ -139,59 +137,3 @@ func TestHertz_Spin(t *testing.T) {
 	<-ch2
 	assert.DeepEqual(t, uint32(1), atomic.LoadUint32(&testint))
 }
-
-func TestWithSenseClientDisconnection(t *testing.T) {
-	var closeFlag int32
-	h := New(WithHostPorts("127.0.0.1:0"), WithSenseClientDisconnection(true))
-	h.GET("/ping", func(c context.Context, ctx *app.RequestContext) {
-		assert.DeepEqual(t, "aa", string(ctx.Host()))
-		ch := make(chan struct{})
-		select {
-		case <-c.Done():
-			atomic.StoreInt32(&closeFlag, 1)
-			assert.DeepEqual(t, context.Canceled, c.Err())
-		case <-ch:
-		}
-	})
-	go h.Spin()
-	waitEngineRunning(h)
-
-	con, err := net.Dial("tcp", testutils.GetListenerAddr(h))
-	assert.Nil(t, err)
-	_, err = con.Write([]byte("GET /ping HTTP/1.1\r\nHost: aa\r\n\r\n"))
-	assert.Nil(t, err)
-	time.Sleep(20 * time.Millisecond)
-	assert.DeepEqual(t, atomic.LoadInt32(&closeFlag), int32(0))
-	assert.Nil(t, con.Close())
-	time.Sleep(20 * time.Millisecond)
-	assert.DeepEqual(t, atomic.LoadInt32(&closeFlag), int32(1))
-}
-
-func TestWithSenseClientDisconnectionAndWithOnConnect(t *testing.T) {
-	var closeFlag int32
-	h := New(WithHostPorts("127.0.0.1:0"), WithSenseClientDisconnection(true), WithOnConnect(func(ctx context.Context, conn network.Conn) context.Context {
-		return ctx
-	}))
-	h.GET("/ping", func(c context.Context, ctx *app.RequestContext) {
-		assert.DeepEqual(t, "aa", string(ctx.Host()))
-		ch := make(chan struct{})
-		select {
-		case <-c.Done():
-			atomic.StoreInt32(&closeFlag, 1)
-			assert.DeepEqual(t, context.Canceled, c.Err())
-		case <-ch:
-		}
-	})
-	go h.Spin()
-	waitEngineRunning(h)
-
-	con, err := net.Dial("tcp", testutils.GetListenerAddr(h))
-	assert.Nil(t, err)
-	_, err = con.Write([]byte("GET /ping HTTP/1.1\r\nHost: aa\r\n\r\n"))
-	assert.Nil(t, err)
-	time.Sleep(20 * time.Millisecond)
-	assert.DeepEqual(t, atomic.LoadInt32(&closeFlag), int32(0))
-	assert.Nil(t, con.Close())
-	time.Sleep(20 * time.Millisecond)
-	assert.DeepEqual(t, atomic.LoadInt32(&closeFlag), int32(1))
-}
diff --git a/pkg/common/adaptor/handler_test.go b/pkg/common/adaptor/handler_test.go
new file mode 100644
index 0000000..0facc2b
--- /dev/null
+++ b/pkg/common/adaptor/handler_test.go
@@ -0,0 +1,255 @@
+/*
+ * Copyright 2025 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 adaptor
+
+import (
+	"bytes"
+	"context"
+	"embed"
+	"fmt"
+	"io"
+	"mime/multipart"
+	"net"
+	"net/http"
+	"strings"
+	"sync"
+	"testing"
+	"time"
+
+	"github.com/cloudwego/hertz/internal/testutils"
+	"github.com/cloudwego/hertz/pkg/app"
+	"github.com/cloudwego/hertz/pkg/common/config"
+	"github.com/cloudwego/hertz/pkg/common/test/assert"
+	"github.com/cloudwego/hertz/pkg/route"
+)
+
+//go:embed *
+var adaptorFiles embed.FS
+
+func runEngine(onCreate func(*route.Engine)) (string, *route.Engine) {
+	opt := config.NewOptions(nil)
+	opt.Addr = "127.0.0.1:0"
+	engine := route.NewEngine(opt)
+	onCreate(engine)
+	go engine.Run()
+	testutils.WaitEngineRunning(engine)
+	return testutils.GetListenerAddr(engine), engine
+}
+
+func TestHertzHandler_BodyStream(t *testing.T) {
+	var wg sync.WaitGroup
+	wg.Add(1)
+	h := HertzHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		defer wg.Done()
+		b := make([]byte, 100)
+		for i := 0; i < 3; i++ { // reading chunked data
+			n, err := r.Body.Read(b)
+			assert.Nil(t, err)
+			assert.Assert(t, n == 5, n)
+			assert.Assert(t, string(b[:n]) == "hello")
+		}
+		n, err := r.Body.Read(b)
+		assert.Assert(t, err == io.EOF)
+		assert.Assert(t, n == 0)
+	}))
+	addr, e := runEngine(func(e *route.Engine) {
+		e.GetOptions().StreamRequestBody = true
+		e.POST("/test", h)
+	})
+	defer e.Close()
+
+	r, w := io.Pipe() // for sending chunked data
+	req, err := http.NewRequest("POST", "http://"+addr+"/test", r)
+	assert.Nil(t, err)
+	cli := &http.Client{}
+	go cli.Do(req)
+	for i := 0; i < 3; i++ {
+		w.Write([]byte("hello"))
+		time.Sleep(50 * time.Millisecond)
+	}
+	w.Close()
+	wg.Wait()
+}
+
+func TestHertzHandler_Chunked(t *testing.T) {
+	h := HertzHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		f := w.(http.Flusher)
+		w.Header().Set("Transfer-Encoding", "chunked")
+		for i := 0; i < 5; i++ {
+			chunk := fmt.Sprintf("data:%d", i)
+			_, err := w.Write([]byte(chunk))
+			assert.Nil(t, err)
+			f.Flush()
+			time.Sleep(20 * time.Millisecond)
+		}
+	}))
+	addr, e := runEngine(func(e *route.Engine) {
+		e.GET("/test", h)
+	})
+	defer e.Close()
+
+	resp, err := http.Get("http://" + addr + "/test")
+	assert.Nil(t, err)
+	defer resp.Body.Close()
+	assert.Assert(t, len(resp.TransferEncoding) == 1 && resp.TransferEncoding[0] == "chunked")
+	for i := 0; i < 5; i++ {
+		b := make([]byte, 10)
+		n, err := resp.Body.Read(b)
+		assert.Nil(t, err)
+		assert.Assert(t, string(b[:n]) == fmt.Sprintf("data:%d", i))
+	}
+}
+
+func TestHertzHandler_Hijack(t *testing.T) {
+	h := HertzHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		_, rw, err := w.(http.Hijacker).Hijack()
+		assert.Nil(t, err)
+		_, _, err = w.(http.Hijacker).Hijack() // hijacked
+		assert.NotNil(t, err)
+
+		w.WriteHeader(500) // hijacked, noop
+		_, err = w.Write([]byte("hello"))
+		assert.Assert(t, err == errConnHijacked)
+
+		rw.Write([]byte("hello"))
+		rw.Flush()
+		b := make([]byte, 10)
+		n, err := rw.Read(b)
+		assert.Nil(t, err)
+		assert.Assert(t, string(b[:n]) == "world")
+	}))
+	addr, e := runEngine(func(e *route.Engine) {
+		e.GET("/test", h)
+	})
+	defer e.Close()
+
+	conn, err := net.Dial("tcp", addr)
+	assert.Nil(t, err)
+	defer conn.Close()
+	conn.Write([]byte("GET /test HTTP/1.1\r\nHost: example.com\r\n\r\n"))
+	b := make([]byte, 100)
+	n, err := conn.Read(b)
+	assert.Nil(t, err)
+	assert.Assert(t, string(b[:n]) == "hello", string(b[:n]))
+	_, err = conn.Write([]byte("world"))
+	assert.Nil(t, err)
+
+	n, err = conn.Read(b) // Keep-Alive will not work if hijacked
+	assert.Assert(t, err == io.EOF)
+	assert.Assert(t, n == 0)
+}
+
+func TestHertzHandler_FSEmbed(t *testing.T) {
+	addr, e := runEngine(func(e *route.Engine) {
+		h := HertzHandler(http.FileServer(http.FS(adaptorFiles)))
+		e.GET("/*filepath", h)
+		e.HEAD("/*filepath", h)
+	})
+	defer e.Close()
+
+	resp, err := http.Get("http://" + addr + "/handler_test.go")
+	assert.Nil(t, err)
+
+	expect := "hello, I'm handler_test.go"
+
+	b, err := io.ReadAll(resp.Body)
+	s := string(b)
+	assert.Nil(t, err)
+	assert.Assert(t, strings.Contains(s, expect), s)
+}
+
+func TestHertzHandler_Multipart(t *testing.T) {
+	kvs := map[string]string{
+		"name":  "Alice",
+		"email": "alice@example.com",
+	}
+	h := HertzHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		for k, expectv := range kvs {
+			v := r.FormValue(k)
+			assert.Assert(t, v == expectv, v)
+		}
+		w.WriteHeader(204)
+		w.Write([]byte("hello"))
+	}))
+	addr, e := runEngine(func(e *route.Engine) {
+		opts := e.GetOptions()
+		opts.StreamRequestBody = true
+		opts.DisablePreParseMultipartForm = true
+		e.POST("/test", func(ctx context.Context, rc *app.RequestContext) {
+			_, err := rc.MultipartForm() // call rc.MultipartForm before HertzHandler
+			assert.Nil(t, err)
+			h(ctx, rc)
+		})
+	})
+	defer e.Close()
+
+	body, ct := createMultipartBody(kvs)
+	req, err := http.NewRequest("POST", "http://"+addr+"/test", bytes.NewReader(body.Bytes()))
+	assert.Nil(t, err)
+	req.Header.Set("Content-Type", ct)
+
+	client := &http.Client{}
+	resp, err := client.Do(req)
+	assert.Nil(t, err)
+	assert.Assert(t, resp.StatusCode == 204, resp.StatusCode)
+	resp.Body.Close()
+}
+
+func createMultipartBody(kvs map[string]string) (*bytes.Buffer, string) {
+	buf := &bytes.Buffer{}
+	w := multipart.NewWriter(buf)
+	for k, v := range kvs {
+		_ = w.WriteField(k, v)
+	}
+	_ = w.Close()
+	return buf, w.FormDataContentType()
+}
+
+func TestNoopHijackWriter(t *testing.T) {
+	writer := noopHijackWriter{}
+
+	// Test Write method
+	n, err := writer.Write([]byte("test"))
+	assert.Assert(t, n == 0, n)
+	assert.Assert(t, err == errConnHijacked, err)
+
+	// Test Flush method
+	err = writer.Flush()
+	assert.Assert(t, err == errConnHijacked, err)
+
+	// Test Finalize method
+	err = writer.Finalize()
+	assert.Nil(t, err)
+}
+
+func TestNoopWriter(t *testing.T) {
+	writer := noopWriter{}
+
+	// Test Write method
+	testData := []byte("test data")
+	n, err := writer.Write(testData)
+	assert.Assert(t, n == len(testData), n)
+	assert.Nil(t, err)
+
+	// Test Flush method
+	err = writer.Flush()
+	assert.Nil(t, err)
+
+	// Test Finalize method
+	err = writer.Finalize()
+	assert.Nil(t, err)
+}
diff --git a/pkg/common/adaptor/utils_test.go b/pkg/common/adaptor/utils_test.go
new file mode 100644
index 0000000..827f39d
--- /dev/null
+++ b/pkg/common/adaptor/utils_test.go
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2025 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 adaptor
+
+import (
+	"runtime"
+	"testing"
+
+	"github.com/cloudwego/hertz/internal/bytestr"
+	"github.com/cloudwego/hertz/pkg/common/test/assert"
+)
+
+func TestMethodStr(t *testing.T) {
+	var m0, m1 runtime.MemStats
+	assertEqual := func(got, expect string) {
+		if got != expect {
+			t.Helper()
+			t.Fatal(got)
+		}
+	}
+	runtime.ReadMemStats(&m0)
+	for i := 0; i < 1000; i++ {
+		assertEqual(methodstr([]byte(nil)), "GET")
+		assertEqual(methodstr(bytestr.StrGet), "GET")
+		assertEqual(methodstr(bytestr.StrHead), "HEAD")
+		assertEqual(methodstr(bytestr.StrPost), "POST")
+		assertEqual(methodstr(bytestr.StrPut), "PUT")
+		assertEqual(methodstr(bytestr.StrDelete), "DELETE")
+		assertEqual(methodstr(bytestr.StrPatch), "PATCH")
+	}
+	runtime.ReadMemStats(&m1)
+
+	// should be zero, but in case of other background task running
+	diff := m1.Mallocs - m0.Mallocs
+	assert.Assert(t, diff < 50, diff)
+}
+
+func TestBytesRWCloser(t *testing.T) {
+	// Test data
+	testData := []byte("test bytesRWCloser")
+
+	// Create a new bytesRWCloser
+	rc := newBytesRWCloser(testData)
+
+	// Read from the reader
+	buf := make([]byte, len(testData))
+	n, err := rc.Read(buf)
+
+	// Verify read was successful
+	assert.Nil(t, err)
+	assert.DeepEqual(t, n, len(testData))
+	assert.DeepEqual(t, buf, testData)
+
+	// Test that Close returns nil
+	err = rc.Close()
+	assert.Nil(t, err)
+}
+
+func TestParseHTTPVersion(t *testing.T) {
+	// Test HTTP/1.0
+	major, minor, err := parseHTTPVersion("HTTP/1.0")
+	assert.Nil(t, err)
+	assert.DeepEqual(t, major, 1)
+	assert.DeepEqual(t, minor, 0)
+
+	// Test HTTP/1.1
+	major, minor, err = parseHTTPVersion("HTTP/1.1")
+	assert.Nil(t, err)
+	assert.DeepEqual(t, major, 1)
+	assert.DeepEqual(t, minor, 1)
+
+	// Test HTTP/2.0
+	major, minor, err = parseHTTPVersion("HTTP/2.0")
+	assert.Nil(t, err)
+	assert.DeepEqual(t, major, 2)
+	assert.DeepEqual(t, minor, 0)
+
+	// Test HTTP/3.1
+	major, minor, err = parseHTTPVersion("HTTP/3.1")
+	assert.Nil(t, err)
+	assert.DeepEqual(t, major, 3)
+	assert.DeepEqual(t, minor, 1)
+
+	// Test missing HTTP prefix
+	major, minor, err = parseHTTPVersion("1.1")
+	assert.NotNil(t, err)
+	assert.DeepEqual(t, major, 1)
+	assert.DeepEqual(t, minor, 1)
+
+	// Test missing dot separator
+	major, minor, err = parseHTTPVersion("HTTP/11")
+	assert.NotNil(t, err)
+	assert.DeepEqual(t, major, 1)
+	assert.DeepEqual(t, minor, 1)
+
+	// Test empty string
+	major, minor, err = parseHTTPVersion("")
+	assert.NotNil(t, err)
+	assert.DeepEqual(t, major, 1)
+	assert.DeepEqual(t, minor, 1)
+}
diff --git a/pkg/common/test/mock/network.go b/pkg/common/test/mock/network.go
index b3ae54b..366e335 100644
--- a/pkg/common/test/mock/network.go
+++ b/pkg/common/test/mock/network.go
@@ -374,3 +374,36 @@ func (m *StreamConn) ReadBinary(n int) (p []byte, err error) {
 func DialerFun(addr string) (network.Conn, error) {
 	return NewConn(""), nil
 }
+
+type MockWriter struct {
+	w network.Writer
+
+	MockMalloc      func(n int) (buf []byte, err error)
+	MockWriteBinary func(b []byte) (n int, err error)
+	MockFlush       func() error
+}
+
+func NewMockWriter(w network.Writer) *MockWriter {
+	return &MockWriter{w: w}
+}
+
+func (m *MockWriter) Malloc(n int) (buf []byte, err error) {
+	if m.MockMalloc != nil {
+		return m.MockMalloc(n)
+	}
+	return m.w.Malloc(n)
+}
+
+func (m *MockWriter) WriteBinary(b []byte) (n int, err error) {
+	if m.MockWriteBinary != nil {
+		return m.MockWriteBinary(b)
+	}
+	return m.w.WriteBinary(b)
+}
+
+func (m *MockWriter) Flush() error {
+	if m.MockFlush != nil {
+		return m.MockFlush()
+	}
+	return m.w.Flush()
+}
diff --git a/pkg/network/netpoll/connection_test.go b/pkg/network/netpoll/connection_test.go
index 2661056..7a4ff07 100644
--- a/pkg/network/netpoll/connection_test.go
+++ b/pkg/network/netpoll/connection_test.go
@@ -14,7 +14,6 @@
 //
 
 //go:build !windows
-// +build !windows
 
 package netpoll
 
diff --git a/pkg/network/netpoll/dial_test.go b/pkg/network/netpoll/dial_test.go
index bfea1be..f867425 100644
--- a/pkg/network/netpoll/dial_test.go
+++ b/pkg/network/netpoll/dial_test.go
@@ -14,7 +14,6 @@
 //
 
 //go:build !windows
-// +build !windows
 
 package netpoll
 
diff --git a/pkg/network/netpoll/transport_test.go b/pkg/network/netpoll/transport_test.go
index d6383eb..2a1e4ed 100644
--- a/pkg/network/netpoll/transport_test.go
+++ b/pkg/network/netpoll/transport_test.go
@@ -14,7 +14,6 @@
 //
 
 //go:build !windows
-// +build !windows
 
 package netpoll
 
diff --git a/pkg/network/standard/connection_test.go b/pkg/network/standard/connection_test.go
index fa950f3..f0796cf 100644
--- a/pkg/network/standard/connection_test.go
+++ b/pkg/network/standard/connection_test.go
@@ -18,6 +18,7 @@ package standard
 
 import (
 	"bytes"
+	"context"
 	"crypto/tls"
 	"errors"
 	"io"
@@ -194,6 +195,34 @@ func TestReadBytes(t *testing.T) {
 	}
 }
 
+func TestStatefulConnConnectionCloseDetection(t *testing.T) {
+	cliConn, svrConn := net.Pipe()
+	svrCtx, cancelCtx := context.WithCancel(context.Background())
+	svrStatefulConn := NewStatefulConn(newConn(svrConn, 4096), newOnReadErrorFunc(cancelCtx)).(*statefulConn)
+	defer svrStatefulConn.Close()
+
+	// 1. normal request
+	svrStatefulConn.DetectConnectionClose()
+	svrStatefulConn.AbortBlockingRead()
+	select {
+	case <-svrCtx.Done():
+		t.Fatal("ctx should not be canceled")
+	default:
+	}
+
+	// 2. client close conn
+	svrStatefulConn.DetectConnectionClose()
+	cliConn.Close()
+	time.Sleep(100 * time.Millisecond) // wait a while
+
+	select {
+	case <-svrCtx.Done():
+		// expected
+	default:
+		t.Fatal("ctx should be canceled")
+	}
+}
+
 func TestWriteLogic(t *testing.T) {
 	c := mockConn{}
 	conn := newConn(&c, 4096)
diff --git a/pkg/network/standard/dial_test.go b/pkg/network/standard/dial_test.go
index 0c7e854..66f0fcf 100644
--- a/pkg/network/standard/dial_test.go
+++ b/pkg/network/standard/dial_test.go
@@ -40,7 +40,7 @@ func getListenerAddr(trans network.Transporter) string {
 
 func TestDial(t *testing.T) {
 	const nw = "tcp"
-	var addr = "127.0.0.1:0"
+	addr := "127.0.0.1:0"
 	transporter := NewTransporter(&config.Options{
 		Addr:    addr,
 		Network: nw,
@@ -65,7 +65,7 @@ func TestDial(t *testing.T) {
 
 func TestDialTLS(t *testing.T) {
 	const nw = "tcp"
-	var addr = "127.0.0.1:0"
+	addr := "127.0.0.1:0"
 	data := []byte("abcdefg")
 	listened := make(chan net.Listener)
 	go func() {
diff --git a/pkg/network/standard/transport_test.go b/pkg/network/standard/transport_test.go
index 57698d4..a38861a 100644
--- a/pkg/network/standard/transport_test.go
+++ b/pkg/network/standard/transport_test.go
@@ -23,11 +23,13 @@ import (
 	"testing"
 	"time"
 
+	internalNetwork "github.com/cloudwego/hertz/internal/network"
+
 	"github.com/cloudwego/hertz/pkg/common/config"
 	"github.com/cloudwego/hertz/pkg/network"
 )
 
-func assertWriteRead(t *testing.T, c io.ReadWriter, w string, r string) {
+func assertWriteRead(t *testing.T, c io.ReadWriter, w, r string) {
 	t.Helper()
 	_, err := c.Write([]byte(w))
 	if err != nil {
@@ -47,8 +49,12 @@ func TestTransporter(t *testing.T) {
 	handlerExit := make(chan struct{})
 	req := "hello"
 	resp := "world"
-	trans := NewTransporter(&config.Options{Network: "tcp", Addr: "127.0.0.1:0"}).(*transport)
+	trans := NewTransporter(&config.Options{Network: "tcp", Addr: "127.0.0.1:0", SenseClientDisconnection: true}).(*transport)
 	go trans.ListenAndServe(func(ctx context.Context, conn interface{}) error {
+		_, isStatefulConn := conn.(internalNetwork.StatefulConn)
+		if !isStatefulConn {
+			t.Fatal("SenseClientDisconnection configure failed")
+		}
 		c := conn.(network.Conn)
 		defer c.Close()
 		assertWriteRead(t, c, resp, req)
diff --git a/pkg/protocol/http1/client_test.go b/pkg/protocol/http1/client_test.go
index 353cf61..f73bea7 100644
--- a/pkg/protocol/http1/client_test.go
+++ b/pkg/protocol/http1/client_test.go
@@ -64,6 +64,7 @@ import (
 	"github.com/cloudwego/hertz/pkg/common/test/mock"
 	"github.com/cloudwego/hertz/pkg/common/utils"
 	"github.com/cloudwego/hertz/pkg/network"
+	"github.com/cloudwego/hertz/pkg/network/dialer"
 	"github.com/cloudwego/hertz/pkg/protocol"
 	"github.com/cloudwego/hertz/pkg/protocol/client"
 	"github.com/cloudwego/hertz/pkg/protocol/consts"
@@ -145,6 +146,36 @@ func TestHostClientMaxConnWaitTimeoutWithEarlierDeadline(t *testing.T) {
 	}
 }
 
+func TestReadHeaderErr(t *testing.T) {
+	ln, _ := net.Listen("tcp", "localhost:0")
+	defer ln.Close()
+	svr := http.Server{}
+	svr.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		hj := w.(http.Hijacker)
+		conn, rw, err := hj.Hijack()
+		assert.Nil(t, err)
+		defer conn.Close()
+		rw.Write([]byte("HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Ty"))
+		rw.Flush()
+	})
+	go svr.Serve(ln)
+
+	req := protocol.AcquireRequest()
+	defer protocol.ReleaseRequest(req)
+	req.SetRequestURI("http://" + ln.Addr().String())
+
+	resp := protocol.AcquireResponse()
+	defer protocol.ReleaseResponse(resp)
+	c := &HostClient{
+		Addr: ln.Addr().String(),
+		ClientOptions: &ClientOptions{
+			Dialer: dialer.DefaultDialer(),
+		},
+	}
+	err := c.Do(context.Background(), req, resp)
+	assert.NotNil(t, err)
+}
+
 func TestResponseReadBodyStream(t *testing.T) {
 	// small body
 	genBody := "abcdef4343"
@@ -182,7 +213,7 @@ func TestResponseReadBodyStream(t *testing.T) {
 func testContinueReadResponseBodyStream(t *testing.T, header, body string, maxBodySize, firstRead, leftBytes, bytesLeftInReader int) {
 	mr := netpoll.NewReader(bytes.NewBufferString(header + body))
 	var r protocol.Response
-	if err := resp.ReadBodyStream(&r, mr, maxBodySize, nil); err != nil {
+	if err := resp.ReadHeaderBodyStream(&r, mr, maxBodySize, nil); err != nil {
 		t.Fatalf("error when reading request body stream: %s", err)
 	}
 	fRead := firstRead
@@ -257,7 +288,7 @@ func TestReadTimeoutPriority(t *testing.T) {
 
 	req := protocol.AcquireRequest()
 	req.SetRequestURI("http://foobar/baz")
-	req.SetOptions(config.WithReadTimeout(time.Second * 1))
+	req.SetOptions(config.WithReadTimeout(200 * time.Millisecond))
 	resp := protocol.AcquireResponse()
 
 	ch := make(chan error, 1)
@@ -293,14 +324,11 @@ func TestDoNonNilReqResp(t *testing.T) {
 	assert.DeepEqual(t, resp.Body(), []byte("123456"))
 }
 
-func TestDoNonNilReqResp1(t *testing.T) {
+func TestDoNonNilReqResp_Err(t *testing.T) {
 	c := &HostClient{
 		ClientOptions: &ClientOptions{
 			Dialer: newSlowConnDialer(func(network, addr string, timeout time.Duration) (network.Conn, error) {
-				return &writeErrConn{
-						Conn: mock.NewConn(""),
-					},
-					nil
+				return peekErrConn{writeErrConn{mock.NewConn("")}}, nil
 			}),
 		},
 	}
@@ -310,9 +338,55 @@ func TestDoNonNilReqResp1(t *testing.T) {
 	retry, err := c.doNonNilReqResp(req, resp)
 	assert.True(t, retry)
 	assert.NotNil(t, err)
+	assert.Assert(t, err == errs.ErrConnectionClosed, err) // returned by writeErrConn
 }
 
-func TestConnUpgrade(t *testing.T) {
+func doGET(t *testing.T, addr string, path string) *protocol.Response {
+	req := protocol.AcquireRequest()
+	defer protocol.ReleaseRequest(req)
+	req.SetRequestURI("http://" + addr + path)
+
+	resp := protocol.AcquireResponse()
+	c := &HostClient{
+		Addr: addr,
+		ClientOptions: &ClientOptions{
+			Dialer: dialer.DefaultDialer(),
+		},
+	}
+	err := c.Do(context.Background(), req, resp)
+	assert.Nil(t, err)
+	return resp
+}
+
+func TestStreamResponse_EventStream(t *testing.T) {
+	ln, _ := net.Listen("tcp", "localhost:0")
+	defer ln.Close()
+	svr := http.Server{}
+	svr.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("Content-Type", "text/event-stream")
+		f := w.(http.Flusher)
+		for i := 0; i < 5; i++ {
+			_, err := w.Write([]byte(fmt.Sprintf("data:%d", i)))
+			assert.Nil(t, err)
+			f.Flush() // Transfer-Encoding chunked
+			time.Sleep(20 * time.Millisecond)
+		}
+	})
+	go svr.Serve(ln)
+
+	resp := doGET(t, ln.Addr().String(), "/")
+	defer protocol.ReleaseResponse(resp)
+	assert.Assert(t, resp.IsBodyStream())
+	r := resp.BodyStream()
+	b := make([]byte, 10)
+	for i := 0; i < 5; i++ {
+		n, err := r.Read(b)
+		assert.Nil(t, err)
+		assert.Assert(t, string(b[:n]) == fmt.Sprintf("data:%d", i))
+	}
+}
+
+func TestStreamResponse_ConnUpgrade(t *testing.T) {
 	ln, _ := net.Listen("tcp", "localhost:0")
 	defer ln.Close()
 	svr := http.Server{}
@@ -346,16 +420,8 @@ func TestConnUpgrade(t *testing.T) {
 	})
 	go svr.Serve(ln)
 
-	c := &HostClient{
-		Addr:          ln.Addr().String(),
-		ClientOptions: &ClientOptions{},
-	}
-	req := protocol.AcquireRequest()
-	req.SetRequestURI("http://" + ln.Addr().String() + "/")
-	resp := protocol.AcquireResponse()
-	retry, err := c.doNonNilReqResp(req, resp)
-	assert.False(t, retry)
-	assert.Nil(t, err)
+	resp := doGET(t, ln.Addr().String(), "/")
+	defer protocol.ReleaseResponse(resp)
 	assert.DeepEqual(t, resp.StatusCode(), 101)
 
 	s := resp.BodyStream()
@@ -385,7 +451,7 @@ func TestWriteTimeoutPriority(t *testing.T) {
 
 	req := protocol.AcquireRequest()
 	req.SetRequestURI("http://foobar/baz")
-	req.SetOptions(config.WithWriteTimeout(time.Second * 1))
+	req.SetOptions(config.WithWriteTimeout(200 * time.Millisecond))
 	resp := protocol.AcquireResponse()
 
 	ch := make(chan error, 1)
@@ -413,7 +479,7 @@ func TestDialTimeoutPriority(t *testing.T) {
 
 	req := protocol.AcquireRequest()
 	req.SetRequestURI("http://foobar/baz")
-	req.SetOptions(config.WithDialTimeout(time.Second * 1))
+	req.SetOptions(config.WithDialTimeout(200 * time.Millisecond))
 	resp := protocol.AcquireResponse()
 
 	ch := make(chan error, 1)
@@ -534,6 +600,14 @@ func (w writeErrConn) WriteBinary(b []byte) (n int, err error) {
 	return 0, errs.ErrConnectionClosed
 }
 
+type peekErrConn struct {
+	network.Conn
+}
+
+func (c peekErrConn) Peek(n int) ([]byte, error) {
+	return nil, errors.New("peek err")
+}
+
 type retryConn struct {
 	network.Conn
 }
diff --git a/pkg/protocol/http1/client_unix_test.go b/pkg/protocol/http1/client_unix_test.go
index 1e88502..1a162ca 100644
--- a/pkg/protocol/http1/client_unix_test.go
+++ b/pkg/protocol/http1/client_unix_test.go
@@ -13,13 +13,13 @@
 // limitations under the License.
 //
 //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
-// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris
 
 package http1
 
 import (
 	"context"
 	"errors"
+	"net"
 	"net/http"
 	"runtime"
 	"sync"
@@ -35,25 +35,28 @@ import (
 )
 
 func TestGcBodyStream(t *testing.T) {
-	srv := &http.Server{Addr: "127.0.0.1:11001", Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+	ln, err := net.Listen("tcp", "127.0.0.1:0")
+	assert.Nil(t, err)
+	defer ln.Close()
+	srv := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
 		for range [1024]int{} {
 			w.Write([]byte("hello world\n"))
 		}
 	})}
-	go srv.ListenAndServe()
-	time.Sleep(100 * time.Millisecond)
+	go srv.Serve(ln)
 
+	addr := ln.Addr().String()
 	c := &HostClient{
 		ClientOptions: &ClientOptions{
 			Dialer:             netpoll.NewDialer(),
 			ResponseBodyStream: true,
 		},
-		Addr: "127.0.0.1:11001",
+		Addr: addr,
 	}
 
 	for i := 0; i < 10; i++ {
 		req, resp := protocol.AcquireRequest(), protocol.AcquireResponse()
-		req.SetRequestURI("http://127.0.0.1:11001")
+		req.SetRequestURI("http://" + addr)
 		req.SetMethod(consts.MethodPost)
 		err := c.Do(context.Background(), req, resp)
 		if err != nil {
@@ -69,12 +72,15 @@ func TestGcBodyStream(t *testing.T) {
 }
 
 func TestMaxConn(t *testing.T) {
-	srv := &http.Server{Addr: "127.0.0.1:11002", Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+	ln, err := net.Listen("tcp", "127.0.0.1:0")
+	assert.Nil(t, err)
+	defer ln.Close()
+	srv := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
 		w.Write([]byte("hello world\n"))
 	})}
-	go srv.ListenAndServe()
-	time.Sleep(100 * time.Millisecond)
+	go srv.Serve(ln)
 
+	addr := ln.Addr().String()
 	c := &HostClient{
 		ClientOptions: &ClientOptions{
 			Dialer:             netpoll.NewDialer(),
@@ -82,7 +88,7 @@ func TestMaxConn(t *testing.T) {
 			MaxConnWaitTimeout: time.Millisecond * 100,
 			MaxConns:           5,
 		},
-		Addr: "127.0.0.1:11002",
+		Addr: addr,
 	}
 
 	var successCount int32
@@ -93,7 +99,7 @@ func TestMaxConn(t *testing.T) {
 		go func() {
 			defer wg.Done()
 			req, resp := protocol.AcquireRequest(), protocol.AcquireResponse()
-			req.SetRequestURI("http://127.0.0.1:11002")
+			req.SetRequestURI("http://" + addr)
 			req.SetMethod(consts.MethodPost)
 			err := c.Do(context.Background(), req, resp)
 			if err != nil {
diff --git a/pkg/protocol/http1/resp/response_test.go b/pkg/protocol/http1/resp/response_test.go
index 3c206e4..ea21ea6 100644
--- a/pkg/protocol/http1/resp/response_test.go
+++ b/pkg/protocol/http1/resp/response_test.go
@@ -732,7 +732,7 @@ func testResponseReadBodyStreamSuccess(t *testing.T, resp *protocol.Response, re
 	expectedContentType, expectedBody string, expectedTrailer map[string]string, expectedProtocol string,
 ) {
 	zr := mock.NewZeroCopyReader(response)
-	err := ReadBodyStream(resp, zr, 0, nil)
+	err := ReadHeaderBodyStream(resp, zr, 0, nil)
 	if err != nil {
 		t.Fatalf("Unexpected error: %s", err)
 	}
@@ -751,7 +751,7 @@ func testResponseReadBodyStreamSuccess(t *testing.T, resp *protocol.Response, re
 
 func testResponseReadBodyStreamBadTrailer(t *testing.T, resp *protocol.Response, response string) {
 	zr := mock.NewZeroCopyReader(response)
-	err := ReadBodyStream(resp, zr, 0, nil)
+	err := ReadHeaderBodyStream(resp, zr, 0, nil)
 	if err != nil {
 		t.Fatalf("Unexpected error: %s", err)
 	}
diff --git a/pkg/protocol/http1/resp/writer_test.go b/pkg/protocol/http1/resp/writer_test.go
index b57281a..80bec2f 100644
--- a/pkg/protocol/http1/resp/writer_test.go
+++ b/pkg/protocol/http1/resp/writer_test.go
@@ -17,10 +17,10 @@
 package resp
 
 import (
+	"errors"
 	"strings"
 	"testing"
 
-	"github.com/cloudwego/hertz/internal/bytestr"
 	"github.com/cloudwego/hertz/pkg/common/test/assert"
 	"github.com/cloudwego/hertz/pkg/common/test/mock"
 	"github.com/cloudwego/hertz/pkg/protocol"
@@ -28,39 +28,52 @@ import (
 
 func TestNewChunkedBodyWriter(t *testing.T) {
 	response := protocol.AcquireResponse()
-	mockConn := mock.NewConn("")
-	w := NewChunkedBodyWriter(response, mockConn)
-	w.Write([]byte("hello"))
-	w.Flush()
-	out, _ := mockConn.WriterRecorder().ReadBinary(mockConn.WriterRecorder().WroteLen())
-	assert.True(t, strings.Contains(string(out), "Transfer-Encoding: chunked"))
-	assert.True(t, strings.Contains(string(out), "5"+string(bytestr.StrCRLF)+"hello"))
-	assert.False(t, strings.Contains(string(out), "0"+string(bytestr.StrCRLF)+string(bytestr.StrCRLF)))
-}
+	defer protocol.ReleaseResponse(response)
 
-func TestNewChunkedBodyWriter1(t *testing.T) {
-	response := protocol.AcquireResponse()
 	mockConn := mock.NewConn("")
 	w := NewChunkedBodyWriter(response, mockConn)
-	w.Write([]byte("hello"))
-	w.Flush()
-	w.Finalize()
-	w.Flush()
-	out, _ := mockConn.WriterRecorder().ReadBinary(mockConn.WriterRecorder().WroteLen())
-	assert.True(t, strings.Contains(string(out), "Transfer-Encoding: chunked"))
-	assert.True(t, strings.Contains(string(out), "5"+string(bytestr.StrCRLF)+"hello"))
-	assert.True(t, strings.Contains(string(out), "0"+string(bytestr.StrCRLF)+string(bytestr.StrCRLF)))
+	_, _ = w.Write([]byte("hello"))
+	assert.Nil(t, w.Flush())
+	out, _ := mockConn.WriterRecorder().Peek(mockConn.WriterRecorder().WroteLen())
+	resp := string(out)
+	assert.True(t, strings.Contains(resp, "Transfer-Encoding: chunked"))
+	assert.True(t, strings.HasSuffix(resp, "5\r\nhello\r\n"))
+
+	// Finalize adds 0\r\n\r\n
+	assert.Nil(t, w.Finalize())
+	assert.Nil(t, w.Finalize()) // noop
+	out, _ = mockConn.WriterRecorder().Peek(mockConn.WriterRecorder().WroteLen())
+	resp = string(out)
+	assert.True(t, strings.HasSuffix(resp, "5\r\nhello\r\n0\r\n\r\n"))
+
+	_, err := w.Write([]byte("world"))
+	assert.True(t, err == errChunkedFinished)
 }
 
-func TestNewChunkedBodyWriterNoData(t *testing.T) {
+func TestNewChunkedBodyWriter_Err(t *testing.T) {
 	response := protocol.AcquireResponse()
-	response.Header.Set("Foo", "Bar")
-	mockConn := mock.NewConn("")
-	w := NewChunkedBodyWriter(response, mockConn)
-	w.Finalize()
-	w.Flush()
-	out, _ := mockConn.WriterRecorder().ReadBinary(mockConn.WriterRecorder().WroteLen())
-	assert.True(t, strings.Contains(string(out), "Transfer-Encoding: chunked"))
-	assert.True(t, strings.Contains(string(out), "Foo: Bar"))
-	assert.True(t, strings.Contains(string(out), "0"+string(bytestr.StrCRLF)+string(bytestr.StrCRLF)))
+	defer protocol.ReleaseResponse(response)
+
+	mw := mock.NewMockWriter(nil)
+	w := NewChunkedBodyWriter(response, mw)
+
+	expectErr := errors.New("mock malloc err")
+
+	mw.MockMalloc = func(n int) ([]byte, error) {
+		return nil, expectErr
+	}
+	_, err := w.Write([]byte("hello"))
+	assert.True(t, err == expectErr)
+
+	mw.MockMalloc = nil
+	_, err = w.Write([]byte("world"))
+	assert.True(t, err == expectErr) // next call will return last err
+
+	w = NewChunkedBodyWriter(response, mw)
+
+	mw.MockMalloc = func(n int) ([]byte, error) {
+		return nil, expectErr
+	}
+	err = w.Finalize()
+	assert.True(t, err == expectErr)
 }
diff --git a/pkg/protocol/http1/server_test.go b/pkg/protocol/http1/server_test.go
index d478b36..491c81a 100644
--- a/pkg/protocol/http1/server_test.go
+++ b/pkg/protocol/http1/server_test.go
@@ -25,6 +25,8 @@ import (
 	"testing"
 	"time"
 
+	internalNetwork "github.com/cloudwego/hertz/internal/network"
+
 	inStats "github.com/cloudwego/hertz/internal/stats"
 	"github.com/cloudwego/hertz/pkg/app"
 	errs "github.com/cloudwego/hertz/pkg/common/errors"
@@ -258,6 +260,25 @@ func TestServerDisableReqCtxPool(t *testing.T) {
 	assert.Nil(t, err)
 }
 
+func TestServer_RaceDetect(t *testing.T) {
+	c := &app.RequestContext{}
+	_ = c.Request.URI() // parsedURI = true
+	m := &mockCore{
+		ctxPool: &sync.Pool{New: func() interface{} {
+			return c
+		}},
+		mockHandler: func(c context.Context, ctx *app.RequestContext) {
+			panic("must not be called")
+		},
+	}
+	s := &Server{}
+	s.Core = m
+	conn := mock.NewConn("GET / HTTP/1.1\nHost: foobar.com\n\n")
+	err := s.Serve(context.Background(), conn)
+	assert.NotNil(t, err)
+	assert.Assert(t, err.Error() == "race detected")
+}
+
 func TestHijackResponseWriter(t *testing.T) {
 	server := &Server{}
 	reqCtx := &app.RequestContext{}
@@ -400,6 +421,139 @@ func TestExpect100ContinueHandler(t *testing.T) {
 	assert.DeepEqual(t, "", string(response.Body()))
 }
 
+func TestSenseClientConnClose(t *testing.T) {
+	type connstate struct {
+		detectCalled bool
+		abortCalled  bool
+	}
+	reset := func(cs *connstate) {
+		cs.detectCalled = false
+		cs.abortCalled = false
+	}
+	state := &connstate{}
+
+	var (
+		detectFunc = func() {
+			state.detectCalled = true
+		}
+		abortFunc = func() {
+			state.abortCalled = true
+		}
+	)
+
+	server := &Server{}
+	reqCtx := &app.RequestContext{}
+	server.Core = &mockCore{
+		ctxPool: &sync.Pool{New: func() interface{} {
+			return reqCtx
+		}},
+		isRunning:   true,
+		mockHandler: func(c context.Context, ctx *app.RequestContext) {},
+	}
+
+	// normal
+	conn := mock.NewConn("GET / HTTP/1.1\nHost: foobar.com\n\n")
+	statefulConn := &mockStatefulConn{
+		conn,
+		nil,
+		detectFunc,
+		abortFunc,
+	}
+	server.Serve(context.Background(), statefulConn)
+	assert.True(t, state.detectCalled)
+	assert.True(t, state.abortCalled)
+	reset(state)
+
+	// 100 continue
+	conn = mock.NewConn("POST /foo HTTP/1.1\r\nHost: gle.com\r\nExpect: 100-continue\r\nContent-Length: 5\r\nContent-Type: a/b\r\n\r\n12345")
+	statefulConn.Conn = conn
+	server.Serve(context.Background(), statefulConn)
+	assert.True(t, state.detectCalled)
+	assert.True(t, state.abortCalled)
+	reset(state)
+
+	// 100 continue: error
+	conn = mock.NewConn("POST /foo HTTP/1.1\r\n" +
+		"Host: gle.com\r\n" +
+		"Expect: 100-continue\r\n" +
+		"Content-Length: 40\r\n" +
+		"Content-Type: multipart/form-data; boundary=1\r\n" +
+		"\r\n" +
+		"--1\r\n" +
+		"broken-multipart-body",
+	)
+	statefulConn.Conn = conn
+	server.Serve(context.Background(), statefulConn)
+	assert.False(t, state.detectCalled)
+	assert.False(t, state.abortCalled)
+	reset(state)
+
+	// bodyStream
+	server.StreamRequestBody = true
+	conn = mock.NewConn("POST /foo HTTP/1.1\r\nHost: gle.com\r\nExpect: 100-continue\r\nContent-Length: 0\r\nContent-Type: a/b\r\n\r\n12345")
+	statefulConn.Conn = conn
+	server.Serve(context.Background(), statefulConn)
+	assert.False(t, state.detectCalled)
+	assert.False(t, state.abortCalled)
+	reset(state)
+	server.StreamRequestBody = false
+
+	// hijacked
+	conn = mock.NewConn("GET / HTTP/1.1\nHost: foobar.com\n\n")
+	statefulConn = &mockStatefulConn{
+		conn,
+		nil,
+		detectFunc,
+		abortFunc,
+	}
+	server.HijackConnHandle = func(c network.Conn, h app.HijackHandler) {
+		h(c)
+	}
+	server.Core = &mockCore{
+		ctxPool: &sync.Pool{New: func() interface{} {
+			return reqCtx
+		}},
+		isRunning: true,
+		mockHandler: func(c context.Context, ctx *app.RequestContext) {
+			ctx.SetHijackHandler(func(conn network.Conn) {
+				_ = conn.Len()
+			})
+		},
+	}
+	server.Serve(context.Background(), statefulConn)
+	assert.True(t, state.detectCalled)
+	assert.True(t, state.abortCalled)
+	reset(state)
+	server.HijackConnHandle = nil
+
+	// handler panic
+	conn = mock.NewConn("GET / HTTP/1.1\nHost: foobar.com\n\n")
+	statefulConn = &mockStatefulConn{
+		conn,
+		nil,
+		detectFunc,
+		abortFunc,
+	}
+	server.Core = &mockCore{
+		ctxPool: &sync.Pool{New: func() interface{} {
+			return reqCtx
+		}},
+		isRunning: true,
+		mockHandler: func(c context.Context, ctx *app.RequestContext) {
+			// panic should be recovered by the recovery middleware, just mock one here.
+			// perform like a normal request
+			defer func() {
+				recover()
+			}()
+			panic("mock panic")
+		},
+	}
+	server.Serve(context.Background(), statefulConn)
+	assert.True(t, state.detectCalled)
+	assert.True(t, state.abortCalled)
+	reset(state)
+}
+
 type mockController struct {
 	FinishTimes int
 }
@@ -494,3 +648,24 @@ func TestShouldRecordInTraceError(t *testing.T) {
 	assert.True(t, shouldRecordInTraceError(errTimeout))
 	assert.True(t, shouldRecordInTraceError(errors.New("foo error")))
 }
+
+var _ internalNetwork.StatefulConn = &mockStatefulConn{}
+
+type mockStatefulConn struct {
+	network.Conn
+	Ctx                       context.Context
+	DetectConnectionCloseFunc func()
+	AbortBlockingReadFunc     func()
+}
+
+func (c *mockStatefulConn) DetectConnectionClose() {
+	c.DetectConnectionCloseFunc()
+}
+
+func (c *mockStatefulConn) AbortBlockingRead() {
+	c.AbortBlockingReadFunc()
+}
+
+func (c *mockStatefulConn) Context() context.Context {
+	return c.Ctx
+}
diff --git a/pkg/protocol/response_test.go b/pkg/protocol/response_test.go
index 5ecf5a0..a3b85bd 100644
--- a/pkg/protocol/response_test.go
+++ b/pkg/protocol/response_test.go
@@ -250,6 +250,8 @@ func TestResponseAcquireResponse(t *testing.T) {
 		resp1 := AcquireResponse()
 		assert.NotNil(t, resp1)
 		assert.Nil(t, resp1.body)
+		assert.Assert(t, resp1.BodyStream() == NoResponseBody)
+		assert.Assert(t, resp1.IsBodyStream() == false)
 
 		resp1.SetBody([]byte("test"))
 		resp1.SetStatusCode(consts.StatusOK)
diff --git a/pkg/protocol/sse/event_test.go b/pkg/protocol/sse/event_test.go
new file mode 100644
index 0000000..a375599
--- /dev/null
+++ b/pkg/protocol/sse/event_test.go
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2025 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 sse
+
+import (
+	"testing"
+	"time"
+
+	"github.com/cloudwego/hertz/pkg/common/test/assert"
+)
+
+func TestEvent_SetAndIsSet(t *testing.T) {
+	e := NewEvent()
+	defer e.Release()
+
+	// Test initial state
+	assert.Assert(t, !e.IsSetID())
+	assert.Assert(t, !e.IsSetType())
+	assert.Assert(t, !e.IsSetRetry())
+	assert.Assert(t, !e.IsSetData())
+
+	// Test SetID and IsSetID
+	e.SetID("test-id")
+	assert.Assert(t, e.IsSetID())
+	assert.DeepEqual(t, "test-id", e.ID)
+
+	// Test SetEvent and IsSetType
+	e.SetEvent("test-event")
+	assert.Assert(t, e.IsSetType())
+	assert.DeepEqual(t, "test-event", e.Type)
+
+	// Test SetRetry and IsSetRetry
+	r := 3 * time.Second
+	e.SetRetry(r)
+	assert.Assert(t, e.IsSetRetry())
+	assert.DeepEqual(t, r, e.Retry)
+
+	// Test SetData and IsSetData
+	d := []byte("test-data")
+	e.SetData(d)
+	assert.Assert(t, e.IsSetData())
+	assert.DeepEqual(t, d, e.Data)
+	e.Reset()
+	assert.Assert(t, e.IsSetData() == false)
+	e.SetDataString(string(d))
+	assert.Assert(t, e.IsSetData())
+	assert.DeepEqual(t, d, e.Data)
+}
+
+func TestEvent_AppendData(t *testing.T) {
+	e := NewEvent()
+	defer e.Release()
+
+	// Test AppendData
+	e.AppendData([]byte("first"))
+	assert.Assert(t, e.IsSetData())
+	assert.DeepEqual(t, []byte("first"), e.Data)
+
+	// Append more data
+	e.AppendDataString("second")
+	assert.DeepEqual(t, []byte("firstsecond"), e.Data)
+}
+
+func TestEvent_Reset(t *testing.T) {
+	e := NewEvent()
+	defer e.Release()
+
+	// Set all fields
+	e.SetID("test-id")
+	e.SetEvent("test-event")
+	e.SetRetry(3 * time.Second)
+	e.SetData([]byte("test-data"))
+
+	// Verify all fields are set
+	assert.Assert(t, e.IsSetID())
+	assert.Assert(t, e.IsSetType())
+	assert.Assert(t, e.IsSetRetry())
+	assert.Assert(t, e.IsSetData())
+
+	// Reset and verify all fields are cleared
+	e.Reset()
+	assert.Assert(t, !e.IsSetID())
+	assert.Assert(t, !e.IsSetType())
+	assert.Assert(t, !e.IsSetRetry())
+	assert.Assert(t, !e.IsSetData())
+	assert.DeepEqual(t, "", e.ID)
+	assert.DeepEqual(t, "", e.Type)
+	assert.DeepEqual(t, time.Duration(0), e.Retry)
+	assert.DeepEqual(t, 0, len(e.Data))
+}
+
+func TestEvent_Clone(t *testing.T) {
+	e1 := NewEvent()
+	e1.SetID("test-id")
+	e1.SetEvent("test-event")
+	e1.SetRetry(3 * time.Second)
+	e1.SetData([]byte("test-data"))
+	e2 := e1.Clone()
+	assert.Assert(t, e2.IsSetID())
+	assert.Assert(t, e2.IsSetType())
+	assert.Assert(t, e2.IsSetRetry())
+	assert.Assert(t, e2.IsSetData())
+	assert.DeepEqual(t, "test-id", e2.ID)
+	assert.DeepEqual(t, "test-event", e2.Type)
+	assert.DeepEqual(t, 3*time.Second, e2.Retry)
+	assert.DeepEqual(t, []byte("test-data"), e2.Data)
+	e1.Release()
+	e2.Release()
+}
+
+func TestEvent_PoolAndRelease(t *testing.T) {
+	e1 := NewEvent()
+	e1.SetID("test-id")
+	e1.Release()
+
+	// Get another event from the pool, should be the same instance but reset
+	e2 := NewEvent()
+	assert.Assert(t, !e2.IsSetID())
+	assert.DeepEqual(t, "", e2.ID)
+	e2.Release()
+}
diff --git a/pkg/protocol/sse/example_test.go b/pkg/protocol/sse/example_test.go
new file mode 100644
index 0000000..6b1a143
--- /dev/null
+++ b/pkg/protocol/sse/example_test.go
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2025 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 sse
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"github.com/cloudwego/hertz/internal/testutils"
+	"github.com/cloudwego/hertz/pkg/app"
+	"github.com/cloudwego/hertz/pkg/app/client"
+	"github.com/cloudwego/hertz/pkg/common/config"
+	"github.com/cloudwego/hertz/pkg/protocol"
+	"github.com/cloudwego/hertz/pkg/route"
+)
+
+// Example demonstrates a simple SSE server and client interaction.
+func Example() {
+	// --- SSE Server ---
+	opt := config.NewOptions([]config.Option{})
+	opt.Addr = "127.0.0.1:0"
+	engine := route.NewEngine(opt)
+	engine.GET("/", func(ctx context.Context, c *app.RequestContext) {
+		println("Server Got LastEventID", GetLastEventID(&c.Request))
+		w := NewWriter(c)
+		for i := 0; i < 5; i++ {
+			w.WriteEvent(fmt.Sprintf("id-%d", i), "message", []byte("hello\n\nworld"))
+			time.Sleep(10 * time.Millisecond)
+		}
+		// [optional] it writes 0\r\n\r\n to indicate the end of chunked response
+		// hertz will do it after handler returns
+		w.Close()
+	})
+	go engine.Run()
+	defer engine.Close()
+	time.Sleep(20 * time.Millisecond) // wait for server to start
+	opt.Addr = testutils.GetListenerAddr(engine)
+
+	// --- SSE Client ---
+	c, _ := client.NewClient()
+	req, resp := protocol.AcquireRequest(), protocol.AcquireResponse()
+	req.SetRequestURI("http://" + opt.Addr + "/")
+	req.SetMethod("GET")
+	req.SetHeader(LastEventIDHeader, "id-0")
+
+	// adds `text/event-stream` to http `Accept` header
+	// may required for some Model Context Protocol(MCP) servers
+	AddAcceptMIME(req)
+
+	if err := c.Do(context.Background(), req, resp); err != nil {
+		panic(err)
+	}
+	r, err := NewReader(resp)
+	if err != nil {
+		panic(err)
+	}
+	defer r.Close()
+
+	ctx, cancel := context.WithCancel(context.Background())
+	go func() {
+		time.Sleep(200 * time.Millisecond)
+		// cancel can be used to force ForEach returns by closing the remote connection
+		_ = cancel
+	}()
+	err = r.ForEach(ctx, func(e *Event) error {
+		println("Event:", e.String())
+		return nil
+	})
+	if err != nil {
+		panic(err)
+	}
+	println("Client LastEventID", r.LastEventID())
+	// Output:
+	//
+}
diff --git a/pkg/protocol/sse/reader_test.go b/pkg/protocol/sse/reader_test.go
new file mode 100644
index 0000000..9de6ebf
--- /dev/null
+++ b/pkg/protocol/sse/reader_test.go
@@ -0,0 +1,345 @@
+/*
+ * Copyright 2025 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 sse
+
+import (
+	"bytes"
+	"context"
+	"errors"
+	"io"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/cloudwego/hertz/internal/bytestr"
+	"github.com/cloudwego/hertz/pkg/common/test/assert"
+	"github.com/cloudwego/hertz/pkg/protocol"
+)
+
+type mockBodyStream struct {
+	reader io.Reader
+	closed bool
+}
+
+func (m *mockBodyStream) Read(p []byte) (n int, err error) {
+	return m.reader.Read(p)
+}
+
+func (m *mockBodyStream) Close() error {
+	m.closed = true
+	return nil
+}
+
+func TestNewReader(t *testing.T) {
+	tests := []struct {
+		name        string
+		contentType []byte
+		body        []byte
+		wantErr     bool
+	}{
+		{
+			name:        "Valid content type",
+			contentType: bytestr.MIMETextEventStream,
+			body:        []byte("event: message\ndata: test\n\n"),
+			wantErr:     false,
+		},
+		{
+			name:        "Invalid content type",
+			contentType: []byte("text/plain"),
+			body:        []byte("event: message\ndata: test\n\n"),
+			wantErr:     true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			resp := &protocol.Response{}
+			resp.Header.SetContentType(string(tt.contentType))
+			resp.SetBody(tt.body)
+
+			r, err := NewReader(resp)
+			if tt.wantErr {
+				assert.Assert(t, err != nil)
+				assert.Assert(t, r == nil)
+			} else {
+				assert.Assert(t, err == nil)
+				assert.Assert(t, r != nil)
+			}
+		})
+	}
+}
+
+func TestReader_ReadEvent(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    string
+		expected *Event
+		wantErr  bool
+	}{
+		{
+			name:  "Basic event",
+			input: "id: 123\nevent: update\ndata: test data\n\n",
+			expected: func() *Event {
+				e := NewEvent()
+				e.SetID("123")
+				e.SetEvent("update")
+				e.SetData([]byte("test data"))
+				return e
+			}(),
+			wantErr: false,
+		},
+		{
+			name:  "Event with retry",
+			input: "id: 123\nevent: update\nretry: 3000\ndata: test data\n\n",
+			expected: func() *Event {
+				e := NewEvent()
+				e.SetID("123")
+				e.SetEvent("update")
+				e.SetRetry(3000 * time.Millisecond)
+				e.SetData([]byte("test data"))
+				return e
+			}(),
+			wantErr: false,
+		},
+		{
+			name:  "Event with multiline data",
+			input: "id: 123\nevent: update\ndata: line1\ndata: line2\ndata: line3\n\n",
+			expected: func() *Event {
+				e := NewEvent()
+				e.SetID("123")
+				e.SetEvent("update")
+				e.SetData([]byte("line1\nline2\nline3"))
+				return e
+			}(),
+			wantErr: false,
+		},
+		{
+			name:  "Event with BOM",
+			input: "\xEF\xBB\xBFid: 123\nevent: update\ndata: test data\n\n",
+			expected: func() *Event {
+				e := NewEvent()
+				e.SetID("123")
+				e.SetEvent("update")
+				e.SetData([]byte("test data"))
+				return e
+			}(),
+			wantErr: false,
+		},
+		{
+			name:  "Event with comments",
+			input: ": this is a comment\nid: 123\n: another comment\nevent: update\ndata: test data\n\n",
+			expected: func() *Event {
+				e := NewEvent()
+				e.SetID("123")
+				e.SetEvent("update")
+				e.SetData([]byte("test data"))
+				return e
+			}(),
+			wantErr: false,
+		},
+		{
+			name:  "Event with no colon in field",
+			input: "id\nevent: update\ndata: test data\n\n",
+			expected: func() *Event {
+				e := NewEvent()
+				e.SetID("")
+				e.SetEvent("update")
+				e.SetData([]byte("test data"))
+				return e
+			}(),
+			wantErr: false,
+		},
+		{
+			name:  "Event with no space after colon",
+			input: "id:123\nevent:update\ndata:test data\n\n",
+			expected: func() *Event {
+				e := NewEvent()
+				e.SetID("123")
+				e.SetEvent("update")
+				e.SetData([]byte("test data"))
+				return e
+			}(),
+			wantErr: false,
+		},
+		{
+			name:  "Event with ID containing null character (should be ignored)",
+			input: "id: test\u0000id\nevent: update\ndata: test data\n\n",
+			expected: func() *Event {
+				e := NewEvent()
+				e.SetEvent("update")
+				e.SetData([]byte("test data"))
+				return e
+			}(),
+			wantErr: false,
+		},
+		{
+			name:  "Event with invalid retry value",
+			input: "id: 123\nevent: update\nretry: invalid\ndata: test data\n\n",
+			expected: func() *Event {
+				e := NewEvent()
+				e.SetID("123")
+				e.SetEvent("update")
+				e.SetData([]byte("test data"))
+				return e
+			}(),
+			wantErr: false,
+		},
+		{
+			name:  "Empty event",
+			input: "\n\n",
+			expected: func() *Event {
+				e := NewEvent()
+				// Empty event doesn't set any fields, so bitset remains 0
+				return e
+			}(),
+			wantErr: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			resp := &protocol.Response{}
+			resp.Header.SetContentType(string(bytestr.MIMETextEventStream))
+			resp.SetBody([]byte(tt.input))
+
+			r, err := NewReader(resp)
+			assert.Assert(t, err == nil)
+
+			e := NewEvent()
+			err = r.Read(e)
+
+			if tt.wantErr {
+				assert.Assert(t, err != nil)
+			} else {
+				assert.Assert(t, err == nil)
+				assert.DeepEqual(t, tt.expected.ID, e.ID)
+				assert.DeepEqual(t, tt.expected.Type, e.Type)
+				assert.DeepEqual(t, tt.expected.Retry, e.Retry)
+				assert.DeepEqual(t, tt.expected.Data, e.Data)
+
+				// LastEventID check
+				if e.ID != "" {
+					assert.DeepEqual(t, r.LastEventID(), e.ID)
+				}
+			}
+
+			e.Release()
+		})
+	}
+}
+
+func TestReader_ReadEvent_WithBodyStream(t *testing.T) {
+	input := "id: 123\nevent: update\ndata: test data\n\n"
+
+	resp := &protocol.Response{}
+	resp.Header.SetContentType(string(bytestr.MIMETextEventStream))
+
+	// Create a mock body stream
+	ms := &mockBodyStream{
+		reader: strings.NewReader(input),
+	}
+	resp.SetBodyStream(ms, -1)
+
+	r, err := NewReader(resp)
+	assert.Assert(t, err == nil)
+
+	e := NewEvent()
+	err = r.Read(e)
+	assert.Assert(t, err == nil)
+
+	// Verify event data
+	assert.DeepEqual(t, "123", e.ID)
+	assert.DeepEqual(t, "update", e.Type)
+	assert.DeepEqual(t, []byte("test data"), e.Data)
+
+	// LastEventID check
+	if e.ID != "" {
+		assert.DeepEqual(t, r.LastEventID(), e.ID)
+	}
+
+	// Test Close
+	err = r.Close()
+	assert.Assert(t, err == nil)
+	assert.Assert(t, ms.closed)
+
+	e.Release()
+}
+
+type mockReadForceClose struct {
+	readFunc  func(b []byte) (int, error)
+	closeFunc func() error
+}
+
+func (m *mockReadForceClose) Read(b []byte) (int, error) {
+	return m.readFunc(b)
+}
+
+func (m *mockReadForceClose) ForceClose() error {
+	return m.closeFunc()
+}
+
+func TestReader_ReadEvent_Error(t *testing.T) {
+	// Create a reader that will return an error
+	errReader := &bytes.Reader{}
+
+	resp := &protocol.Response{}
+	resp.Header.SetContentType(string(bytestr.MIMETextEventStream))
+	resp.SetBodyStream(errReader, -1)
+
+	r, err := NewReader(resp)
+	assert.Assert(t, err == nil)
+
+	e := NewEvent()
+	err = r.Read(e)
+
+	// The error from bytes.Reader will be io.EOF
+	assert.Assert(t, err == io.EOF)
+
+	e.Release()
+}
+
+func TestReader_ForEach(t *testing.T) {
+	// mock Read & ForceClose
+	mr := &mockReadForceClose{}
+	ch := make(chan error, 1)
+	defer close(ch)
+	mr.readFunc = func(b []byte) (int, error) {
+		return 0, <-ch
+	}
+	mr.closeFunc = func() error {
+		ch <- errors.New("closed")
+		return nil
+	}
+
+	// create protocol.Response
+	resp := &protocol.Response{}
+	resp.Header.SetContentType(string(bytestr.MIMETextEventStream))
+	resp.SetBodyStream(mr, -1)
+	r, err := NewReader(resp)
+	assert.Assert(t, err == nil)
+
+	// test ForEach with context
+	ctx, cancel := context.WithCancel(context.Background())
+	go func() { // cancel after 50ms
+		time.Sleep(50 * time.Millisecond)
+		cancel()
+	}()
+	err = r.ForEach(ctx, func(e *Event) error {
+		panic("must not called")
+	})
+	assert.Assert(t, err == ctx.Err())
+}
diff --git a/pkg/protocol/sse/utils_test.go b/pkg/protocol/sse/utils_test.go
new file mode 100644
index 0000000..4c73a3f
--- /dev/null
+++ b/pkg/protocol/sse/utils_test.go
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2025 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 sse
+
+import (
+	"testing"
+
+	"github.com/cloudwego/hertz/internal/bytestr"
+	"github.com/cloudwego/hertz/pkg/common/test/assert"
+	"github.com/cloudwego/hertz/pkg/protocol"
+)
+
+func TestSetGetLastEventID(t *testing.T) {
+	req := protocol.AcquireRequest()
+	defer protocol.ReleaseRequest(req)
+
+	SetLastEventID(req, "123")
+	assert.DeepEqual(t, "123", GetLastEventID(req))
+}
+
+func TestAddAcceptMIME(t *testing.T) {
+	// Test case 1: Empty Accept header
+	req := protocol.AcquireRequest()
+	defer protocol.ReleaseRequest(req)
+
+	AddAcceptMIME(req)
+
+	acceptHeader := req.Header.Peek("Accept")
+	assert.DeepEqual(t, string(bytestr.MIMETextEventStream), string(acceptHeader))
+
+	// Test case 2: Existing Accept header without text/event-stream
+	req.Reset()
+	req.Header.Set("Accept", "text/html, application/json")
+
+	AddAcceptMIME(req)
+
+	acceptHeader = req.Header.Peek("Accept")
+	assert.DeepEqual(t, "text/html, application/json, text/event-stream", string(acceptHeader))
+
+	// Test case 3: Existing Accept header already containing text/event-stream
+	req.Reset()
+	req.Header.Set("Accept", "text/html, text/event-stream, application/json")
+
+	AddAcceptMIME(req)
+
+	acceptHeader = req.Header.Peek("Accept")
+	assert.DeepEqual(t, "text/html, text/event-stream, application/json", string(acceptHeader))
+}
+
+func TestHasCRLF(t *testing.T) {
+	assert.Assert(t, hasCRLF("\nThis is a test string"))
+	assert.Assert(t, hasCRLF("This is \na test string"))
+	assert.Assert(t, hasCRLF("This is a test string\n"))
+	assert.Assert(t, hasCRLF("\rThis is a test string"))
+	assert.Assert(t, hasCRLF("This is \rna test string"))
+	assert.Assert(t, hasCRLF("This is a test string\r"))
+	assert.Assert(t, hasCRLF("This is a test string") == false)
+}
+
+func TestSseEventType(t *testing.T) {
+	assert.DeepEqual(t, "message", sseEventType([]byte("message")))
+	assert.DeepEqual(t, "custom", sseEventType([]byte("custom")))
+}
diff --git a/pkg/protocol/sse/writer_test.go b/pkg/protocol/sse/writer_test.go
new file mode 100644
index 0000000..3c2c693
--- /dev/null
+++ b/pkg/protocol/sse/writer_test.go
@@ -0,0 +1,251 @@
+/*
+ * Copyright 2025 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 sse
+
+import (
+	"bytes"
+	"errors"
+	"testing"
+	"time"
+
+	"github.com/cloudwego/hertz/pkg/app"
+	"github.com/cloudwego/hertz/pkg/common/test/assert"
+)
+
+// mockWriteFlusher implements the writeFlusher interface for testing
+type mw struct {
+	buf         bytes.Buffer
+	flushCalled bool
+	writeErr    error
+	flushErr    error
+	finalizeErr error
+}
+
+func (m *mw) Write(p []byte) (n int, err error) {
+	if m.writeErr != nil {
+		return 0, m.writeErr
+	}
+	return m.buf.Write(p)
+}
+
+func (m *mw) Flush() error {
+	m.flushCalled = true
+	return m.flushErr
+}
+
+func (m *mw) String() string {
+	return m.buf.String()
+}
+
+func (m *mw) Finalize() error {
+	return m.finalizeErr
+}
+
+func TestWriter_WriteEvent(t *testing.T) {
+	tests := []struct {
+		name      string
+		id        string
+		eventType string
+		data      []byte
+		wantErr   bool
+		expected  string
+	}{
+		{
+			name:      "Basic event",
+			id:        "123",
+			eventType: "message",
+			data:      []byte("test data"),
+			wantErr:   false,
+			expected:  "id: 123\nevent: message\ndata: test data\n\n",
+		},
+		{
+			name:      "Empty fields",
+			id:        "",
+			eventType: "",
+			data:      nil,
+			wantErr:   false,
+			expected:  "\n",
+		},
+		{
+			name:      "ID with CRLF",
+			id:        "test\nid",
+			eventType: "update",
+			data:      []byte("test data"),
+			wantErr:   true,
+			expected:  "",
+		},
+		{
+			name:      "Event type with CRLF",
+			id:        "123",
+			eventType: "up\ndate",
+			data:      []byte("test data"),
+			wantErr:   true,
+			expected:  "",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			m := &mw{}
+			w := &Writer{w: m}
+
+			err := w.WriteEvent(tt.id, tt.eventType, tt.data)
+
+			if tt.wantErr {
+				assert.Assert(t, err != nil)
+			} else {
+				assert.Assert(t, err == nil)
+				assert.DeepEqual(t, tt.expected, m.String())
+				assert.Assert(t, m.flushCalled)
+			}
+		})
+	}
+}
+
+func TestWriter_Write(t *testing.T) {
+	tests := []struct {
+		name     string
+		event    *Event
+		writeErr error
+		flushErr error
+		wantErr  bool
+		expected string
+	}{
+		{
+			name: "Complete event",
+			event: func() *Event {
+				e := NewEvent()
+				e.SetID("123")
+				e.SetEvent("update")
+				e.SetRetry(3 * time.Second)
+				e.SetData([]byte("test data"))
+				return e
+			}(),
+			wantErr:  false,
+			expected: "id: 123\nevent: update\nretry: 3000\ndata: test data\n\n",
+		},
+		{
+			name: "Multiline data",
+			event: func() *Event {
+				e := NewEvent()
+				e.SetData([]byte("line1\nline2\nline3"))
+				return e
+			}(),
+			wantErr:  false,
+			expected: "data: line1\ndata: line2\ndata: line3\n\n",
+		},
+		{
+			name: "Write error",
+			event: func() *Event {
+				e := NewEvent()
+				e.SetData([]byte("test data"))
+				return e
+			}(),
+			writeErr: errors.New("write error"),
+			wantErr:  true,
+			expected: "",
+		},
+		{
+			name: "Flush error",
+			event: func() *Event {
+				e := NewEvent()
+				e.SetData([]byte("test data"))
+				return e
+			}(),
+			flushErr: errors.New("flush error"),
+			wantErr:  true,
+			expected: "data: test data\n\n",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			m := &mw{
+				writeErr: tt.writeErr,
+				flushErr: tt.flushErr,
+			}
+			w := &Writer{w: m}
+
+			err := w.Write(tt.event)
+
+			if tt.wantErr {
+				assert.Assert(t, err != nil)
+			} else {
+				assert.Assert(t, err == nil)
+				assert.DeepEqual(t, tt.expected, m.String())
+				assert.Assert(t, m.flushCalled)
+			}
+		})
+	}
+}
+
+func TestNewWriter(t *testing.T) {
+	c := app.NewContext(0)
+	w := NewWriter(c)
+	assert.Assert(t, w != nil)
+	assert.DeepEqual(t, "no-cache", string(c.Response.Header.Peek("Cache-Control")))
+	assert.DeepEqual(t, "text/event-stream", string(c.Response.Header.Peek("Content-Type")))
+}
+
+func TestWriter_WriteComment(t *testing.T) {
+	m := &mw{}
+	w := &Writer{w: m}
+
+	err := w.WriteComment("test\ncomment")
+
+	assert.Assert(t, err == nil)
+	assert.DeepEqual(t, ":test\n:comment\n", m.String())
+	assert.Assert(t, m.flushCalled)
+
+	// empty string
+	m = &mw{}
+	w = &Writer{w: m}
+	err = w.WriteComment("")
+	assert.Assert(t, err == nil)
+	assert.DeepEqual(t, ":", m.String())
+
+	// keep-alive
+	m = &mw{}
+	w = &Writer{w: m}
+	err = w.WriteKeepAlive()
+	assert.Assert(t, err == nil)
+	assert.DeepEqual(t, ":keep-alive\n", m.String())
+}
+
+func TestWriter_Close(t *testing.T) {
+	// Create a mock writeFlusher
+	m := &mw{}
+
+	// Create a Writer with the mock
+	w := &Writer{w: m}
+
+	// Test Close method
+	err := w.Close()
+
+	// Verify no error occurred
+	assert.Nil(t, err)
+
+	// Set an error to be returned by Finalize
+	expectedErr := errors.New("finalize error")
+	m.finalizeErr = expectedErr
+
+	// Test Close method with error
+	err = w.Close()
+
+	// Verify the error is propagated
+	assert.DeepEqual(t, expectedErr, err)
+}
diff --git a/pkg/protocol/uri_unix_test.go b/pkg/protocol/uri_unix_test.go
index f89cd11..3e4e678 100644
--- a/pkg/protocol/uri_unix_test.go
+++ b/pkg/protocol/uri_unix_test.go
@@ -1,5 +1,4 @@
 //go:build !windows
-// +build !windows
 
 /*
  * Copyright 2023 CloudWeGo Authors
diff --git a/pkg/route/engine_test.go b/pkg/route/engine_test.go
index 2c8c97f..5a5d1a2 100644
--- a/pkg/route/engine_test.go
+++ b/pkg/route/engine_test.go
@@ -801,6 +801,37 @@ func TestValidateConfigSetErrorFactory(t *testing.T) {
 	performRequest(e, "GET", "/bind?b=1")
 }
 
+func TestValidateConfigSetErrorFactoryWithBindConfig(t *testing.T) {
+	type TestValidate struct {
+		B int `query:"b" vd:"$>100"`
+	}
+	opt := config.NewOptions([]config.Option{})
+	CustomValidateErrFunc := func(failField, msg string) error {
+		err := ValidateError{
+			ErrType:   "validateErr",
+			FailField: "[validateFailField]: " + failField,
+			Msg:       "[validateErrMsg]: " + msg,
+		}
+
+		return &err
+	}
+
+	validateConfig := binding.NewValidateConfig()
+	validateConfig.SetValidatorErrorFactory(CustomValidateErrFunc)
+	opt.ValidateConfig = validateConfig
+	opt.BindConfig = binding.NewBindConfig()
+	e := NewEngine(opt)
+	e.GET("/bind", func(c context.Context, ctx *app.RequestContext) {
+		var req TestValidate
+		err := ctx.BindAndValidate(&req)
+		if err == nil {
+			t.Fatal("expect an error")
+		}
+		assert.DeepEqual(t, "validateErr: expr_path=[validateFailField]: B, cause=[validateErrMsg]: ", err.Error())
+	})
+	performRequest(e, "GET", "/bind?b=1")
+}
+
 func TestCustomBinder(t *testing.T) {
 	type Req struct {
 		A int `query:"a"`
@@ -889,12 +920,19 @@ func newMockStandardTransporter(opt *config.Options) network.Transporter {
 }
 
 func TestEngineShutdown(t *testing.T) {
-	mockCtxCallback := func(ctx context.Context) {}
-
 	opt := config.NewOptions(nil)
 	opt.Addr = "127.0.0.1:0"
 	opt.TransporterNewer = newMockStandardTransporter
 
+	mockCtxCallback := func(ctx context.Context) {
+		// Shutdown adds `ExitWaitTimeout` to the given context
+		dl, ok := ctx.Deadline()
+		assert.Assert(t, ok)
+		assert.Assert(t,
+			opt.ExitWaitTimeout-time.Until(dl) < 50*time.Millisecond, // runtime schedule latency
+			opt.ExitWaitTimeout, time.Until(dl))
+	}
+
 	var wg sync.WaitGroup
 	var engine *Engine