@@ -233,6 +233,10 @@ func sendErrorResponse(ws *websocket.Conn, errInfo errorInfo, ip string) error {
233233 return err
234234 }
235235
236+ // Allow time for client to receive error before connection closes.
237+ // Without this delay, TCP close can race with message delivery, causing clients to see EOF.
238+ time .Sleep (100 * time .Millisecond )
239+
236240 return nil
237241}
238242
@@ -418,20 +422,20 @@ func (h *WebSocketHandler) validateAuth(ctx context.Context, ws *websocket.Conn,
418422}
419423
420424// closeWebSocket gracefully closes a WebSocket connection with cleanup.
421- func closeWebSocket (ws * websocket.Conn ) {
425+ // If client is provided, shutdown message is sent via control channel to avoid race.
426+ func closeWebSocket (ws * websocket.Conn , client * Client ) {
422427 clientIP := security .ClientIP (ws .Request ())
423428 log .Printf ("WebSocket Handle() cleanup - closing connection for IP %s" , clientIP )
424429
425- // Send a final shutdown message to allow graceful client disconnect
426- shutdownMsg := map [string ]string {"type" : "server_closing" , "code" : "1001" }
427- if err := ws .SetWriteDeadline (time .Now ().Add (200 * time .Millisecond )); err != nil {
428- log .Printf ("failed to set write deadline for shutdown message: %v" , err )
429- }
430- if err := websocket .JSON .Send (ws , shutdownMsg ); err != nil {
431- // Expected during abrupt disconnection - don't log common cases
432- if ! strings .Contains (err .Error (), "use of closed network connection" ) &&
433- ! strings .Contains (err .Error (), "broken pipe" ) {
434- log .Printf ("failed to send shutdown message: %v" , err )
430+ // Send shutdown message via control channel if client exists
431+ if client != nil {
432+ shutdownMsg := map [string ]any {"type" : "server_closing" , "code" : "1001" }
433+ select {
434+ case client .control <- shutdownMsg :
435+ // Give brief time for shutdown message to be sent
436+ time .Sleep (100 * time .Millisecond )
437+ case <- time .After (200 * time .Millisecond ):
438+ log .Printf ("Timeout sending shutdown message to client %s" , client .ID )
435439 }
436440 }
437441
@@ -460,8 +464,11 @@ func (h *WebSocketHandler) Handle(ws *websocket.Conn) {
460464 ctx , cancel := context .WithCancel (ws .Request ().Context ())
461465 defer cancel ()
462466
463- // Ensure WebSocket is properly closed
464- defer closeWebSocket (ws )
467+ // Ensure WebSocket is properly closed (client will be set later if connection succeeds)
468+ var client * Client
469+ defer func () {
470+ closeWebSocket (ws , client )
471+ }()
465472
466473 // Get client IP
467474 ip := security .ClientIP (ws .Request ())
@@ -645,7 +652,7 @@ func (h *WebSocketHandler) Handle(ws *websocket.Conn) {
645652 }
646653 id [i ] = charset [n .Int64 ()]
647654 }
648- client : = NewClient (
655+ client = NewClient (
649656 string (id ),
650657 sub ,
651658 ws ,
@@ -765,17 +772,17 @@ func (h *WebSocketHandler) Handle(ws *websocket.Conn) {
765772 // (which happens for ANY message including pong) keeps the connection alive
766773 continue
767774 case "ping" :
768- // Client sent us a ping, send pong back
775+ // Client sent us a ping, send pong back via control channel to avoid race
769776 pong := map [string ]any {"type" : "pong" }
770777 if seq , ok := msgMap ["seq" ]; ok {
771778 pong ["seq" ] = seq
772779 }
773- if err := ws . SetWriteDeadline ( time . Now (). Add ( writeTimeout )); err != nil {
774- log . Printf ( "failed to set write deadline for pong to client %s: %v" , client . ID , err )
775- continue
776- }
777- if err := websocket . JSON . Send ( ws , pong ); err != nil {
778- log .Printf ("failed to send pong to client %s: %v " , client .ID , err )
780+ // Non-blocking send to avoid deadlock if control channel is full
781+ select {
782+ case client . control <- pong :
783+ // Pong queued successfully
784+ default :
785+ log .Printf ("WARNING: client %s control channel full, dropping pong " , client .ID )
779786 }
780787 continue
781788 case "keepalive" , "heartbeat" :
0 commit comments