From e2af5b079fcb3e6bf3371029ed76eeb9c11724f7 Mon Sep 17 00:00:00 2001 From: Lukasz Raczylo Date: Tue, 3 Nov 2026 00:02:15 +0000 Subject: [PATCH] fix(coalesce): copy the primary response body for waiters Waiters read the primary response buffer while fasthttp could reuse it for another request. A waiter could then send the wrong body. The primary now gives waiters their own copy. --- coalesced_snapshot_test.go | 27 +++++++++++++++++++++++++++ proxy.go | 17 ++++++++++++----- 2 files changed, 39 insertions(+), 5 deletions(-) create mode 100644 coalesced_snapshot_test.go diff --git a/coalesced_snapshot_test.go b/coalesced_snapshot_test.go new file mode 100644 index 0000000..e288d3a --- /dev/null +++ b/coalesced_snapshot_test.go @@ -0,0 +1,27 @@ +package main + +import ( + "testing" + + "github.com/gofiber/fiber/v3" + "github.com/stretchr/testify/assert" + "github.com/valyala/fasthttp" +) + +// Pins theorems/Theorems/CoalesceAlias.lean: waiters never alias the primary's response buffer. +func TestSnapshotCoalescedResponse_OwnsBody(t *testing.T) { + app := fiber.New() + fctx := &fasthttp.RequestCtx{} + c := app.AcquireCtx(fctx) + defer app.ReleaseCtx(c) + + c.Response().SetStatusCode(201) + c.Response().SetBody([]byte(`{"user":"alice"}`)) + + snap := snapshotCoalescedResponse(c) + + c.Response().SetBody([]byte(`{"user":"bobby"}`)) // buffer reused by a later request + + assert.Equal(t, 201, snap.StatusCode) + assert.Equal(t, `{"user":"alice"}`, string(snap.Body)) +} diff --git a/proxy.go b/proxy.go index c5ad4ea..79a286b 100644 --- a/proxy.go +++ b/proxy.go @@ -740,11 +740,7 @@ func performProxyRequest(c fiber.Ctx, proxyURL string) error { }, proxyErr } - return &CoalescedResponse{ - Body: c.Response().Body(), - StatusCode: c.Response().StatusCode(), - // Headers intentionally left nil; not populated or read anywhere. - }, nil + return snapshotCoalescedResponse(c), nil }) // Check for error from rc.Do (though it typically returns nil) @@ -773,6 +769,17 @@ func performProxyRequest(c fiber.Ctx, proxyURL string) error { return performProxyRequestCore(c, proxyURL, cacheKey) } +// snapshotCoalescedResponse captures the primary's response for waiters. It +// copies the body because fasthttp reuses the buffer once the primary handler +// returns, while waiters can still be reading. +func snapshotCoalescedResponse(c fiber.Ctx) *CoalescedResponse { + return &CoalescedResponse{ + Body: bytes.Clone(c.Response().Body()), + StatusCode: c.Response().StatusCode(), + // Headers intentionally left nil; not populated or read anywhere. + } +} + // performProxyRequestCore executes the proxy request with retries and circuit breaker // This is the core implementation used by both direct calls and coalesced requests func performProxyRequestCore(c fiber.Ctx, proxyURL string, cacheKey string) error {