Repository: cloudwego/hertz
Pull request #1118 (2024-05-15 03:50:08)

fix(hz): client header and path tag
#### What type of PR is this?
<!--
Add one of the following kinds:

build: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
ci: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
docs: Documentation only changes
feat: A new feature
optimize: A new optimization
fix: A bug fix
perf: A code change that improves performance
refactor: A code change that neither fixes a bug nor adds a feature
style: Changes that do not affect the meaning of the code (white space, formatting, missing semi-colons, etc)
test: Adding missing tests or correcting existing tests
chore: Changes to the build process or auxiliary tools and libraries such as documentation generation
-->
fix
#### Check the PR title.
<!--
The description of the title will be attached in Release Notes, 
so please describe it from user-oriented, what this PR does / why we need it.
Please check your PR title with the below requirements:
-->
- [x] This PR title match the format: \<type\>(optional scope): \<description\>
- [x] The description of this PR title is user-oriented and clear enough for others to understand.
- [x] Attach the PR updating the user documentation if the current PR requires user awareness at the usage level. [User docs repo](https://github.com/cloudwego/cloudwego.github.io)


#### (Optional) Translate the PR title into Chinese.
hz client 生成的 path/header参数不默认做 snake style

#### (Optional) More detailed description for this PR(en: English/zh: Chinese).
<!--
Provide more detailed info for review(e.g., it's recommended to provide perf data if this is a perf type PR).
-->
en: client header and path tag
zh(optional): path/header参数不默认做 snake style
<img width="1279" alt="image" src="https://github.com/cloudwego/hertz/assets/97507466/de8e48c4-8167-410e-b484-4f8e8a3fbd5c">

#### (Optional) Which issue(s) this PR fixes:
<!--
Automatically closes linked issue when PR is merged.
Eg: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
-->

#### (Optional) The PR that updates user documentation:
<!--
If the current PR requires user awareness at the usage level, please submit a PR to update user docs. [User docs repo](https://github.com/cloudwego/cloudwego.github.io)
-->


Patch:
diff --git a/cmd/hz/protobuf/ast.go b/cmd/hz/protobuf/ast.go
index d607a83..3b4759a 100644
--- a/cmd/hz/protobuf/ast.go
+++ b/cmd/hz/protobuf/ast.go
@@ -312,7 +312,7 @@ func parseAnnotationToClient(clientMethod *generator.ClientMethod, gen *protogen
 		if proto.HasExtension(f.Desc.Options(), api.E_Path) {
 			hasAnnotation = true
 			pathAnnos := proto.GetExtension(f.Desc.Options(), api.E_Path)
-			val := checkSnakeName(pathAnnos.(string))
+			val := pathAnnos.(string)
 			if isStringFieldType {
 				clientMethod.PathParamsCode += fmt.Sprintf("%q: req.Get%s(),\n", val, f.GoName)
 			} else {
@@ -323,7 +323,7 @@ func parseAnnotationToClient(clientMethod *generator.ClientMethod, gen *protogen
 		if proto.HasExtension(f.Desc.Options(), api.E_Header) {
 			hasAnnotation = true
 			headerAnnos := proto.GetExtension(f.Desc.Options(), api.E_Header)
-			val := checkSnakeName(headerAnnos.(string))
+			val := headerAnnos.(string)
 			if isStringFieldType {
 				clientMethod.HeaderParamsCode += fmt.Sprintf("%q: req.Get%s(),\n", val, f.GoName)
 			} else {
@@ -350,7 +350,7 @@ func parseAnnotationToClient(clientMethod *generator.ClientMethod, gen *protogen
 		if fileAnnos := getCompatibleAnnotation(f.Desc.Options(), api.E_FileName, api.E_FileNameCompatible); fileAnnos != nil {
 			hasAnnotation = true
 			hasFormAnnotation = true
-			val := checkSnakeName(fileAnnos.(string))
+			val := fileAnnos.(string)
 			clientMethod.FormFileCode += fmt.Sprintf("%q: req.Get%s(),\n", val, f.GoName)
 		}
 		if !hasAnnotation && strings.EqualFold(clientMethod.HTTPMethod, "get") {
diff --git a/cmd/hz/thrift/ast.go b/cmd/hz/thrift/ast.go
index 5005a20..d577b95 100644
--- a/cmd/hz/thrift/ast.go
+++ b/cmd/hz/thrift/ast.go
@@ -281,7 +281,7 @@ func parseAnnotationToClient(clientMethod *generator.ClientMethod, p *parser.Typ
 
 		if anno := getAnnotation(field.Annotations, AnnotationPath); len(anno) > 0 {
 			hasAnnotation = true
-			path := checkSnakeName(anno[0])
+			path := anno[0]
 			if isStringFieldType {
 				clientMethod.PathParamsCode += fmt.Sprintf("%q: req.Get%s(),\n", path, field.GoName().String())
 			} else {
@@ -291,7 +291,7 @@ func parseAnnotationToClient(clientMethod *generator.ClientMethod, p *parser.Typ
 
 		if anno := getAnnotation(field.Annotations, AnnotationHeader); len(anno) > 0 {
 			hasAnnotation = true
-			header := checkSnakeName(anno[0])
+			header := anno[0]
 			if isStringFieldType {
 				clientMethod.HeaderParamsCode += fmt.Sprintf("%q: req.Get%s(),\n", header, field.GoName().String())
 			} else {
@@ -317,7 +317,7 @@ func parseAnnotationToClient(clientMethod *generator.ClientMethod, p *parser.Typ
 
 		if anno := getAnnotation(field.Annotations, AnnotationFileName); len(anno) > 0 {
 			hasAnnotation = true
-			fileName := checkSnakeName(anno[0])
+			fileName := anno[0]
 			hasFormAnnotation = true
 			clientMethod.FormFileCode += fmt.Sprintf("%q: req.Get%s(),\n", fileName, field.GoName().String())
 		}
diff --git a/go.mod b/go.mod
index 3619b83..36f7e51 100644
--- a/go.mod
+++ b/go.mod
@@ -7,7 +7,7 @@ require (
 	github.com/bytedance/gopkg v0.0.0-20220413063733-65bf48ffb3a7
 	github.com/bytedance/mockey v1.2.1
 	github.com/bytedance/sonic v1.8.1
-	github.com/cloudwego/netpoll v0.5.0
+	github.com/cloudwego/netpoll v0.6.0
 	github.com/fsnotify/fsnotify v1.5.4
 	github.com/tidwall/gjson v1.14.4
 	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
diff --git a/go.sum b/go.sum
index 3cf3c2f..4b60e99 100644
--- a/go.sum
+++ b/go.sum
@@ -10,8 +10,8 @@ github.com/bytedance/sonic v1.8.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZX
 github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
 github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
 github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
-github.com/cloudwego/netpoll v0.5.0 h1:oRrOp58cPCvK2QbMozZNDESvrxQaEHW2dCimmwH1lcU=
-github.com/cloudwego/netpoll v0.5.0/go.mod h1:xVefXptcyheopwNDZjDPcfU6kIjZXZ4nY550k1yH9eQ=
+github.com/cloudwego/netpoll v0.6.0 h1:JRMkrA1o8k/4quxzg6Q1XM+zIhwZsyoWlq6ef+ht31U=
+github.com/cloudwego/netpoll v0.6.0/go.mod h1:xVefXptcyheopwNDZjDPcfU6kIjZXZ4nY550k1yH9eQ=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
diff --git a/pkg/app/server/binding/internal/decoder/base_type_decoder.go b/pkg/app/server/binding/internal/decoder/base_type_decoder.go
index 9c5cb12..bf26c9c 100644
--- a/pkg/app/server/binding/internal/decoder/base_type_decoder.go
+++ b/pkg/app/server/binding/internal/decoder/base_type_decoder.go
@@ -75,7 +75,7 @@ func (d *baseTypeFieldTextDecoder) Decode(req *protocol.Request, params param.Pa
 				if found {
 					err = nil
 				} else {
-					err = fmt.Errorf("'%s' field is a 'required' parameter, but the request body does not have this parameter '%s'", d.fieldName, tagInfo.JSONName)
+					err = fmt.Errorf("'%s' field is a 'required' parameter, but the request body does not have this parameter '%s'", tagInfo.Value, tagInfo.JSONName)
 				}
 				if len(tagInfo.Default) != 0 && keyExist(req, tagInfo) {
 					defaultValue = ""
@@ -90,7 +90,7 @@ func (d *baseTypeFieldTextDecoder) Decode(req *protocol.Request, params param.Pa
 			break
 		}
 		if tagInfo.Required {
-			err = fmt.Errorf("'%s' field is a 'required' parameter, but the request does not have this parameter", d.fieldName)
+			err = fmt.Errorf("'%s' field is a 'required' parameter, but the request does not have this parameter", tagInfo.Value)
 		}
 	}
 	if err != nil {
diff --git a/pkg/app/server/binding/internal/decoder/map_type_decoder.go b/pkg/app/server/binding/internal/decoder/map_type_decoder.go
index 59fed77..18e15c9 100644
--- a/pkg/app/server/binding/internal/decoder/map_type_decoder.go
+++ b/pkg/app/server/binding/internal/decoder/map_type_decoder.go
@@ -67,7 +67,7 @@ func (d *mapTypeFieldTextDecoder) Decode(req *protocol.Request, params param.Par
 				if found {
 					err = nil
 				} else {
-					err = fmt.Errorf("'%s' field is a 'required' parameter, but the request does not have this parameter", d.fieldName)
+					err = fmt.Errorf("'%s' field is a 'required' parameter, but the request does not have this parameter", tagInfo.Value)
 				}
 				if len(tagInfo.Default) != 0 && keyExist(req, tagInfo) {
 					defaultValue = ""
@@ -82,7 +82,7 @@ func (d *mapTypeFieldTextDecoder) Decode(req *protocol.Request, params param.Par
 			break
 		}
 		if tagInfo.Required {
-			err = fmt.Errorf("'%s' field is a 'required' parameter, but the request does not have this parameter", d.fieldName)
+			err = fmt.Errorf("'%s' field is a 'required' parameter, but the request does not have this parameter", tagInfo.Value)
 		}
 	}
 	if err != nil {
diff --git a/pkg/app/server/binding/internal/decoder/slice_type_decoder.go b/pkg/app/server/binding/internal/decoder/slice_type_decoder.go
index c2887d1..7b6c591 100644
--- a/pkg/app/server/binding/internal/decoder/slice_type_decoder.go
+++ b/pkg/app/server/binding/internal/decoder/slice_type_decoder.go
@@ -70,7 +70,7 @@ func (d *sliceTypeFieldTextDecoder) Decode(req *protocol.Request, params param.P
 				if found {
 					err = nil
 				} else {
-					err = fmt.Errorf("'%s' field is a 'required' parameter, but the request does not have this parameter", d.fieldName)
+					err = fmt.Errorf("'%s' field is a 'required' parameter, but the request does not have this parameter", tagInfo.Value)
 				}
 				if len(tagInfo.Default) != 0 && keyExist(req, tagInfo) { //
 					defaultValue = ""
@@ -88,7 +88,7 @@ func (d *sliceTypeFieldTextDecoder) Decode(req *protocol.Request, params param.P
 			break
 		}
 		if tagInfo.Required {
-			err = fmt.Errorf("'%s' field is a 'required' parameter, but the request does not have this parameter", d.fieldName)
+			err = fmt.Errorf("'%s' field is a 'required' parameter, but the request does not have this parameter", tagInfo.Value)
 		}
 	}
 	if err != nil {
diff --git a/pkg/app/server/binding/internal/decoder/struct_type_decoder.go b/pkg/app/server/binding/internal/decoder/struct_type_decoder.go
index 75f3ae4..c2af2c0 100644
--- a/pkg/app/server/binding/internal/decoder/struct_type_decoder.go
+++ b/pkg/app/server/binding/internal/decoder/struct_type_decoder.go
@@ -44,7 +44,7 @@ func (d *structTypeFieldTextDecoder) Decode(req *protocol.Request, params param.
 				if found {
 					err = nil
 				} else {
-					err = fmt.Errorf("'%s' field is a 'required' parameter, but the request does not have this parameter", d.fieldName)
+					err = fmt.Errorf("'%s' field is a 'required' parameter, but the request does not have this parameter", tagInfo.Value)
 				}
 				if len(tagInfo.Default) != 0 && keyExist(req, tagInfo) {
 					defaultValue = ""
@@ -59,7 +59,7 @@ func (d *structTypeFieldTextDecoder) Decode(req *protocol.Request, params param.
 			break
 		}
 		if tagInfo.Required {
-			err = fmt.Errorf("'%s' field is a 'required' parameter, but the request does not have this parameter", d.fieldName)
+			err = fmt.Errorf("'%s' field is a 'required' parameter, but the request does not have this parameter", tagInfo.Value)
 		}
 	}
 	if err != nil {
diff --git a/pkg/app/server/option.go b/pkg/app/server/option.go
index e797034..18f1843 100644
--- a/pkg/app/server/option.go
+++ b/pkg/app/server/option.go
@@ -394,3 +394,19 @@ func WithDisableDefaultContentType(disable bool) config.Option {
 		o.NoDefaultContentType = disable
 	}}
 }
+
+// WithSenseClientDisconnection sets the ability to sense client disconnections.
+// If we don't set it, it will default to false.
+// There are two issues to note when using this option:
+// 1. Warning: It only applies to netpoll.
+// 2. After opening, the context.Context in the request will be cancelled.
+//
+//	Example:
+//	server.Default(
+//	server.WithSenseClientDisconnection(true),
+//	)
+func WithSenseClientDisconnection(b bool) config.Option {
+	return config.Option{F: func(o *config.Options) {
+		o.SenseClientDisconnection = b
+	}}
+}
diff --git a/pkg/common/config/option.go b/pkg/common/config/option.go
index 417955f..958d9b3 100644
--- a/pkg/common/config/option.go
+++ b/pkg/common/config/option.go
@@ -63,6 +63,7 @@ type Options struct {
 	StreamRequestBody            bool
 	NoDefaultServerHeader        bool
 	DisablePrintRoute            bool
+	SenseClientDisconnection     bool
 	Network                      string
 	Addr                         string
 	BasePath                     string
@@ -203,6 +204,9 @@ func NewOptions(opts []Option) *Options {
 		// Disabled when set to True
 		DisablePrintRoute: false,
 
+		// The ability to sense client disconnection is disabled by default
+		SenseClientDisconnection: false,
+
 		// "tcp", "udp", "unix"(unix domain socket)
 		Network: defaultNetwork,
 
diff --git a/pkg/network/netpoll/transport.go b/pkg/network/netpoll/transport.go
index 17829cb..7450292 100644
--- a/pkg/network/netpoll/transport.go
+++ b/pkg/network/netpoll/transport.go
@@ -36,33 +36,45 @@ func init() {
 	netpoll.SetLoggerOutput(io.Discard)
 }
 
+type ctxCancelKeyStruct struct{}
+
+var ctxCancelKey = ctxCancelKeyStruct{}
+
+func cancelContext(ctx context.Context) context.Context {
+	ctx, cancel := context.WithCancel(ctx)
+	ctx = context.WithValue(ctx, ctxCancelKey, cancel)
+	return ctx
+}
+
 type transporter struct {
 	sync.RWMutex
-	network          string
-	addr             string
-	keepAliveTimeout time.Duration
-	readTimeout      time.Duration
-	writeTimeout     time.Duration
-	listener         net.Listener
-	eventLoop        netpoll.EventLoop
-	listenConfig     *net.ListenConfig
-	OnAccept         func(conn net.Conn) context.Context
-	OnConnect        func(ctx context.Context, conn network.Conn) context.Context
+	senseClientDisconnection bool
+	network                  string
+	addr                     string
+	keepAliveTimeout         time.Duration
+	readTimeout              time.Duration
+	writeTimeout             time.Duration
+	listener                 net.Listener
+	eventLoop                netpoll.EventLoop
+	listenConfig             *net.ListenConfig
+	OnAccept                 func(conn net.Conn) context.Context
+	OnConnect                func(ctx context.Context, conn network.Conn) context.Context
 }
 
 // For transporter switch
 func NewTransporter(options *config.Options) network.Transporter {
 	return &transporter{
-		network:          options.Network,
-		addr:             options.Addr,
-		keepAliveTimeout: options.KeepAliveTimeout,
-		readTimeout:      options.ReadTimeout,
-		writeTimeout:     options.WriteTimeout,
-		listener:         nil,
-		eventLoop:        nil,
-		listenConfig:     options.ListenConfig,
-		OnAccept:         options.OnAccept,
-		OnConnect:        options.OnConnect,
+		senseClientDisconnection: options.SenseClientDisconnection,
+		network:                  options.Network,
+		addr:                     options.Addr,
+		keepAliveTimeout:         options.KeepAliveTimeout,
+		readTimeout:              options.ReadTimeout,
+		writeTimeout:             options.WriteTimeout,
+		listener:                 nil,
+		eventLoop:                nil,
+		listenConfig:             options.ListenConfig,
+		OnAccept:                 options.OnAccept,
+		OnConnect:                options.OnConnect,
 	}
 }
 
@@ -88,10 +100,14 @@ func (t *transporter) ListenAndServe(onReq network.OnData) (err error) {
 			if t.writeTimeout > 0 {
 				conn.SetWriteTimeout(t.writeTimeout)
 			}
+			ctx := context.Background()
 			if t.OnAccept != nil {
-				return t.OnAccept(newConn(conn))
+				ctx = t.OnAccept(newConn(conn))
+			}
+			if t.senseClientDisconnection {
+				ctx = cancelContext(ctx)
 			}
-			return context.Background()
+			return ctx
 		}),
 	}
 
@@ -101,6 +117,15 @@ func (t *transporter) ListenAndServe(onReq network.OnData) (err error) {
 		}))
 	}
 
+	if t.senseClientDisconnection {
+		opts = append(opts, netpoll.WithOnDisconnect(func(ctx context.Context, connection netpoll.Connection) {
+			cancelFunc, ok := ctx.Value(ctxCancelKey).(context.CancelFunc)
+			if cancelFunc != nil && ok {
+				cancelFunc()
+			}
+		}))
+	}
+
 	// Create EventLoop
 	t.Lock()
 	t.eventLoop, err = netpoll.NewEventLoop(func(ctx context.Context, connection netpoll.Connection) error {


Test patch:
diff --git a/pkg/app/server/binding/binder_test.go b/pkg/app/server/binding/binder_test.go
index 225b8bd..ec962da 100644
--- a/pkg/app/server/binding/binder_test.go
+++ b/pkg/app/server/binding/binder_test.go
@@ -466,10 +466,14 @@ func TestBind_RequiredBind(t *testing.T) {
 		A int `query:"a,required"`
 	}
 	req := newMockRequest().
+		SetRequestURI("http://foobar.com")
+	err := DefaultBinder().Bind(req.Req, &s, nil)
+	assert.DeepEqual(t, "'a' field is a 'required' parameter, but the request does not have this parameter", err.Error())
+
+	req = newMockRequest().
 		SetRequestURI("http://foobar.com").
 		SetHeader("A", "1")
-
-	err := DefaultBinder().Bind(req.Req, &s, nil)
+	err = DefaultBinder().Bind(req.Req, &s, nil)
 	if err == nil {
 		t.Fatal("expected error")
 	}
@@ -904,6 +908,7 @@ func TestBind_JSONRequiredField(t *testing.T) {
 	if err == nil {
 		t.Errorf("expected an error, but get nil")
 	}
+	assert.DeepEqual(t, "'c' field is a 'required' parameter, but the request body does not have this parameter 'n.n2.c'", err.Error())
 	assert.DeepEqual(t, 1, result.N.A)
 	assert.DeepEqual(t, 2, result.N.B)
 	assert.DeepEqual(t, 0, result.N.N2.C)
@@ -1492,6 +1497,7 @@ func Test_ValidatorErrorFactory(t *testing.T) {
 	if err == nil {
 		t.Fatalf("unexpected nil, expected an error")
 	}
+	assert.DeepEqual(t, "'a' field is a 'required' parameter, but the request does not have this parameter", err.Error())
 
 	type TestValidate struct {
 		B int `query:"b" vd:"$>100"`
diff --git a/pkg/app/server/binding/tagexpr_bind_test.go b/pkg/app/server/binding/tagexpr_bind_test.go
index 1533b5a..1d331e1 100644
--- a/pkg/app/server/binding/tagexpr_bind_test.go
+++ b/pkg/app/server/binding/tagexpr_bind_test.go
@@ -121,7 +121,7 @@ func TestGetBody(t *testing.T) {
 	if err == nil {
 		t.Fatalf("expected an error, but get nil")
 	}
-	assert.DeepEqual(t, err.Error(), "'E' field is a 'required' parameter, but the request body does not have this parameter 'X.e'")
+	assert.DeepEqual(t, err.Error(), "'e' field is a 'required' parameter, but the request body does not have this parameter 'X.e'")
 }
 
 func TestQueryNum(t *testing.T) {
@@ -431,7 +431,7 @@ func TestJSON(t *testing.T) {
 	if err == nil {
 		t.Error("expected an error, but get nil")
 	}
-	assert.DeepEqual(t, err.Error(), "'Y' field is a 'required' parameter, but the request body does not have this parameter 'y'")
+	assert.DeepEqual(t, err.Error(), "'y' field is a 'required' parameter, but the request body does not have this parameter 'y'")
 	assert.DeepEqual(t, []string{"a1", "a2"}, (**recv.X).A)
 	assert.DeepEqual(t, int32(21), (**recv.X).B)
 	assert.DeepEqual(t, &[]uint16{31, 32}, (**recv.X).C)
@@ -753,7 +753,7 @@ func TestOption(t *testing.T) {
 	req = newRequest("", header, nil, bodyReader)
 	recv = new(Recv)
 	err = DefaultBinder().Bind(req.Req, recv, nil)
-	assert.DeepEqual(t, err.Error(), "'C' field is a 'required' parameter, but the request body does not have this parameter 'X.c'")
+	assert.DeepEqual(t, err.Error(), "'c' field is a 'required' parameter, but the request body does not have this parameter 'X.c'")
 	assert.DeepEqual(t, 0, recv.X.C)
 	assert.DeepEqual(t, 0, recv.X.D)
 	assert.DeepEqual(t, "y1", recv.Y)
diff --git a/pkg/app/server/hertz_unix_test.go b/pkg/app/server/hertz_unix_test.go
index b37ddfb..b1f7d70 100644
--- a/pkg/app/server/hertz_unix_test.go
+++ b/pkg/app/server/hertz_unix_test.go
@@ -34,6 +34,7 @@ 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"
@@ -134,3 +135,57 @@ func TestHertz_Spin(t *testing.T) {
 
 	<-ch2
 }
+
+func TestWithSenseClientDisconnection(t *testing.T) {
+	var closeFlag int32
+	h := New(WithHostPorts("127.0.0.1:6631"), 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()
+	time.Sleep(time.Second)
+	con, err := net.Dial("tcp", "127.0.0.1:6631")
+	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(time.Second)
+	assert.DeepEqual(t, atomic.LoadInt32(&closeFlag), int32(0))
+	assert.Nil(t, con.Close())
+	time.Sleep(time.Second)
+	assert.DeepEqual(t, atomic.LoadInt32(&closeFlag), int32(1))
+}
+
+func TestWithSenseClientDisconnectionAndWithOnConnect(t *testing.T) {
+	var closeFlag int32
+	h := New(WithHostPorts("127.0.0.1:6632"), 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()
+	time.Sleep(time.Second)
+	con, err := net.Dial("tcp", "127.0.0.1:6632")
+	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(time.Second)
+	assert.DeepEqual(t, atomic.LoadInt32(&closeFlag), int32(0))
+	assert.Nil(t, con.Close())
+	time.Sleep(time.Second)
+	assert.DeepEqual(t, atomic.LoadInt32(&closeFlag), int32(1))
+}
diff --git a/pkg/app/server/option_test.go b/pkg/app/server/option_test.go
index f5d7f7b..bc43e7c 100644
--- a/pkg/app/server/option_test.go
+++ b/pkg/app/server/option_test.go
@@ -61,6 +61,7 @@ func TestOptions(t *testing.T) {
 		WithBasePath("/"),
 		WithMaxRequestBodySize(2),
 		WithDisablePrintRoute(true),
+		WithSenseClientDisconnection(true),
 		WithNetwork("unix"),
 		WithExitWaitTime(time.Second),
 		WithMaxKeepBodySize(500),
@@ -93,6 +94,7 @@ func TestOptions(t *testing.T) {
 	assert.DeepEqual(t, opt.BasePath, "/")
 	assert.DeepEqual(t, opt.MaxRequestBodySize, 2)
 	assert.DeepEqual(t, opt.DisablePrintRoute, true)
+	assert.DeepEqual(t, opt.SenseClientDisconnection, true)
 	assert.DeepEqual(t, opt.Network, "unix")
 	assert.DeepEqual(t, opt.ExitWaitTimeout, time.Second)
 	assert.DeepEqual(t, opt.MaxKeepBodySize, 500)
@@ -130,6 +132,7 @@ func TestDefaultOptions(t *testing.T) {
 	assert.DeepEqual(t, opt.GetOnly, false)
 	assert.DeepEqual(t, opt.DisableKeepalive, false)
 	assert.DeepEqual(t, opt.DisablePrintRoute, false)
+	assert.DeepEqual(t, opt.SenseClientDisconnection, false)
 	assert.DeepEqual(t, opt.Network, "tcp")
 	assert.DeepEqual(t, opt.ExitWaitTimeout, time.Second*5)
 	assert.DeepEqual(t, opt.MaxKeepBodySize, 4*1024*1024)
diff --git a/pkg/common/config/option_test.go b/pkg/common/config/option_test.go
index 67fcab7..b836a3a 100644
--- a/pkg/common/config/option_test.go
+++ b/pkg/common/config/option_test.go
@@ -39,6 +39,7 @@ func TestDefaultOptions(t *testing.T) {
 	assert.False(t, options.RemoveExtraSlash)
 	assert.True(t, options.UnescapePathValues)
 	assert.False(t, options.DisablePreParseMultipartForm)
+	assert.False(t, options.SenseClientDisconnection)
 	assert.DeepEqual(t, defaultNetwork, options.Network)
 	assert.DeepEqual(t, defaultAddr, options.Addr)
 	assert.DeepEqual(t, defaultMaxRequestBodySize, options.MaxRequestBodySize)
diff --git a/pkg/network/netpoll/transport_test.go b/pkg/network/netpoll/transport_test.go
index d8a0609..5323920 100644
--- a/pkg/network/netpoll/transport_test.go
+++ b/pkg/network/netpoll/transport_test.go
@@ -69,6 +69,35 @@ func TestTransport(t *testing.T) {
 		assert.Assert(t, atomic.LoadInt32(&onDataFlag) == 1)
 	})
 
+	t.Run("TestSenseClientDisconnection", func(t *testing.T) {
+		var onReqFlag int32
+		transporter := NewTransporter(&config.Options{
+			Addr:                     addr,
+			Network:                  nw,
+			SenseClientDisconnection: true,
+		})
+
+		go transporter.ListenAndServe(func(ctx context.Context, conn interface{}) error {
+			atomic.StoreInt32(&onReqFlag, 1)
+			time.Sleep(100 * time.Millisecond)
+			assert.DeepEqual(t, context.Canceled, ctx.Err())
+			return nil
+		})
+		defer transporter.Close()
+		time.Sleep(100 * time.Millisecond)
+
+		dial := NewDialer()
+		conn, err := dial.DialConnection(nw, addr, time.Second, nil)
+		assert.Nil(t, err)
+		_, err = conn.Write([]byte("123"))
+		assert.Nil(t, err)
+		err = conn.Close()
+		assert.Nil(t, err)
+		time.Sleep(100 * time.Millisecond)
+
+		assert.Assert(t, atomic.LoadInt32(&onReqFlag) == 1)
+	})
+
 	t.Run("TestListenConfig", func(t *testing.T) {
 		listenCfg := &net.ListenConfig{Control: func(network, address string, c syscall.RawConn) error {
 			return c.Control(func(fd uintptr) {