rpc: send WebSocket close frame on client disconnect (#33909)
Some checks are pending
/ Linux Build (push) Waiting to run
/ Linux Build (arm) (push) Waiting to run
/ Keeper Build (push) Waiting to run
/ Windows Build (push) Waiting to run
/ Docker Image (push) Waiting to run

When `rpc.Client.Close()` is called, the TCP connection is torn down
without sending a WebSocket Close frame. The server sees `websocket:
close 1006 (abnormal closure): unexpected EOF` instead of a clean 1000
(normal closure).

### Root cause

`websocketCodec.close()` delegates to `jsonCodec.close()` which calls
`c.conn.Close()` — gorilla/websocket's `Conn.Close` explicitly "[closes
the underlying network connection without sending or waiting for a close
message](https://pkg.go.dev/github.com/gorilla/websocket#Conn.Close)"
(per RFC 6455).

### Fix

Send a WebSocket Close control frame (opcode 0x8, status 1000) before
closing the underlying connection. Uses `WriteControl` with the same
`encMu` mutex pattern already used by `pingLoop` for write
serialization, and reuses the existing `wsPingWriteTimeout` (5s)
constant.

`WriteControl` errors are safe to ignore — the connection may already be
broken by the time we attempt the close frame.

Fixes #30482
This commit is contained in:
YQ 2026-04-24 17:27:39 +08:00 committed by GitHub
parent 6ece4cd143
commit 33c1bd59ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -324,6 +324,16 @@ func newWebsocketCodec(conn *websocket.Conn, host string, req http.Header, readL
}
func (wc *websocketCodec) close() {
// Send a WebSocket Close frame before closing the underlying connection,
// so the server sees a clean 1000 (normal closure) instead of 1006 (abnormal).
wc.jsonCodec.encMu.Lock()
wc.conn.WriteControl(
websocket.CloseMessage,
websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""),
time.Now().Add(wsPingWriteTimeout),
)
wc.jsonCodec.encMu.Unlock()
wc.jsonCodec.close()
wc.wg.Wait()
}