gofiber / fiber

⚡️ Express inspired web framework written in Go
https://gofiber.io
MIT License
33.86k stars 1.66k forks source link

📝 [Proposal]: Add buffered streaming support #3127

Open grivera64 opened 1 month ago

grivera64 commented 1 month ago

Feature Proposal Description

This feature proposal proposes to add buffered streaming support to Fiber v3. By adding this feature, Fiber users can send shorter segments of content over persistent HTTP connections. This functionality is important for several web-based applications such as:

To implement this feature, this would involve using the underlying *fasthttp.RequestCtx struct. Specifically, this feature can leverage the RequestCtx.SetBodyStreamWriter method.

// File: https://github.com/valyala/fasthttp/server.go
// Note: type StreamWriter func(w *bufio.Writer)
func (ctx *RequestCtx) SetBodyStreamWriter(sw StreamWriter) {
    ctx.Response.SetBodyStreamWriter(sw)
}

The StreamWriter function passed into RequestCtx.SetBodyStreamWriter describes what a handler should write to an endpoint's response body as a buffered stream.

Within Fiber's existing codebase, implementing this feature is the equivalent of:

c.SendStream(fasthttp.NewStreamReader(func(w *bufio.Writer) {
    // ...
}))

It is also possible to implement this feature by directly using the fasthttp API:

// From a gofiber/recipe example:
// https://github.com/gofiber/recipes/blob/master/sse/main.go#L59
c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) {
    // ...
})

For the feature's API, please view Feature Examples below.

Alignment with Express API

Using the Express API, buffered streaming is supported for req argument in an express route handler by using the write, and end methods. Below is an example usage of this feature for Server-Side Events (SSE):

app.get('/sse', function(req, res) {
    res.setHeader('Content-Type', 'text/event-stream');
    res.setHeader('Cache-Control', 'no-cache');
    res.setHeader('Connection', 'keep-alive');
    res.flushHeaders();

    // Send messages every 2 seconds
    let intervalIndex = 0;
    const intervalId = setInterval(function() {
        intervalIndex++;

        // Format message using Server-Side Event specification
        // `data: ${content}\n\n`
        const currTime = new Date().toLocaleTimeString();
        const msg = `data: Message: ${i} - the time is ${currTime}\n\n`;

        // Write the message to the response stream (auto-flushes)
        res.write(msg);

        // Handle errors during writing
        res.on('error', function(err) {
            console.error('Error writing SSE message:', err);
            clearInterval(intervalId);
            res.end(); // Close the connection
        });
    }, 2000);

    // Handle client disconnection
    req.on('close', function() {
        clearInterval(intervalId);
        res.end();
    });
});

By providing a way to support buffered streams in Fiber, this will help make transitioning from Express much easier for more advanced use cases such as SSE as shown above.

To view the same Express example in Fiber with this new feature, please view Feature Examples.

HTTP RFC Standards Compliance

This feature does not modify how Fiber or fasthttp complies with HTTP RFC standards, as it just provides simplified access to an existing fasthttp feature.

API Stability

This feature's API will be introduced in v3 as a separate Ctx method, augmenting its interface without modifying existing methods. This will ensure that there are no breaking changes to old and new features for Fiber v3. (For more information, please view Feature Examples below.)

Besides potential naming changes, this API feature will require very few changes in the future due to fasthttp methods it relies on have been stable since 9 years ago. Unless fasthttp makes big API changes to their fasthttp.StreamWriter type, maintenance will be limited.

On the rare circumstance that the StreamWriter type does change, we can use an alias for that type and have no code change required. The only change that we would need is to update documentation.

type StreamHandler fasthttp.StreamWriter

Feature Examples

Function Signature:

func (c Ctx) SendStreamWriter(streamWriter func(*bufio.Writer)) error

Note: The name of this method is still being finalized. A few other options are:

  • WriteStream
  • SendWritableStream
  • SendBufferedStream

Use in Server-Side Events (SSE):

// Modified version of SSE example in gofiber/recipes
// https://github.com/gofiber/recipes/blob/master/sse/main.go#L59
app.Get("/sse", func (c Ctx) error {
    c.Set("Content-Type", "text/event-stream")
    c.Set("Cache-Control", "no-cache")
    c.Set("Connection", "keep-alive")
    c.Set("Transfer-Encoding", "chunked")

    return c.SendStreamWriter(func (w *bufio.Writer) {
        intervalIndex := 0
        for {
            intervalIndex++

            // Format message using Server-Side Event specification
            // `data: ${content}\n\n`
            currTime := time.Now().Local()
            msg := fmt.Sprintf("%d - the time is %v", i, currTime)    
            fmt.Fprintf(w, "data: Message: %s\n\n", msg)    // write the message

            // Handle errors while writing (after flushing)
            err := w.Flush()    // flush the message
            if err != nil {
                break    // close the connection
            }
            time.Sleep(2 * time.Second)    // Send messages every 2 seconds
        }
    })
})

Checklist:

grivera64 commented 1 month ago

I am currently working on a PR for this feature proposal.

Also: After considering the different names, I believe that either SendStreamWriter() or SendWritableStream() best fit the feature's API. Though since the code base maps SendStream() to fasthttp'sSetBodyStream() method, I will keep the name SendStreamWriter() in the PR to map to fasthttp's SetBodyStreamWriter() method.

What are your thoughts on this?