Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,3 +219,7 @@ go tool pprof mem.prof
Remember: You're Bolt, making switchAILocal lightning fast. But speed without correctness is useless. Measure, optimize, verify.

**If you can't find a clear performance win today, stop and do not create a PR.**

## 2026-08-20 - Pool bytes.Buffer in HTTP Middleware
**Learning:** Naive buffer allocation in HTTP middleware (`ResponseWriterWrapper` and `ResponseRewriter`) causes massive heap allocations and high GC pressure during streaming and large non-streaming requests. Returning them to a `sync.Pool` without `.Cap()` checks causes long-term memory leaks when processing occasional massive payloads.
**Action:** Always recycle `bytes.Buffer` in middleware via `sync.Pool`. Ensure the buffer is explicitly released *after* the request is fully complete (e.g. at the handler level, not inside `Flush()`), and enforce a `.Cap()` check (e.g., `<= 128*1024`) before returning the buffer to the pool to prevent memory bloat.
3 changes: 3 additions & 0 deletions internal/api/middleware/request_logging.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ func RequestLoggingMiddleware(logger logging.RequestLogger) gin.HandlerFunc {
// Error is logged but we continue serving the response
_ = err
}

// Release the buffer back to the pool
wrapper.Release()
}
}

Expand Down
22 changes: 21 additions & 1 deletion internal/api/middleware/response_writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"bytes"
"net/http"
"strings"
"sync"

"github.com/gin-gonic/gin"
"github.com/traylinx/switchAILocal/internal/interfaces"
Expand All @@ -28,6 +29,13 @@ type RequestInfo struct {

// ResponseWriterWrapper wraps the standard gin.ResponseWriter to intercept and log response data.
// It is designed to handle both standard and streaming responses, ensuring that logging operations do not block the client response.

var responseBodyBufferPool = sync.Pool{
New: func() interface{} {
return &bytes.Buffer{}
},
}

type ResponseWriterWrapper struct {
gin.ResponseWriter
body *bytes.Buffer // body is a buffer to store the response body for non-streaming responses.
Expand Down Expand Up @@ -55,7 +63,7 @@ type ResponseWriterWrapper struct {
func NewResponseWriterWrapper(w gin.ResponseWriter, logger logging.RequestLogger, requestInfo *RequestInfo) *ResponseWriterWrapper {
return &ResponseWriterWrapper{
ResponseWriter: w,
body: &bytes.Buffer{},
body: responseBodyBufferPool.Get().(*bytes.Buffer),
logger: logger,
requestInfo: requestInfo,
headers: make(map[string][]string),
Expand Down Expand Up @@ -304,6 +312,18 @@ func (w *ResponseWriterWrapper) Finalize(c *gin.Context) error {
return w.logRequest(finalStatusCode, w.cloneHeaders(), w.body.Bytes(), w.extractAPIRequest(c), w.extractAPIResponse(c), slicesAPIResponseError, forceLog)
}

// Release returns the internal buffer to the sync.Pool to prevent memory bloat.
// This must be called after the request is completely finished and logged.
func (w *ResponseWriterWrapper) Release() {
if w.body != nil {
if w.body.Cap() <= 128*1024 {
w.body.Reset()
responseBodyBufferPool.Put(w.body)
}
w.body = nil
}
}

func (w *ResponseWriterWrapper) cloneHeaders() map[string][]string {
w.ensureHeadersCaptured()

Expand Down
Loading