Skip to content

test: add tests reproducing singleflight body duplication and buffer pool races#703

Open
cdaguerre wants to merge 1 commit intodarkweak:masterfrom
cdaguerre:fix/singleflight-body-duplication-tests
Open

test: add tests reproducing singleflight body duplication and buffer pool races#703
cdaguerre wants to merge 1 commit intodarkweak:masterfrom
cdaguerre:fix/singleflight-body-duplication-tests

Conversation

@cdaguerre
Copy link
Contributor

Problem

When a client cancels a request that is being coalesced via singleflight, subsequent responses can contain duplicated or corrupted bodies. This manifests as responses containing two HTTP bodies concatenated together.

Root Cause

Three related bugs in pkg/middleware/middleware.go:

  1. Missing Buf.Reset() before writing singleflight result — After singleflight.Do() returns, the post-Do code writes sfWriter.body back into customWriter.Buf without resetting it first. Since next() already wrote the body during the Do callback, the body gets appended a second time. (This was partially fixed in the Traefik override in f1195d5 but never propagated to core.)

  2. bytes.Buffer.Bytes() slice alias corruption — The singleflight value captures customWriter.Buf.Bytes(), which returns a slice pointing to the buffer's internal array, not a copy. When the buffer is returned to sync.Pool and reused, the shared slice contents are corrupted for coalesced requests still reading it.

  3. errorCacheCh data race on cancellationServeHTTP creates an unbuffered channel and defers close(). When the client cancels, the deferred close fires while the orphaned goroutine running Upstream later tries to send on the closed channel, causing a race (and potential panic caught by recover).

This PR

Adds 5 tests that reproduce these bugs:

Test Bug -race
TestSingleflightWinnerWritesBodyTwice Double-write (missing Reset) PASS (body assertion catches it)
TestBufferPoolReuseAfterCancellation Channel race on cancellation FAIL (data race)
TestCoalescedRequestGetsCleanBody Concurrent buffer write FAIL (data race)
TestCancelledRequestDoesNotCorruptCoalescedResponse Channel race + buffer race FAIL (data race)
TestBufferBytesSliceCorruption Slice alias corruption PASS (deterministic unit test)

Run with go test -race -count=1 -v ./pkg/middleware/ to see the races.

Related

  • f1195d5 — partial fix applied only to `plugins/traefik/override/middleware/middleware.go

…pool races

Add tests to pkg/middleware that reproduce three related bugs in the
request coalescing (singleflight) and buffer pool lifecycle:

1. TestSingleflightWinnerWritesBodyTwice: The singleflight winner gets
   its body written into the CustomWriter buffer by next() inside the
   callback, then the post-Do code unconditionally writes sfWriter.body
   back into the same buffer. The buffer reset via defer and slice alias
   behavior currently masks this for solo requests, but the pattern is
   unsafe.

2. TestBufferPoolReuseAfterCancellation: When a client cancels a request,
   ServeHTTP returns and defer s.bufPool.Put(bufPool) returns the buffer
   to the pool while the orphaned upstream goroutine is still writing to
   it. A subsequent request can pick up the same buffer. Run with -race
   to detect the data race on errorCacheCh (defer close races with the
   goroutine sending on it).

3. TestCoalescedRequestGetsCleanBody: Two concurrent requests coalesced
   via singleflight both call Upstream which writes sfWriter.body into
   customWriter.Buf. Run with -race to detect the concurrent
   bytes.Buffer.Write race.

4. TestCancelledRequestDoesNotCorruptCoalescedResponse: Request A and B
   are coalesced, A is cancelled mid-flight. Run with -race to detect the
   errorCacheCh race between the cancelled request's deferred close and
   the orphaned goroutine.

5. TestBufferBytesSliceCorruption: Unit test demonstrating that
   bytes.Buffer.Bytes() returns a slice alias (not a copy) that gets
   corrupted when the buffer is returned to sync.Pool and reused. This is
   the mechanism underlying the singleflight value body corruption.

These tests pass without -race (the races are timing-dependent) but
reliably fail with -race, confirming the bugs exist in production code.
@netlify
Copy link

netlify bot commented Feb 6, 2026

Deploy Preview for teal-sprinkles-4c7f14 canceled.

Name Link
🔨 Latest commit 408ce2e
🔍 Latest deploy log https://app.netlify.com/projects/teal-sprinkles-4c7f14/deploys/6985f4be35d51f0008d90f5d

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant