siyuan-note/siyuan · error
WebSocket is closing
Error message
WebSocket is closing
What it means
The WebSocket close was already initiated (ready state = CLOSING) when open() was called, so the binding rejects with 'WebSocket is closing' instead of registering a new open promise. A socket in CLOSING state will never transition to OPEN, so waiting would be pointless.
Solutions
- Check socket.readyState before calling open(); skip or wait when it is CLOSING.
- Wait for the close event (or onclose callback) to complete, then create a fresh socket and open it.
- Re-run the open on a new socket instance rather than reusing the closing one.
Example fix
// before
socket.open();
// after
if (socket.readyState === 0 || socket.readyState === 1) {
// already connecting/open, do nothing
} else if (socket.readyState === 3) {
socket = client.socket.connect('/ws');
socket.open();
} // CLOSING (2): wait for close event before reopening Defensive patterns
Strategy: type-guard
Validate before calling
if (socket.readyState === 2) { /* CLOSING: defer reopen until onclose */ } Type guard
const canOpen = (s) => s.readyState === 3; // only CLOSED sockets may be replaced/reopened; CLOSING must wait
Try / catch
try {
socket.open();
} catch (e) {
if (String(e).includes('WebSocket is closing')) {
socket.onclose = () => { socket = client.socket.connect(path); socket.open(); };
}
} Prevention
- Check readyState before every open call
- Drive reconnection from the onclose event, not from timers alone
- Avoid calling open() from teardown/close handlers
When it happens
Trigger: Calling socket.open() while a close() is in progress — e.g. an open retry racing with plugin shutdown, or calling open() from a close handler / during runtime teardown.
Common situations: Reconnect logic that calls open() without checking readyState after an error; asynchronous callbacks firing during window unload; plugin reload where the old socket is still closing.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19).
Data as JSON: /api/errors/a560f2bff87ee30f.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/plugin/api_client.go:335
doClose := func() {
closeOnce.Do(func() {
cancel()
})
}
ws_open := rt.ToValue(func(openCall goja.FunctionCall, rt *goja.Runtime) goja.Value {
openPromise, openResolve, openReject := rt.NewPromise()
openRunErr := p.worker.Run(func(rt *goja.Runtime) (_ any, err error) {
state := WebSocketState(readyState.Load())
switch state {
case WebSocketReadyStateOpen:
if resolveErr := openResolve(nil); resolveErr != nil {
logging.LogErrorf("[plugin:%s] siyuan.client.socket.open resolve: %v", p.Name, resolveErr)
}
return
case WebSocketReadyStateClosing:
err = fmt.Errorf("WebSocket is closing")
return
case WebSocketReadyStateClosed:
err = fmt.Errorf("WebSocket is closed")
return
}
addOpenPromise(openResolve, openReject)
openOnce.Do(func() {
go func() {
conn, _, dialErr := gws.NewClient(h, &gws.ClientOption{
Addr: wsURL,
RequestHeader: wsHeader,
})
if dialErr != nil {
p.worker.Run(func(rt *goja.Runtime) (_ any, _ error) {
setReadyState(rt, WebSocketReadyStateClosed)
event := rt.NewObject()View on GitHub (pinned to 9f775e8a12)