From be99b3cb74cb7886b8d836a441d037c0f5363cc4 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Sun, 29 Mar 2026 20:11:30 -0400 Subject: [PATCH] fix: prioritize context cancellation in WebSocket sendEvent (#23756) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem Commit 386b449 (PR #23745) changed the `OneWayWebSocketEventSender` event channel from unbuffered to buffered(64) to reduce chat streaming latency. This introduced a nondeterministic race in `sendEvent`: ```go sendEvent := func(event codersdk.ServerSentEvent) error { select { case eventC <- event: // buffered channel — almost always ready case <-ctx.Done(): // also ready after cancellation } return nil } ``` After context cancellation, Go's `select` randomly picks between two ready cases, so `send()` sometimes returns `nil` instead of `ctx.Err()`. With the old unbuffered channel the send case was rarely ready (no reader), masking the bug. ## Fix Add a priority `select` that checks `ctx.Done()` before attempting the channel send: ```go select { case <-ctx.Done(): return ctx.Err() default: } select { case eventC <- event: case <-ctx.Done(): return ctx.Err() } ``` This is the standard Go pattern for prioritizing one channel over another. When the context is already cancelled, the first select returns immediately. The second select still handles the case where cancellation happens concurrently with the send. ## Verification - Ran the flaky test 20× in a loop (`-count=20`): all passed - Ran the full `TestOneWayWebSocketEventSender` suite 5× (`-count=5`): all passed - Ran the complete `coderd/httpapi` test package: all passed Fixes coder/internal#1429 --- coderd/httpapi/httpapi.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/coderd/httpapi/httpapi.go b/coderd/httpapi/httpapi.go index b524b6223c..0b11a1ef0d 100644 --- a/coderd/httpapi/httpapi.go +++ b/coderd/httpapi/httpapi.go @@ -488,6 +488,16 @@ func OneWayWebSocketEventSender(log slog.Logger) func(rw http.ResponseWriter, r }() sendEvent := func(event codersdk.ServerSentEvent) error { + // Prioritize context cancellation over sending to the + // buffered channel. Without this check, both cases in + // the select below can fire simultaneously when the + // context is already done and the channel has capacity, + // making the result nondeterministic. + select { + case <-ctx.Done(): + return ctx.Err() + default: + } select { case eventC <- event: case <-ctx.Done():