Summary
ssestream.RegisterDecoder mutates the package-level decoder registry while ssestream.NewDecoder reads the same map without synchronization. Because both functions are public and no init-only restriction is documented, registering a decoder while requests are opening streams can race and can terminate the process with fatal error: concurrent map writes.
Reproduction
On current main at 66688d6990b364431e7698d9466180bd9db3dc93, run concurrent RegisterDecoder calls alongside NewDecoder calls under the race detector:
var wg sync.WaitGroup
for i := 0; i < 4; i++ {
wg.Add(1)
go func(worker int) {
defer wg.Done()
for j := 0; j < 500; j++ {
ssestream.RegisterDecoder(
fmt.Sprintf("application/x-race-%d-%d", worker, j),
func(io.ReadCloser) ssestream.Decoder { return nil },
)
}
}(i)
}
for i := 0; i < 4; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 500; j++ {
decoder := ssestream.NewDecoder(&http.Response{
Header: http.Header{"Content-Type": []string{"text/event-stream"}},
Body: io.NopCloser(strings.NewReader("")),
})
_ = decoder.Close()
}
}()
}
wg.Wait()
go test -race reports reads in NewDecoder racing with writes in RegisterDecoder; the reproduction can also terminate with fatal error: concurrent map writes.
Expected behavior
The public decoder registry should be safe to read while another goroutine registers a decoder. A small read/write lock around registry access is sufficient; decoder execution itself should remain outside the lock.
Summary
ssestream.RegisterDecodermutates the package-level decoder registry whilessestream.NewDecoderreads the same map without synchronization. Because both functions are public and no init-only restriction is documented, registering a decoder while requests are opening streams can race and can terminate the process withfatal error: concurrent map writes.Reproduction
On current
mainat66688d6990b364431e7698d9466180bd9db3dc93, run concurrentRegisterDecodercalls alongsideNewDecodercalls under the race detector:go test -racereports reads inNewDecoderracing with writes inRegisterDecoder; the reproduction can also terminate withfatal error: concurrent map writes.Expected behavior
The public decoder registry should be safe to read while another goroutine registers a decoder. A small read/write lock around registry access is sufficient; decoder execution itself should remain outside the lock.