From 885f807f0d42dfc63fee1a3379756c48dd19521d Mon Sep 17 00:00:00 2001 From: Giovanni Rivera Date: Thu, 12 Sep 2024 15:26:49 -0700 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=94=A5=20Feature:=20Add=20SendStreamW?= =?UTF-8?q?riter=20to=20Ctx?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Create a new `*DefaultCtx` method called `SendStreamWriter()` that maps to fasthttp's `Response.SetBodyStreamWriter()` --- ctx.go | 9 ++++++++- ctx_interface_gen.go | 3 +++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/ctx.go b/ctx.go index 4d7417ee2a..b17959374b 100644 --- a/ctx.go +++ b/ctx.go @@ -5,6 +5,7 @@ package fiber import ( + "bufio" "bytes" "context" "crypto/tls" @@ -45,7 +46,6 @@ const userContextKey contextKey = 0 // __local_user_context__ // DefaultCtx is the default implementation of the Ctx interface // generation tool `go install github.com/vburenin/ifacemaker@975a95966976eeb2d4365a7fb236e274c54da64c` // https://github.com/vburenin/ifacemaker/blob/975a95966976eeb2d4365a7fb236e274c54da64c/ifacemaker.go#L14-L30 -// //go:generate ifacemaker --file ctx.go --struct DefaultCtx --iface Ctx --pkg fiber --output ctx_interface_gen.go --not-exported true --iface-comment "Ctx represents the Context which hold the HTTP request and response.\nIt has methods for the request query string, parameters, body, HTTP headers and so on." type DefaultCtx struct { app *App // Reference to *App @@ -1669,6 +1669,13 @@ func (c *DefaultCtx) SendStream(stream io.Reader, size ...int) error { return nil } +// SendStreamWriter sets response body stream writer +func (c *DefaultCtx) SendStreamWriter(streamWriter func(*bufio.Writer)) error { + c.fasthttp.Response.SetBodyStreamWriter(fasthttp.StreamWriter(streamWriter)) + + return nil +} + // Set sets the response's HTTP header field to the specified key, value. func (c *DefaultCtx) Set(key, val string) { c.fasthttp.Response.Header.Set(key, val) diff --git a/ctx_interface_gen.go b/ctx_interface_gen.go index 7709f7c929..35b9de1e1c 100644 --- a/ctx_interface_gen.go +++ b/ctx_interface_gen.go @@ -3,6 +3,7 @@ package fiber import ( + "bufio" "context" "crypto/tls" "io" @@ -282,6 +283,8 @@ type Ctx interface { SendString(body string) error // SendStream sets response body stream and optional body size. SendStream(stream io.Reader, size ...int) error + // SendStreamWriter sets response body stream writer + SendStreamWriter(streamWriter func(*bufio.Writer)) error // Set sets the response's HTTP header field to the specified key, value. Set(key, val string) setCanonical(key, val string) From 1ff06dcbc997a8895c599625ad5355e4d4cccbbc Mon Sep 17 00:00:00 2001 From: Giovanni Rivera Date: Thu, 12 Sep 2024 17:00:34 -0700 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=9A=A8=20Test:=20Validate=20regular?= =?UTF-8?q?=20use=20of=20c.SendStreamWriter()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adds Test_Ctx_SendStreamWriter to ctx_test.go --- ctx_test.go | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/ctx_test.go b/ctx_test.go index 6dcd74109e..45b094b13c 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -4327,6 +4327,35 @@ func Test_Ctx_SendStream(t *testing.T) { require.Equal(t, "Hello bufio", string(c.Response().Body())) } +// go test -run Test_Ctx_SendStreamWriter +func Test_Ctx_SendStreamWriter(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + + err := c.SendStreamWriter(func(w *bufio.Writer) { + w.WriteString("Don't crash please") //nolint:errcheck, revive // It is fine to ignore the error + }) + require.NoError(t, err) + require.Equal(t, "Don't crash please", string(c.Response().Body())) + + err = c.SendStreamWriter(func(w *bufio.Writer) { + for lineNum := 1; lineNum <= 5; lineNum++ { + fmt.Fprintf(w, "Line %d\n", lineNum) //nolint:errcheck, revive // It is fine to ignore the error + if err := w.Flush(); err != nil { + t.Errorf("unexpected error: %s", err) + return + } + } + }) + require.NoError(t, err) + require.Equal(t, "Line 1\nLine 2\nLine 3\nLine 4\nLine 5\n", string(c.Response().Body())) + + err = c.SendStreamWriter(func(_ *bufio.Writer) {}) + require.NoError(t, err) + require.Empty(t, c.Response().Body()) +} + // go test -run Test_Ctx_Set func Test_Ctx_Set(t *testing.T) { t.Parallel() From c977b38ff1d7a31452bf3a39650d2d8c790780d5 Mon Sep 17 00:00:00 2001 From: Giovanni Rivera Date: Thu, 12 Sep 2024 17:42:04 -0700 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=9A=A8=20Test:=20(WIP)=20Validate=20i?= =?UTF-8?q?nterrupted=20use=20of=20c.SendStreamWriter()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adds Test_Ctx_SendStreamWriter_Interrupted to ctx_test.go - (Work-In-Progress) This test verifies that some data is still sent before a client disconnects when using the method `c.SendStreamWriter()`. **Note:** Running this test reports a race condition when using the `-race` flag or running `make test`. The test uses a channel and mutex to prevent race conditions, but still triggers a warning. --- ctx.go | 1 + ctx_test.go | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/ctx.go b/ctx.go index b17959374b..2628f5121d 100644 --- a/ctx.go +++ b/ctx.go @@ -46,6 +46,7 @@ const userContextKey contextKey = 0 // __local_user_context__ // DefaultCtx is the default implementation of the Ctx interface // generation tool `go install github.com/vburenin/ifacemaker@975a95966976eeb2d4365a7fb236e274c54da64c` // https://github.com/vburenin/ifacemaker/blob/975a95966976eeb2d4365a7fb236e274c54da64c/ifacemaker.go#L14-L30 +// //go:generate ifacemaker --file ctx.go --struct DefaultCtx --iface Ctx --pkg fiber --output ctx_interface_gen.go --not-exported true --iface-comment "Ctx represents the Context which hold the HTTP request and response.\nIt has methods for the request query string, parameters, body, HTTP headers and so on." type DefaultCtx struct { app *App // Reference to *App diff --git a/ctx_test.go b/ctx_test.go index 45b094b13c..937ff3d018 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -24,6 +24,7 @@ import ( "path/filepath" "strconv" "strings" + "sync" "testing" "text/template" "time" @@ -4356,6 +4357,44 @@ func Test_Ctx_SendStreamWriter(t *testing.T) { require.Empty(t, c.Response().Body()) } +// go test -run Test_Ctx_SendStreamWriter_Interrupted +func Test_Ctx_SendStreamWriter_Interrupted(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + + var mutex sync.Mutex + startChan := make(chan bool) + interruptStreamWriter := func() { + <-startChan + time.Sleep(5 * time.Millisecond) + mutex.Lock() + c.Response().CloseBodyStream() //nolint:errcheck // It is fine to ignore the error + mutex.Unlock() + } + err := c.SendStreamWriter(func(w *bufio.Writer) { + go interruptStreamWriter() + + startChan <- true + for lineNum := 1; lineNum <= 5; lineNum++ { + mutex.Lock() + fmt.Fprintf(w, "Line %d\n", lineNum) //nolint:errcheck, revive // It is fine to ignore the error + mutex.Unlock() + + if err := w.Flush(); err != nil { + if lineNum < 3 { + t.Errorf("unexpected error: %s", err) + } + return + } + + time.Sleep(1500 * time.Microsecond) + } + }) + require.NoError(t, err) + require.Equal(t, "Line 1\nLine 2\nLine 3\n", string(c.Response().Body())) +} + // go test -run Test_Ctx_Set func Test_Ctx_Set(t *testing.T) { t.Parallel() From 024ac5e8313fb5eff640419006361f333d596ee6 Mon Sep 17 00:00:00 2001 From: Giovanni Rivera Date: Fri, 13 Sep 2024 17:36:10 -0700 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=93=9A=20Doc:=20Add=20`SendStreamWrit?= =?UTF-8?q?er`=20to=20docs/api/ctx.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/api/ctx.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/docs/api/ctx.md b/docs/api/ctx.md index 2640512a3e..9007137266 100644 --- a/docs/api/ctx.md +++ b/docs/api/ctx.md @@ -1871,6 +1871,47 @@ app.Get("/", func(c fiber.Ctx) error { }) ``` +## SendStreamWriter + +Sets the response body stream writer. + +:::note +The argument `streamWriter` represents a function that populates +the response body using a buffered stream writer. +::: + +```go title="Signature" +func (c Ctx) SendStreamWriter(streamWriter func(*bufio.Writer)) error +``` + +```go title="Example" +app.Get("/", func (c fiber.Ctx) error { + return c.SendStreamWriter(func(w *bufio.Writer) { + fmt.Fprintf(w, "Hello, World!\n") + }) + // => "Hello, World!" +}) +``` + +:::info +To flush data before the function returns, you can call `w.Flush()` +on the provided writer. Otherwise, the buffered stream flushes after +`streamWriter` returns. +::: + +```go title="Example" +app.Get("/wait", func(c fiber.Ctx) error { + return c.SendStreamWriter(func(w *bufio.Writer) { + fmt.Fprintf(w, "Waiting for 10 seconds\n") + if err := w.Flush(); err != nil { + log.Print("User quit early") + } + time.Sleep(10 * time.Second) + fmt.Fprintf(w, "Done!\n") + }) +}) +``` + ## Set Sets the response’s HTTP header field to the specified `key`, `value`.