Repository: cloudwego/hertz
Pull request #611 (2023-02-14 13:17:45)

optimize(hz): package name
#### 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
-->
optimize
#### 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:
-->
- [ ] This PR title match the format: \<type\>(optional scope): \<description\>
- [ ] The description of this PR title is user-oriented and clear enough for others to understand.


#### (Optional) Translate the PR title into Chinese.
优化生成代码的 package 名

#### (Optional) More detail description for this PR(en: English/zh: Chinese).
<!--
Provide more detailed info for review. If it is a perf type PR, perf data is suggested to give.
-->
en: optimize 'package' name for generated file
zh(optional): 优化生成代码的 package 名

- [ ] package name of "handler/ping.go" will modify by "handler_dir" flag
- [ ] package name of "router/register.go"(generated by layout/package) will modify by "router_dir" flag

#### Which issue(s) this PR fixes:
<!--
Automatically closes linked issue when PR is merged.
Eg: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
-->


codecov[bot]: # [Codecov](https://codecov.io/gh/cloudwego/hertz/pull/611?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego) Report
Base: **72.12**% // Head: **72.04**% // Decreases project coverage by **`-0.08%`** :warning:
> Coverage data is based on head [(`071fb86`)](https://codecov.io/gh/cloudwego/hertz/pull/611?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego) compared to base [(`e4cc656`)](https://codecov.io/gh/cloudwego/hertz/commit/e4cc65689777ea5fe6101a6e6037e1b57944c4c3?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego).
> Patch has no changes to coverable lines.

> :exclamation: Current head 071fb86 differs from pull request most recent head 1a27a3e. Consider uploading reports for the commit 1a27a3e to get more accurate results

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##           develop     #611      +/-   ##
===========================================
- Coverage    72.12%   72.04%   -0.08%     
===========================================
  Files           94       94              
  Lines         9234     9234              
===========================================
- Hits          6660     6653       -7     
- Misses        2155     2162       +7     
  Partials       419      419              
```


| [Impacted Files](https://codecov.io/gh/cloudwego/hertz/pull/611?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego) | Coverage Δ | |
|---|---|---|
| [pkg/common/timer/timer.go](https://codecov.io/gh/cloudwego/hertz/pull/611?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego#diff-cGtnL2NvbW1vbi90aW1lci90aW1lci5nbw==) | `61.90% <0.00%> (-19.05%)` | :arrow_down: |
| [pkg/common/compress/compress.go](https://codecov.io/gh/cloudwego/hertz/pull/611?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego#diff-cGtnL2NvbW1vbi9jb21wcmVzcy9jb21wcmVzcy5nbw==) | `80.95% <0.00%> (-2.86%)` | :arrow_down: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/cloudwego/hertz/pull/611?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego).

FGYFFFF: @welkeyever @li-jin-gou @Duslia @wzekin cc
welkeyever: Expand the specific optimization in the pr description~
FGYFFFF: > Expand the specific optimization in the pr description~

done

Patch:
diff --git a/cmd/hz/generator/layout.go b/cmd/hz/generator/layout.go
index 2db26b2..3292d38 100644
--- a/cmd/hz/generator/layout.go
+++ b/cmd/hz/generator/layout.go
@@ -153,11 +153,21 @@ func serviceToLayoutData(service Layout) (map[string]interface{}, error) {
 	if goMod == "" {
 		return nil, errors.New("please specify go-module")
 	}
+	handlerPkg := filepath.Base(defaultHandlerDir)
+	if len(service.HandlerDir) != 0 {
+		handlerPkg = filepath.Base(service.HandlerDir)
+	}
+	routerPkg := filepath.Base(defaultRouterDir)
+	if len(service.RouterDir) != 0 {
+		routerPkg = filepath.Base(service.RouterDir)
+	}
 
 	return map[string]interface{}{
 		"GoModule":        goMod,
 		"ServiceName":     service.ServiceName,
 		"UseApacheThrift": service.UseApacheThrift,
+		"HandlerPkg":      handlerPkg,
+		"RouterPkg":       routerPkg,
 	}, nil
 }
 
diff --git a/cmd/hz/generator/layout_tpl.go b/cmd/hz/generator/layout_tpl.go
index 6d88bac..6f2b4c3 100644
--- a/cmd/hz/generator/layout_tpl.go
+++ b/cmd/hz/generator/layout_tpl.go
@@ -126,7 +126,7 @@ dumped_hertz_remote_config.json
 			Path: defaultHandlerDir + sp + "ping.go",
 			Body: `// Code generated by hertz generator.
 
-package handler
+package {{.HandlerPkg}}
 
 import (
 	"context"
@@ -187,7 +187,7 @@ func customizedRegister(r *server.Hertz){
 			Path: defaultRouterDir + sp + registerTplName,
 			Body: `// Code generated by hertz generator. DO NOT EDIT.
 
-package router
+package {{.RouterPkg}}
 
 import (
 	"github.com/cloudwego/hertz/pkg/app/server"
diff --git a/cmd/hz/generator/package_tpl.go b/cmd/hz/generator/package_tpl.go
index e313d91..1947c36 100644
--- a/cmd/hz/generator/package_tpl.go
+++ b/cmd/hz/generator/package_tpl.go
@@ -146,17 +146,17 @@ func Register(r *server.Hertz) {
 			Path: defaultRouterDir + sp + registerTplName,
 			Body: `// Code generated by hertz generator. DO NOT EDIT.
 
-package router
+package {{.PackageName}}
 
 import (
 	"github.com/cloudwego/hertz/pkg/app/server"
-	{{$.PkgAlias}} "{{$.Pkg}}"
+	{{$.DepPkgAlias}} "{{$.DepPkg}}"
 )
 
 // GeneratedRegister registers routers generated by IDL.
 func GeneratedRegister(r *server.Hertz){
 	` + insertPointNew + `
-	{{$.PkgAlias}}.Register(r)
+	{{$.DepPkgAlias}}.Register(r)
 }
 `,
 		},
diff --git a/cmd/hz/generator/router.go b/cmd/hz/generator/router.go
index 3ea65fc..e335a26 100644
--- a/cmd/hz/generator/router.go
+++ b/cmd/hz/generator/router.go
@@ -48,9 +48,10 @@ type RouterNode struct {
 	HttpMethod          string
 }
 
-type RegisterDependency struct {
-	PkgAlias string
-	Pkg      string
+type RegisterInfo struct {
+	PackageName string
+	DepPkgAlias string
+	DepPkg      string
 }
 
 // NewRouterTree contains "/" as root node
@@ -237,9 +238,10 @@ var (
 )
 
 func (pkgGen *HttpPackageGenerator) updateRegister(pkg, rDir, pkgName string) error {
-	register := RegisterDependency{
-		PkgAlias: strings.ReplaceAll(pkgName, "/", "_"),
-		Pkg:      pkg,
+	register := RegisterInfo{
+		PackageName: filepath.Base(rDir),
+		DepPkgAlias: strings.ReplaceAll(pkgName, "/", "_"),
+		DepPkg:      pkg,
 	}
 	registerPath := filepath.Join(rDir, registerTplName)
 	isExist, err := util.PathExist(registerPath)
@@ -255,13 +257,13 @@ func (pkgGen *HttpPackageGenerator) updateRegister(pkg, rDir, pkgName string) er
 		return fmt.Errorf("read register '%s' failed, err: %v", registerPath, err.Error())
 	}
 
-	if !bytes.Contains(file, []byte(register.Pkg)) {
-		file, err = util.AddImport(registerPath, register.PkgAlias, register.Pkg)
+	if !bytes.Contains(file, []byte(register.DepPkg)) {
+		file, err = util.AddImport(registerPath, register.DepPkgAlias, register.DepPkg)
 		if err != nil {
 			return err
 		}
 
-		insertReg := register.PkgAlias + ".Register(r)\n"
+		insertReg := register.DepPkgAlias + ".Register(r)\n"
 		if bytes.Contains(file, []byte(insertReg)) {
 			return fmt.Errorf("the router(%s) has been registered", insertReg)
 		}
@@ -289,7 +291,7 @@ func (pkgGen *HttpPackageGenerator) genRouter(pkg *HttpPackage, root *RouterNode
 	}
 	router := Router{
 		FilePath:    filepath.Join(routerDir, util.BaseNameAndTrim(pkg.IdlName)+".go"),
-		PackageName: util.ToCamelCase(util.BaseName(routerPackage, "")),
+		PackageName: filepath.Base(routerDir),
 		HandlerPackages: map[string]string{
 			util.BaseName(handlerPackage, ""): handlerPackage,
 		},
diff --git a/pkg/app/context.go b/pkg/app/context.go
index e86cc4b..3ed3f19 100644
--- a/pkg/app/context.go
+++ b/pkg/app/context.go
@@ -211,6 +211,15 @@ type RequestContext struct {
 	formValueFunc FormValueFunc
 }
 
+// Flush is the shortcut for ctx.Response.GetHijackWriter().Flush().
+// Will return nil if the response writer is not hijacked.
+func (ctx *RequestContext) Flush() error {
+	if ctx.Response.GetHijackWriter() == nil {
+		return nil
+	}
+	return ctx.Response.GetHijackWriter().Flush()
+}
+
 func (ctx *RequestContext) SetClientIPFunc(f ClientIP) {
 	ctx.clientIPFunc = f
 }
diff --git a/pkg/app/fs.go b/pkg/app/fs.go
index 72aad87..5bad7cc 100644
--- a/pkg/app/fs.go
+++ b/pkg/app/fs.go
@@ -276,8 +276,6 @@ func (r *fsSmallFileReader) WriteTo(w io.Writer) (int64, error) {
 // Directory contents is returned if path points to directory.
 //
 // Use ServeFileUncompressed is you don't need serving compressed file contents.
-//
-// See also RequestCtx.SendFile.
 func ServeFile(ctx *RequestContext, path string) {
 	rootFSOnce.Do(func() {
 		rootFSHandler = rootFS.NewRequestHandler()
@@ -1204,8 +1202,6 @@ func stripLeadingSlashes(path []byte, stripSlashes int) []byte {
 //
 // ServeFile may be used for saving network traffic when serving files
 // with good compression ratio.
-//
-// See also RequestCtx.SendFile.
 func ServeFileUncompressed(ctx *RequestContext, path string) {
 	ctx.Request.Header.DelBytes(bytestr.StrAcceptEncoding)
 	ServeFile(ctx, path)
diff --git a/pkg/network/writer.go b/pkg/network/writer.go
index 0b3a405..ed6c6ac 100644
--- a/pkg/network/writer.go
+++ b/pkg/network/writer.go
@@ -103,3 +103,12 @@ func NewWriter(w io.Writer) Writer {
 		w: w,
 	}
 }
+
+type ExtWriter interface {
+	io.Writer
+	Flush() error
+
+	// Finalize will be called by framework before the writer is released.
+	// Implementations must guarantee that Finalize is safe for multiple calls.
+	Finalize() error
+}
diff --git a/pkg/protocol/header.go b/pkg/protocol/header.go
index 693ae47..d961418 100644
--- a/pkg/protocol/header.go
+++ b/pkg/protocol/header.go
@@ -87,7 +87,7 @@ type RequestHeader struct {
 
 	h       []argsKV
 	bufKV   argsKV
-	trailer Trailer
+	trailer *Trailer
 
 	cookies []argsKV
 
@@ -127,7 +127,7 @@ type ResponseHeader struct {
 
 	h       []argsKV
 	bufKV   argsKV
-	trailer Trailer
+	trailer *Trailer
 
 	cookies []argsKV
 
@@ -1788,10 +1788,16 @@ func (h *RequestHeader) setSpecialHeader(key, value []byte) bool {
 
 // Trailer returns the Trailer of HTTP Header.
 func (h *ResponseHeader) Trailer() *Trailer {
-	return &h.trailer
+	if h.trailer == nil {
+		h.trailer = new(Trailer)
+	}
+	return h.trailer
 }
 
 // Trailer returns the Trailer of HTTP Header.
 func (h *RequestHeader) Trailer() *Trailer {
-	return &h.trailer
+	if h.trailer == nil {
+		h.trailer = new(Trailer)
+	}
+	return h.trailer
 }
diff --git a/pkg/protocol/http1/ext/common.go b/pkg/protocol/http1/ext/common.go
index 94526b7..b31b40c 100644
--- a/pkg/protocol/http1/ext/common.go
+++ b/pkg/protocol/http1/ext/common.go
@@ -118,14 +118,14 @@ func WriteBodyChunked(w network.Writer, r io.Reader) error {
 				panic("BUG: io.Reader returned 0, nil")
 			}
 			if err == io.EOF {
-				if err = writeChunk(w, buf[:0]); err != nil {
+				if err = WriteChunk(w, buf[:0], true); err != nil {
 					break
 				}
 				err = nil
 			}
 			break
 		}
-		if err = writeChunk(w, buf[:n]); err != nil {
+		if err = WriteChunk(w, buf[:n], true); err != nil {
 			break
 		}
 	}
@@ -287,7 +287,7 @@ func round2(n int) int {
 	return 1 << x
 }
 
-func writeChunk(w network.Writer, b []byte) (err error) {
+func WriteChunk(w network.Writer, b []byte, withFlush bool) (err error) {
 	n := len(b)
 	if err = bytesconv.WriteHexInt(w, n); err != nil {
 		return err
@@ -303,6 +303,9 @@ func writeChunk(w network.Writer, b []byte) (err error) {
 		w.WriteBinary(bytestr.StrCRLF) //nolint:errcheck
 	}
 
+	if !withFlush {
+		return nil
+	}
 	err = w.Flush()
 	return
 }
diff --git a/pkg/protocol/http1/resp/writer.go b/pkg/protocol/http1/resp/writer.go
new file mode 100644
index 0000000..745ab4c
--- /dev/null
+++ b/pkg/protocol/http1/resp/writer.go
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2023 CloudWeGo Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package resp
+
+import (
+	"sync"
+
+	"github.com/cloudwego/hertz/pkg/network"
+	"github.com/cloudwego/hertz/pkg/protocol"
+	"github.com/cloudwego/hertz/pkg/protocol/http1/ext"
+)
+
+type chunkedBodyWriter struct {
+	sync.Once
+	finalizeErr error
+	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.
+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 err = ext.WriteChunk(c.w, p, false); err != nil {
+		return
+	}
+	return len(p), nil
+}
+
+func (c *chunkedBodyWriter) Flush() error {
+	return c.w.Flush()
+}
+
+// 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() {
+		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
+}
+
+func NewChunkedBodyWriter(r *protocol.Response, w network.Writer) network.ExtWriter {
+	return &chunkedBodyWriter{
+		r: r,
+		w: w,
+	}
+}
diff --git a/pkg/protocol/http1/server.go b/pkg/protocol/http1/server.go
index 99d2da7..ba8915a 100644
--- a/pkg/protocol/http1/server.go
+++ b/pkg/protocol/http1/server.go
@@ -413,6 +413,11 @@ func writeErrorResponse(zw network.Writer, ctx *app.RequestContext, serverName [
 }
 
 func writeResponse(ctx *app.RequestContext, w network.Writer) error {
+	// Skip default response writing logic if it has been hijacked
+	if ctx.Response.GetHijackWriter() != nil {
+		return ctx.Response.GetHijackWriter().Finalize()
+	}
+
 	err := resp.Write(&ctx.Response, w)
 	if err != nil {
 		return err
diff --git a/pkg/protocol/response.go b/pkg/protocol/response.go
index 04440f3..016af92 100644
--- a/pkg/protocol/response.go
+++ b/pkg/protocol/response.go
@@ -46,6 +46,7 @@ import (
 	"net"
 	"sync"
 
+	"github.com/cloudwego/hertz/internal/bytesconv"
 	"github.com/cloudwego/hertz/internal/nocopy"
 	"github.com/cloudwego/hertz/pkg/common/bytebufferpool"
 	"github.com/cloudwego/hertz/pkg/common/compress"
@@ -90,6 +91,17 @@ type Response struct {
 	raddr net.Addr
 	// Local TCPAddr from concurrently net.Conn
 	laddr net.Addr
+
+	// If set a hijackWriter, hertz will skip the default header/body writer process.
+	hijackWriter network.ExtWriter
+}
+
+func (resp *Response) GetHijackWriter() network.ExtWriter {
+	return resp.hijackWriter
+}
+
+func (resp *Response) HijackWriter(writer network.ExtWriter) {
+	resp.hijackWriter = writer
 }
 
 type responseBodyWriter struct {
@@ -133,7 +145,7 @@ func (resp *Response) ConstructBodyStream(body *bytebufferpool.ByteBuffer, bodyS
 // BodyWriter returns writer for populating response body.
 //
 // If used inside RequestHandler, the returned writer must not be used
-// after returning from RequestHandler. Use RequestCtx.Write
+// after returning from RequestHandler. Use RequestContext.Write
 // or SetBodyStreamWriter in this case.
 func (resp *Response) BodyWriter() io.Writer {
 	resp.w.r = resp
@@ -265,6 +277,7 @@ func (resp *Response) Reset() {
 	resp.raddr = nil
 	resp.laddr = nil
 	resp.ImmediateHeaderFlush = false
+	resp.hijackWriter = nil
 }
 
 func (resp *Response) resetSkipHeader() {
@@ -302,8 +315,22 @@ func (resp *Response) StatusCode() int {
 //
 // It is safe re-using body argument after the function returns.
 func (resp *Response) SetBody(body []byte) {
-	resp.CloseBodyStream()      //nolint:errcheck
-	resp.BodyBuffer().Set(body) //nolint:errcheck
+	resp.CloseBodyStream() //nolint:errcheck
+	if resp.GetHijackWriter() == nil {
+		resp.BodyBuffer().Set(body) //nolint:errcheck
+		return
+	}
+
+	// If the hijack writer support .SetBody() api, then use it.
+	if setter, ok := resp.GetHijackWriter().(interface {
+		SetBody(b []byte)
+	}); ok {
+		setter.SetBody(body)
+		return
+	}
+
+	// Otherwise, call .Write() api instead.
+	resp.GetHijackWriter().Write(body) //nolint:errcheck
 }
 
 func (resp *Response) BodyStream() io.Reader {
@@ -314,13 +341,21 @@ func (resp *Response) BodyStream() io.Reader {
 //
 // It is safe re-using p after the function returns.
 func (resp *Response) AppendBody(p []byte) {
-	resp.CloseBodyStream()     //nolint:errcheck
+	resp.CloseBodyStream() //nolint:errcheck
+	if resp.hijackWriter != nil {
+		resp.hijackWriter.Write(p) //nolint:errcheck
+		return
+	}
 	resp.BodyBuffer().Write(p) //nolint:errcheck
 }
 
 // AppendBodyString appends s to response body.
 func (resp *Response) AppendBodyString(s string) {
-	resp.CloseBodyStream()           //nolint:errcheck
+	resp.CloseBodyStream() //nolint:errcheck
+	if resp.hijackWriter != nil {
+		resp.hijackWriter.Write(bytesconv.S2b(s)) //nolint:errcheck
+		return
+	}
 	resp.BodyBuffer().WriteString(s) //nolint:errcheck
 }
 


Test patch:
diff --git a/pkg/common/test/mock/network.go b/pkg/common/test/mock/network.go
index bb08cb4..22c8266 100644
--- a/pkg/common/test/mock/network.go
+++ b/pkg/common/test/mock/network.go
@@ -32,6 +32,12 @@ type Conn struct {
 	readTimeout time.Duration
 	zr          network.Reader
 	zw          network.ReadWriter
+	wroteLen    int
+}
+
+type Recorder interface {
+	network.Reader
+	WroteLen() int
 }
 
 func (m *Conn) SetWriteTimeout(t time.Duration) error {
@@ -98,19 +104,31 @@ func (m *Conn) Len() int {
 }
 
 func (m *Conn) Malloc(n int) (buf []byte, err error) {
+	m.wroteLen += n
 	return m.zw.Malloc(n)
 }
 
 func (m *Conn) WriteBinary(b []byte) (n int, err error) {
-	return m.zw.WriteBinary(b)
+	n, err = m.zw.WriteBinary(b)
+	m.wroteLen += n
+	return n, err
 }
 
 func (m *Conn) Flush() error {
 	return m.zw.Flush()
 }
 
-func (m *Conn) WriterRecorder() network.Reader {
-	return m.zw
+func (m *Conn) WriterRecorder() Recorder {
+	return &recorder{c: m, Reader: m.zw}
+}
+
+type recorder struct {
+	c *Conn
+	network.Reader
+}
+
+func (r *recorder) WroteLen() int {
+	return r.c.wroteLen
 }
 
 func (m *SlowReadConn) Peek(i int) ([]byte, error) {
diff --git a/pkg/common/test/mock/writer.go b/pkg/common/test/mock/writer.go
new file mode 100644
index 0000000..b326242
--- /dev/null
+++ b/pkg/common/test/mock/writer.go
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2023 CloudWeGo Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package mock
+
+import "bytes"
+
+type ExtWriter struct {
+	tmp     []byte
+	Buf     *bytes.Buffer
+	IsFinal *bool
+}
+
+func (m *ExtWriter) Write(p []byte) (n int, err error) {
+	m.tmp = p
+	return len(p), nil
+}
+
+func (m *ExtWriter) Flush() error {
+	_, err := m.Buf.Write(m.tmp)
+	return err
+}
+
+func (m *ExtWriter) Finalize() error {
+	if !*m.IsFinal {
+		*m.IsFinal = true
+	}
+	return nil
+}
+
+func (m *ExtWriter) SetBody(body []byte) {
+	m.Buf.Reset()
+	m.tmp = body
+}
diff --git a/pkg/protocol/http1/resp/writer_test.go b/pkg/protocol/http1/resp/writer_test.go
new file mode 100644
index 0000000..bca2d43
--- /dev/null
+++ b/pkg/protocol/http1/resp/writer_test.go
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2023 CloudWeGo Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package resp
+
+import (
+	"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"
+)
+
+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)))
+}
+
+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)))
+}
diff --git a/pkg/protocol/http1/server_test.go b/pkg/protocol/http1/server_test.go
index 4a2a15a..dfa8b78 100644
--- a/pkg/protocol/http1/server_test.go
+++ b/pkg/protocol/http1/server_test.go
@@ -17,6 +17,7 @@
 package http1
 
 import (
+	"bytes"
 	"context"
 	"errors"
 	"sync"
@@ -31,6 +32,8 @@ import (
 	"github.com/cloudwego/hertz/pkg/common/tracer/stats"
 	"github.com/cloudwego/hertz/pkg/common/tracer/traceinfo"
 	"github.com/cloudwego/hertz/pkg/network"
+	"github.com/cloudwego/hertz/pkg/protocol"
+	"github.com/cloudwego/hertz/pkg/protocol/http1/resp"
 )
 
 var pool = &sync.Pool{New: func() interface{} {
@@ -189,9 +192,65 @@ func TestEventStack(t *testing.T) {
 	}
 }
 
+func TestDefaultWriter(t *testing.T) {
+	server := &Server{}
+	reqCtx := &app.RequestContext{}
+	server.Core = &mockCore{
+		ctxPool: &sync.Pool{New: func() interface{} {
+			return reqCtx
+		}},
+		mockHandler: func(c context.Context, ctx *app.RequestContext) {
+			ctx.Write([]byte("hello, hertz"))
+			ctx.Flush()
+		},
+	}
+	defaultConn := mock.NewConn("GET / HTTP/1.1\nHost: foobar.com\n\n")
+	err := server.Serve(context.TODO(), defaultConn)
+	assert.True(t, errors.Is(err, errs.ErrShortConnection))
+	defaultResponseResult := defaultConn.WriterRecorder()
+	assert.DeepEqual(t, 0, defaultResponseResult.Len()) // all data is flushed so the buffer length is 0
+	response := protocol.AcquireResponse()
+	resp.Read(response, defaultResponseResult)
+	assert.DeepEqual(t, "hello, hertz", string(response.Body()))
+}
+
+func TestHijackResponseWriter(t *testing.T) {
+	server := &Server{}
+	reqCtx := &app.RequestContext{}
+	buf := new(bytes.Buffer)
+	isFinal := false
+	server.Core = &mockCore{
+		ctxPool: &sync.Pool{New: func() interface{} {
+			return reqCtx
+		}},
+		mockHandler: func(c context.Context, ctx *app.RequestContext) {
+			// response before write will be dropped
+			ctx.Write([]byte("invalid data"))
+
+			ctx.Response.HijackWriter(&mock.ExtWriter{
+				Buf:     buf,
+				IsFinal: &isFinal,
+			})
+
+			ctx.Write([]byte("hello, hertz"))
+			ctx.Flush()
+		},
+	}
+	defaultConn := mock.NewConn("GET / HTTP/1.1\nHost: foobar.com\n\n")
+	err := server.Serve(context.TODO(), defaultConn)
+	assert.True(t, errors.Is(err, errs.ErrShortConnection))
+	defaultResponseResult := defaultConn.WriterRecorder()
+	response := protocol.AcquireResponse()
+	resp.Read(response, defaultResponseResult)
+	assert.DeepEqual(t, 0, len(response.Body()))
+	assert.DeepEqual(t, "hello, hertz", buf.String())
+	assert.True(t, isFinal)
+}
+
 type mockCore struct {
-	ctxPool    *sync.Pool
-	controller tracer.Controller
+	ctxPool     *sync.Pool
+	controller  tracer.Controller
+	mockHandler func(c context.Context, ctx *app.RequestContext)
 }
 
 func (m *mockCore) IsRunning() bool {
@@ -202,7 +261,11 @@ func (m *mockCore) GetCtxPool() *sync.Pool {
 	return m.ctxPool
 }
 
-func (m *mockCore) ServeHTTP(c context.Context, ctx *app.RequestContext) {}
+func (m *mockCore) ServeHTTP(c context.Context, ctx *app.RequestContext) {
+	if m.mockHandler != nil {
+		m.mockHandler(c, ctx)
+	}
+}
 
 func (m *mockCore) GetTracer() tracer.Controller {
 	return m.controller
diff --git a/pkg/protocol/response_test.go b/pkg/protocol/response_test.go
index 1ecb806..20a18ff 100644
--- a/pkg/protocol/response_test.go
+++ b/pkg/protocol/response_test.go
@@ -51,6 +51,7 @@ import (
 	"github.com/cloudwego/hertz/pkg/common/bytebufferpool"
 	"github.com/cloudwego/hertz/pkg/common/compress"
 	"github.com/cloudwego/hertz/pkg/common/test/assert"
+	"github.com/cloudwego/hertz/pkg/common/test/mock"
 	"github.com/cloudwego/hertz/pkg/protocol/consts"
 )
 
@@ -272,3 +273,24 @@ func TestRespSafeCopy(t *testing.T) {
 		assert.DeepEqual(t, []byte{byte(i)}, resps[i].Body())
 	}
 }
+
+func TestResponse_HijackWriter(t *testing.T) {
+	resp := AcquireResponse()
+	buf := new(bytes.Buffer)
+	isFinal := false
+	resp.HijackWriter(&mock.ExtWriter{Buf: buf, IsFinal: &isFinal})
+	resp.AppendBody([]byte("hello"))
+	assert.DeepEqual(t, 0, buf.Len())
+	resp.GetHijackWriter().Flush()
+	assert.DeepEqual(t, "hello", buf.String())
+	resp.AppendBodyString(", world")
+	assert.DeepEqual(t, "hello", buf.String())
+	resp.GetHijackWriter().Flush()
+	assert.DeepEqual(t, "hello, world", buf.String())
+	resp.SetBody([]byte("hello, hertz"))
+	resp.GetHijackWriter().Flush()
+	assert.DeepEqual(t, "hello, hertz", buf.String())
+	assert.False(t, isFinal)
+	resp.GetHijackWriter().Finalize()
+	assert.True(t, isFinal)
+}