> ## Content Index
> Fetch the complete content index at: https://aresa.me/llms.txt
> Use this file to discover other available public pages before exploring further.

# An SSE Test Exposed a Real ResponseWriter Data Race
- URL: https://aresa.me/signalbin-sse-responsewriter-race/
- Published: 2026-08-26T20:39:49.000Z
- Updated: 2026-08-26T20:39:49.000Z
- Description: Signalbin's SSE handler was fine, but its test read an httptest.ResponseRecorder while another goroutine wrote to it. A synchronized wrapper fixed the test contract.
- Author: Alex Oleshkevich
- Tags: Dev Log, Signalbin

Signalbin's race detector found concurrent access to an SSE response body. The race was real, but it lived in the test harness: one goroutine ran the streaming handler while the test goroutine polled `httptest.ResponseRecorder.Body`.

The fix in commit `61feaa5` did not add a lock to production. It made the test's fake `ResponseWriter` safe for the concurrency the test itself introduced.

## Streaming breaks the usual recorder pattern

Most HTTP handler tests call `ServeHTTP` synchronously, then inspect the recorder after the handler returns. An SSE handler is designed to stay open, so this test started it in a goroutine, waited until the `ready` event appeared, canceled the request context, and then waited for shutdown.

The waiting loop repeatedly called `response.Body.String()`. At the same time, the handler could call `Write` and `Flush`. `httptest.ResponseRecorder` embeds a `bytes.Buffer`, and that buffer is not safe for concurrent reading and writing.

Go's HTTP contract also makes the lifecycle boundary explicit: a [0 may not be used after 1 returns](https://pkg.go.dev/net/http?ref=aresa.me#ResponseWriter). Here the accesses happened before return, but from two goroutines with no synchronization. The test had created a concurrent observer that a real HTTP client would implement through a socket, not by reading the server's buffer directly.

## The wrapper had to synchronize both sides

The replacement `syncRecorder` holds a mutex around `Write`, `WriteHeader`, `Flush`, body reads, and status reads. It still delegates behavior to `httptest.ResponseRecorder`, so the test preserves its useful semantics while adding the missing happens-before relationship.

It is not enough to lock only the polling method. Synchronization works when the writer and reader coordinate on the same lock. Wrapping reads while leaving `Write` unguarded would make the code look defensive without removing the race.

The handler also checks for `http.Flusher`, because streaming depends on the concrete writer supporting it. The standard HTTP/1.x and HTTP/2 writers do, while arbitrary wrappers may not. A response wrapper that hides `Flush` can therefore break SSE even when its `Write` method works.

## The test harness was the component to fix

It would have been easy to dismiss this as a testing artifact. Instead, the race clarified the contract of the test double and kept synchronization out of the production hot path.

When a handler test runs `ServeHTTP` concurrently, `ResponseRecorder` is no longer a passive value inspected after completion. It is shared mutable state. Either observe the stream through a real HTTP connection, or wrap every concurrent access with one synchronization mechanism.