fix: utils StreamResponseBody() memory use for large get requests

The StreamResponseBody() called ctx.Write() in a loop with a small
buffer in an attempt to stream data back to client. But the
ctx.Write() was just calling append buffer to the response instead
of streaming the data back to the client.

The correct way to stream the response back is to use
(ctx *fasthttp.RequestCtx).SetBodyStream() to set the body stream
reader, and the response will automatically get streamed back
using the reader. This will also call Close() on our body
since we are providing an io.ReadCloser.

Testing this should be done with single large get requests such as
aws s3api get-object --bucket bucket --key file /tmp/data
for very large objects. The testing shows significantly reduced
memory usage for large objects once the streaming is enabled.

Fixes #1082
This commit is contained in:
Ben McClelland
2025-02-26 10:24:46 -08:00
parent 0d94d9a77f
commit 85ba390ebd
2 changed files with 9 additions and 30 deletions

View File

@@ -644,16 +644,12 @@ func (c S3ApiController) GetActions(ctx *fiber.Ctx) error {
}
if res.Body != nil {
err := utils.StreamResponseBody(ctx, res.Body)
if err != nil {
SendResponse(ctx, nil,
&MetaOpts{
Logger: c.logger,
MetricsMng: c.mm,
Action: metrics.ActionGetObject,
BucketOwner: parsedAcl.Owner,
})
// -1 will stream response body until EOF if content length not set
contentLen := -1
if res.ContentLength != nil {
contentLen = int(*res.ContentLength)
}
utils.StreamResponseBody(ctx, res.Body, contentLen)
}
return SendResponse(ctx, nil,

View File

@@ -204,27 +204,10 @@ func SetResponseHeaders(ctx *fiber.Ctx, headers []CustomHeader) {
}
// Streams the response body by chunks
func StreamResponseBody(ctx *fiber.Ctx, rdr io.ReadCloser) error {
buf := make([]byte, 4096) // 4KB chunks
defer rdr.Close()
for {
n, err := rdr.Read(buf)
if n > 0 {
_, writeErr := ctx.Write(buf[:n])
if writeErr != nil {
return fmt.Errorf("write chunk: %w", writeErr)
}
}
if err != nil {
if errors.Is(err, io.EOF) {
break
}
return fmt.Errorf("read chunk: %w", err)
}
}
return nil
func StreamResponseBody(ctx *fiber.Ctx, rdr io.ReadCloser, bodysize int) {
// SetBodyStream will call Close() on the reader when the stream is done
// since rdr is a ReadCloser
ctx.Context().SetBodyStream(rdr, bodysize)
}
func IsValidBucketName(bucket string) bool {