From 4e5fb0701a3299c13ec7657621261d9940d9b6e3 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Fri, 13 Mar 2026 13:23:06 +0000 Subject: [PATCH] Fix panic on send to closed event channel in Go SDK Protect dispatchEvent with a recover guard so that a notification arriving after Disconnect does not crash the process. Also wrap the channel close in sync.Once so Disconnect is safe to call more than once. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- go/session.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/go/session.go b/go/session.go index 68e881c72..e472e35ac 100644 --- a/go/session.go +++ b/go/session.go @@ -67,7 +67,8 @@ type Session struct { // eventCh serializes user event handler dispatch. dispatchEvent enqueues; // a single goroutine (processEvents) dequeues and invokes handlers in FIFO order. - eventCh chan SessionEvent + eventCh chan SessionEvent + closeOnce sync.Once // guards eventCh close so Disconnect is safe to call more than once // RPC provides typed session-scoped RPC methods. RPC *rpc.SessionRpc @@ -451,7 +452,15 @@ func (s *Session) handleHooksInvoke(hookType string, rawInput json.RawMessage) ( // serial, FIFO dispatch without blocking the read loop. func (s *Session) dispatchEvent(event SessionEvent) { go s.handleBroadcastEvent(event) - s.eventCh <- event + + // Send to the event channel in a closure with a recover guard. + // Disconnect closes eventCh, and in Go sending on a closed channel + // panics — there is no non-panicking send primitive. We only want + // to suppress that specific panic; other panics are not expected here. + func() { + defer func() { recover() }() + s.eventCh <- event + }() } // processEvents is the single consumer goroutine for the event channel. @@ -657,7 +666,7 @@ func (s *Session) Disconnect() error { return fmt.Errorf("failed to disconnect session: %w", err) } - close(s.eventCh) + s.closeOnce.Do(func() { close(s.eventCh) }) // Clear handlers s.handlerMutex.Lock()