Repository: cloudwego/hertz
Pull request #64 (2022-06-17 11:31:13)

feat: add net/http adaptor
#### What type of PR is this?

feat: add net/http adaptor

#### What this PR does / why we need it (English/Chinese):
en: Add adaptor for Hertz Request & Response to net/http Request & ResponseWriter
zh: 增加 Hertz Request & Response 到 net/http Request & ResponseWriter 的适配器


Patch:
diff --git a/README.md b/README.md
index 5dc0d04..538005b 100644
--- a/README.md
+++ b/README.md
@@ -70,7 +70,7 @@ Hertz is distributed under the [Apache License, version 2.0](https://github.com/
 ![LarkGroup](images/lark_group.png)
 - Wechat: CloudWeGo community wechat group.
 
-  ![WechatGroup](images/wechat_group_cn.png)
+![WechatGroup](images/wechat_group_cn.png)
 ## Landscapes
 
 <p align="center">
diff --git a/pkg/app/context.go b/pkg/app/context.go
index 27b16c7..b564a52 100644
--- a/pkg/app/context.go
+++ b/pkg/app/context.go
@@ -970,6 +970,11 @@ func (ctx *RequestContext) GetRawData() []byte {
 	return ctx.Request.Body()
 }
 
+// Body returns body data
+func (ctx *RequestContext) Body() ([]byte, error) {
+	return ctx.Request.BodyE()
+}
+
 type ClientIP func(ctx *RequestContext) string
 
 var defaultClientIP = func(ctx *RequestContext) string {
diff --git a/pkg/common/adaptor/request.go b/pkg/common/adaptor/request.go
new file mode 100644
index 0000000..efe1989
--- /dev/null
+++ b/pkg/common/adaptor/request.go
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2022 CloudWeGo Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package adaptor
+
+import (
+	"bytes"
+	"net/http"
+
+	"github.com/cloudwego/hertz/pkg/protocol"
+)
+
+// GetCompatRequest only support basic function of Request, not for all.
+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 {
+		return r, err
+	}
+
+	h := make(map[string][]string)
+	req.Header.VisitAll(func(k, v []byte) {
+		h[string(k)] = append(h[string(k)], string(v))
+	})
+
+	r.Header = h
+	return r, nil
+}
diff --git a/pkg/common/adaptor/response.go b/pkg/common/adaptor/response.go
new file mode 100644
index 0000000..e981007
--- /dev/null
+++ b/pkg/common/adaptor/response.go
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2022 CloudWeGo Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package adaptor
+
+import (
+	"net/http"
+
+	"github.com/cloudwego/hertz/pkg/protocol"
+	"github.com/cloudwego/hertz/pkg/protocol/consts"
+)
+
+type compatResponse struct {
+	h           *protocol.Response
+	header      http.Header
+	writeHeader bool
+}
+
+func (c *compatResponse) Header() http.Header {
+	if c.header != nil {
+		return c.header
+	}
+	c.header = make(map[string][]string)
+	return c.header
+}
+
+func (c *compatResponse) Write(b []byte) (int, error) {
+	if !c.writeHeader {
+		c.WriteHeader(consts.StatusOK)
+	}
+
+	return c.h.BodyWriter().Write(b)
+}
+
+func (c *compatResponse) WriteHeader(statusCode int) {
+	if !c.writeHeader {
+		for k, v := range c.header {
+			for _, vv := range v {
+				if k == consts.HeaderContentLength {
+					continue
+				}
+				if k == consts.HeaderSetCookie {
+					cookie := protocol.AcquireCookie()
+					cookie.Parse(vv)
+					c.h.Header.SetCookie(cookie)
+					continue
+				}
+				c.h.Header.Add(k, vv)
+			}
+		}
+		c.writeHeader = true
+	}
+
+	c.h.Header.SetStatusCode(statusCode)
+}
+
+// GetCompatResponseWriter only support basic function of ResponseWriter, not for all.
+func GetCompatResponseWriter(resp *protocol.Response) http.ResponseWriter {
+	c := &compatResponse{
+		h: resp,
+	}
+	c.h.Header.SetNoDefaultContentType(true)
+
+	h := make(map[string][]string)
+	tmpKey := make([][]byte, 0, c.h.Header.Len())
+	c.h.Header.VisitAll(func(k, v []byte) {
+		h[string(k)] = append(h[string(k)], string(v))
+		tmpKey = append(tmpKey, k)
+	})
+
+	for _, k := range tmpKey {
+		c.h.Header.DelBytes(k)
+	}
+
+	c.header = h
+	return c
+}
diff --git a/pkg/common/errors/errors.go b/pkg/common/errors/errors.go
index 0c19915..38e90c9 100644
--- a/pkg/common/errors/errors.go
+++ b/pkg/common/errors/errors.go
@@ -49,13 +49,14 @@ import (
 
 var (
 	// These errors are the base error, which are used for checking in errors.Is()
-	ErrNeedMore      = errors.New("need more data")
-	ErrChunkedStream = errors.New("chunked stream")
-	ErrBodyTooLarge  = errors.New("body size exceeds the given limit")
-	ErrHijacked      = errors.New("connection has been hijacked")
-	ErrIdleTimeout   = errors.New("idle timeout")
-	ErrTimeout       = errors.New("timeout")
-	ErrNothingRead   = errors.New("nothing read")
+	ErrNeedMore        = errors.New("need more data")
+	ErrChunkedStream   = errors.New("chunked stream")
+	ErrBodyTooLarge    = errors.New("body size exceeds the given limit")
+	ErrHijacked        = errors.New("connection has been hijacked")
+	ErrIdleTimeout     = errors.New("idle timeout")
+	ErrTimeout         = errors.New("timeout")
+	ErrNothingRead     = errors.New("nothing read")
+	ErrShortConnection = errors.New("short connection")
 )
 
 // ErrorType is an unsigned 64-bit error code as defined in the hertz spec.
diff --git a/pkg/network/connection.go b/pkg/network/connection.go
index c2dd8e3..9a5e358 100644
--- a/pkg/network/connection.go
+++ b/pkg/network/connection.go
@@ -89,3 +89,7 @@ type ConnTLSer interface {
 	Handshake() error
 	ConnectionState() tls.ConnectionState
 }
+
+type HandleSpecificError interface {
+	HandleSpecificError(err error, rip string) (needIgnore bool)
+}
diff --git a/pkg/network/dialer/default.go b/pkg/network/dialer/default.go
new file mode 100644
index 0000000..35c48a0
--- /dev/null
+++ b/pkg/network/dialer/default.go
@@ -0,0 +1,26 @@
+//go:build !windows
+// +build !windows
+
+/*
+ * Copyright 2022 CloudWeGo Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package dialer
+
+import "github.com/cloudwego/hertz/pkg/network/netpoll"
+
+func init() {
+	defaultDialer = netpoll.NewDialer()
+}
diff --git a/pkg/network/dialer/default_windows.go b/pkg/network/dialer/default_windows.go
new file mode 100644
index 0000000..e2ff5b1
--- /dev/null
+++ b/pkg/network/dialer/default_windows.go
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2022 CloudWeGo Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package dialer
+
+import (
+	"github.com/cloudwego/hertz/pkg/network/standard"
+)
+
+func init() {
+	defaultDialer = standard.NewDialer()
+}
diff --git a/pkg/network/dialer/dialer.go b/pkg/network/dialer/dialer.go
index 007dded..9678bd0 100644
--- a/pkg/network/dialer/dialer.go
+++ b/pkg/network/dialer/dialer.go
@@ -22,15 +22,10 @@ import (
 	"time"
 
 	"github.com/cloudwego/hertz/pkg/network"
-	"github.com/cloudwego/hertz/pkg/network/netpoll"
 )
 
 var defaultDialer network.Dialer
 
-func init() {
-	defaultDialer = netpoll.NewDialer()
-}
-
 func SetDialer(dialer network.Dialer) {
 	defaultDialer = dialer
 }
diff --git a/pkg/network/netpoll/connection.go b/pkg/network/netpoll/connection.go
index 12a559f..a407abd 100644
--- a/pkg/network/netpoll/connection.go
+++ b/pkg/network/netpoll/connection.go
@@ -1,3 +1,6 @@
+//go:build !windows
+// +build !windows
+
 /*
  * Copyright 2022 CloudWeGo Authors
  *
@@ -20,6 +23,7 @@ import (
 	"errors"
 	"io"
 
+	"github.com/cloudwego/hertz/pkg/common/hlog"
 	"github.com/cloudwego/hertz/pkg/network"
 	"github.com/cloudwego/netpoll"
 )
@@ -70,6 +74,14 @@ func (c *Conn) Flush() error {
 	return c.Conn.Flush()
 }
 
+func (c *Conn) HandleSpecificError(err error, rip string) (needIgnore bool) {
+	if errors.Is(err, netpoll.ErrConnClosed) {
+		hlog.Warnf("HERTZ: Netpoll error=%s, remoteAddr=%s", err.Error(), rip)
+		return true
+	}
+	return false
+}
+
 func normalizeErr(err error) error {
 	if errors.Is(err, netpoll.ErrEOF) {
 		return io.EOF
diff --git a/pkg/network/netpoll/dial.go b/pkg/network/netpoll/dial.go
index 0df38dd..9c6d7b3 100644
--- a/pkg/network/netpoll/dial.go
+++ b/pkg/network/netpoll/dial.go
@@ -1,3 +1,6 @@
+//go:build !windows
+// +build !windows
+
 /*
  * Copyright 2022 CloudWeGo Authors
  *
diff --git a/pkg/network/netpoll/transport.go b/pkg/network/netpoll/transport.go
index 6377999..803e34e 100644
--- a/pkg/network/netpoll/transport.go
+++ b/pkg/network/netpoll/transport.go
@@ -1,3 +1,6 @@
+//go:build !windows
+// +build !windows
+
 /*
  * Copyright 2022 CloudWeGo Authors
  *
diff --git a/pkg/protocol/http1/server.go b/pkg/protocol/http1/server.go
index 4fdaf3d..b4e9b4e 100644
--- a/pkg/protocol/http1/server.go
+++ b/pkg/protocol/http1/server.go
@@ -45,9 +45,10 @@ import (
 const NextProtoTLS = suite.HTTP1
 
 var (
-	errHijacked      = errs.New(errs.ErrHijacked, errs.ErrorTypePublic, nil)
-	errIdleTimeout   = errs.New(errs.ErrIdleTimeout, errs.ErrorTypePublic, nil)
-	errUnexpectedEOF = errs.NewPublic(io.ErrUnexpectedEOF.Error() + " when reading request")
+	errHijacked        = errs.New(errs.ErrHijacked, errs.ErrorTypePublic, nil)
+	errIdleTimeout     = errs.New(errs.ErrIdleTimeout, errs.ErrorTypePublic, nil)
+	errShortConnection = errs.New(errs.ErrShortConnection, errs.ErrorTypePublic, "server is going to close the connection")
+	errUnexpectedEOF   = errs.NewPublic(io.ErrUnexpectedEOF.Error() + " when reading request")
 )
 
 type Option struct {
@@ -327,7 +328,7 @@ func (s Server) Serve(c context.Context, conn network.Conn) (err error) {
 		}
 
 		if connectionClose {
-			return
+			return errShortConnection
 		}
 
 		if s.IdleTimeout == 0 {
diff --git a/pkg/route/default.go b/pkg/route/default.go
new file mode 100644
index 0000000..f0e4153
--- /dev/null
+++ b/pkg/route/default.go
@@ -0,0 +1,28 @@
+//go:build !windows
+// +build !windows
+
+/*
+ * Copyright 2022 CloudWeGo Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package route
+
+import (
+	"github.com/cloudwego/hertz/pkg/network/netpoll"
+)
+
+func init() {
+	defaultTransporter = netpoll.NewTransporter
+}
diff --git a/pkg/route/default_windows.go b/pkg/route/default_windows.go
new file mode 100644
index 0000000..71deb19
--- /dev/null
+++ b/pkg/route/default_windows.go
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2022 CloudWeGo Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package route
+
+import (
+	"github.com/cloudwego/hertz/pkg/network/standard"
+)
+
+func init() {
+	defaultTransporter = standard.NewTransporter
+}
diff --git a/pkg/route/engine.go b/pkg/route/engine.go
index 035549f..10f49b0 100644
--- a/pkg/route/engine.go
+++ b/pkg/route/engine.go
@@ -67,17 +67,16 @@ import (
 	"github.com/cloudwego/hertz/pkg/common/tracer/traceinfo"
 	"github.com/cloudwego/hertz/pkg/common/utils"
 	"github.com/cloudwego/hertz/pkg/network"
-	netpoll2 "github.com/cloudwego/hertz/pkg/network/netpoll"
+	"github.com/cloudwego/hertz/pkg/network/standard"
 	"github.com/cloudwego/hertz/pkg/protocol"
 	"github.com/cloudwego/hertz/pkg/protocol/consts"
 	"github.com/cloudwego/hertz/pkg/protocol/http1"
 	"github.com/cloudwego/hertz/pkg/protocol/http1/factory"
 	"github.com/cloudwego/hertz/pkg/protocol/suite"
-	"github.com/cloudwego/netpoll"
 )
 
 var (
-	defaultTransporter = netpoll2.NewTransporter
+	defaultTransporter = standard.NewTransporter
 
 	errInitFailed       = errs.NewPrivate("engine has been init already")
 	errAlreadyRunning   = errs.NewPrivate("engine is already running")
@@ -365,7 +364,7 @@ func errProcess(conn io.Closer, err error) {
 	}()
 
 	// Quiet close the connection
-	if errors.Is(err, errs.ErrIdleTimeout) {
+	if errors.Is(err, errs.ErrShortConnection) || errors.Is(err, errs.ErrIdleTimeout) {
 		return
 	}
 
@@ -378,14 +377,11 @@ func errProcess(conn io.Closer, err error) {
 	// Get remote address
 	rip := getRemoteAddrFromCloser(conn)
 
-	// Handle netpoll error
-	if errors.Is(err, netpoll.ErrConnClosed) {
-		hlog.Warnf("HERTZ: Netpoll error=%s, remoteAddr=%s", err.Error(), rip)
-		return
-	}
-	if errors.Is(err, netpoll.ErrReadTimeout) {
-		hlog.Errorf("HERTZ: Netpoll error=%s, remoteAddr=%s", err.Error(), rip)
-		return
+	// Handle Specific error
+	if hsp, ok := conn.(network.HandleSpecificError); ok {
+		if hsp.HandleSpecificError(err, rip) {
+			return
+		}
 	}
 	// other errors
 	hlog.Errorf("HERTZ: Error=%s, remoteAddr=%s", err.Error(), rip)


Test patch:
diff --git a/pkg/common/adaptor/request_test.go b/pkg/common/adaptor/request_test.go
new file mode 100644
index 0000000..f0909b1
--- /dev/null
+++ b/pkg/common/adaptor/request_test.go
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2022 CloudWeGo Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package adaptor
+
+import (
+	"context"
+	"io/ioutil"
+	"net/http"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/cloudwego/hertz/pkg/app"
+	"github.com/cloudwego/hertz/pkg/app/server"
+)
+
+func TestCompatResponse_WriteHeader(t *testing.T) {
+	var testHeader http.Header
+	var testBody string
+	testUrl := "http://127.0.0.1:9000/test"
+	testStatusCode := 299
+
+	testHeader = make(map[string][]string)
+	testHeader["Key1"] = []string{"value1"}
+	testHeader["Key2"] = []string{"value2", "value22"}
+	testHeader["Key3"] = []string{"value3", "value33", "value333"}
+
+	testBody = "test body"
+
+	h := server.New(server.WithHostPorts("127.0.0.1:9000"))
+	h.POST("/test", func(c context.Context, ctx *app.RequestContext) {
+		req, _ := GetCompatRequest(&ctx.Request)
+		resp := GetCompatResponseWriter(&ctx.Response)
+		handlerAndCheck(t, resp, req, testHeader, testBody, testStatusCode)
+	})
+
+	go h.Spin()
+	time.Sleep(200 * time.Millisecond)
+
+	makeACall(t, http.MethodPost, testUrl, testHeader, testBody, testStatusCode)
+}
+
+func makeACall(t *testing.T, method, url string, header http.Header, body string, expectStatusCode int) {
+	client := http.Client{}
+	req, _ := http.NewRequest(method, url, strings.NewReader(body))
+	req.Header = header
+	resp, err := client.Do(req)
+	if err != nil {
+		t.Fatalf("make a call error: %s", err)
+	}
+
+	respHeader := resp.Header
+
+	for k, v := range header {
+		for i := 0; i < len(v); i++ {
+			if respHeader[k][i] != v[i] {
+				t.Fatalf("Header error: want %s=%s, got %s=%s", respHeader[k], respHeader[k][i], respHeader[k], v[i])
+			}
+		}
+	}
+
+	b, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		t.Fatalf("Read body error: %s", err)
+	}
+	if string(b) != body {
+		t.Fatalf("Body not equal: want: %s, got: %s", body, string(b))
+	}
+
+	if resp.StatusCode != expectStatusCode {
+		t.Fatalf("Status code not equal: want: %d, got: %d", expectStatusCode, resp.StatusCode)
+	}
+}
+
+func handlerAndCheck(t *testing.T, writer http.ResponseWriter, request *http.Request, wantHeader http.Header, wantBody string, statusCode int) {
+	reqHeader := request.Header
+	for k, v := range wantHeader {
+		if reqHeader[k] == nil {
+			t.Fatalf("Header error: want %s=%s, got %s=nil", reqHeader[k], reqHeader[k][0], reqHeader[k])
+		}
+		if reqHeader[k][0] != v[0] {
+			t.Fatalf("Header error: want %s=%s, got %s=%s", reqHeader[k], reqHeader[k][0], reqHeader[k], v[0])
+		}
+	}
+
+	body, err := ioutil.ReadAll(request.Body)
+	if err != nil {
+		t.Fatalf("Read body error: %s", err)
+	}
+	if string(body) != wantBody {
+		t.Fatalf("Body not equal: want: %s, got: %s", wantBody, string(body))
+	}
+
+	respHeader := writer.Header()
+	for k, v := range reqHeader {
+		respHeader[k] = v
+	}
+	writer.WriteHeader(statusCode)
+	_, err = writer.Write([]byte("test"))
+	if err != nil {
+		t.Fatalf("Write body error: %s", err)
+	}
+	_, err = writer.Write([]byte(" body"))
+	if err != nil {
+		t.Fatalf("Write body error: %s", err)
+	}
+}
diff --git a/pkg/route/engine_test.go b/pkg/route/engine_test.go
index 96e9506..451ea35 100644
--- a/pkg/route/engine_test.go
+++ b/pkg/route/engine_test.go
@@ -43,16 +43,19 @@ package route
 import (
 	"context"
 	"crypto/tls"
+	"errors"
 	"fmt"
 	"html/template"
 	"io/ioutil"
 	"net"
 	"net/http"
+	"sync/atomic"
 	"testing"
 	"time"
 
 	"github.com/cloudwego/hertz/pkg/app"
 	"github.com/cloudwego/hertz/pkg/common/config"
+	errs "github.com/cloudwego/hertz/pkg/common/errors"
 	"github.com/cloudwego/hertz/pkg/common/test/assert"
 	"github.com/cloudwego/hertz/pkg/common/test/mock"
 )
@@ -148,9 +151,35 @@ func TestEngineUnescapeRaw(t *testing.T) {
 	}
 }
 
+func TestConnectionClose(t *testing.T) {
+	engine := NewEngine(config.NewOptions(nil))
+	atomic.StoreUint32(&engine.status, statusRunning)
+	engine.Init()
+	engine.GET("/foo", func(c context.Context, ctx *app.RequestContext) {
+		ctx.String(200, "ok")
+	})
+	conn := mock.NewConn("GET /foo HTTP/1.1\r\nHost: google.com\r\nConnection: close\r\n\r\n")
+	err := engine.Serve(context.Background(), conn)
+	assert.True(t, errors.Is(err, errs.ErrShortConnection))
+}
+
+func TestConnectionClose01(t *testing.T) {
+	engine := NewEngine(config.NewOptions(nil))
+	atomic.StoreUint32(&engine.status, statusRunning)
+	engine.Init()
+	engine.GET("/foo", func(c context.Context, ctx *app.RequestContext) {
+		ctx.SetConnectionClose()
+		ctx.String(200, "ok")
+	})
+	conn := mock.NewConn("GET /foo HTTP/1.1\r\nHost: google.com\r\n\r\n")
+	err := engine.Serve(context.Background(), conn)
+	assert.True(t, errors.Is(err, errs.ErrShortConnection))
+}
+
 func TestIdleTimeout(t *testing.T) {
 	engine := NewEngine(config.NewOptions(nil))
 	engine.options.IdleTimeout = 0
+	atomic.StoreUint32(&engine.status, statusRunning)
 	engine.Init()
 	engine.GET("/foo", func(c context.Context, ctx *app.RequestContext) {
 		time.Sleep(100 * time.Millisecond)
@@ -180,9 +209,9 @@ func TestIdleTimeout(t *testing.T) {
 func TestIdleTimeout01(t *testing.T) {
 	engine := NewEngine(config.NewOptions(nil))
 	engine.options.IdleTimeout = 1 * time.Second
-	engine.status = statusRunning
+	atomic.StoreUint32(&engine.status, statusRunning)
 	engine.Init()
-	// engine.status = route.statusRunning
+	atomic.StoreUint32(&engine.status, statusRunning)
 	engine.GET("/foo", func(c context.Context, ctx *app.RequestContext) {
 		time.Sleep(10 * time.Millisecond)
 		ctx.String(200, "ok")