siyuan-note/siyuan · error
WebSocket is closed
Error message
WebSocket is closed
What it means
The WebSocket has already fully closed (ready state = CLOSED) when open() was called on it. A closed goja-backed socket instance cannot be reopened; the binding rejects immediately rather than queueing an open promise that could never resolve.
Solutions
- Create a new socket via client.socket.connect(path) and open that instead of reopening the closed instance.
- Track the onclose event and rebuild the socket in that handler.
- Before open(), check readyState === 3 and replace the socket object.
Example fix
// before
if (socket.readyState === 3) { socket.open(); } // rejects
// after
if (socket.readyState === 3) {
socket = client.socket.connect('/ws');
socket.open();
} Defensive patterns
Strategy: type-guard
Validate before calling
if (socket.readyState === 3) { socket = client.socket.connect(path); } Type guard
const isOpenable = (s) => s.readyState !== 2 && s.readyState !== 3; // or: recreate if CLOSED
Try / catch
try {
socket.open();
} catch (e) {
if (String(e).includes('WebSocket is closed')) {
socket = client.socket.connect(path);
socket.open();
}
} Prevention
- Treat socket instances as single-use: recreate after close
- Listen for onclose and rebuild the socket there
- Clear reconnect timers that reference stale socket objects
When it happens
Trigger: Calling socket.open() on a socket whose connection already terminated — after onclose fired, after the kernel shut down, or after an explicit close() completed.
Common situations: Reconnect timers firing on a stale socket object; plugin suspends/resumes where the socket closed in the background; kernel restart killing all connections.
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
- WebSocket is closing
- WebSocket is not open
- WebSocket is not open
- agent runtime turn is not finalized
- capability handler unavailable
AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19).
Data as JSON: /api/errors/cc2a9dcfed776e5a.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/plugin/api_client.go:338
})
}
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()
event.Set("type", rt.ToValue("error"))
event.Set("error", rt.NewGoError(dialErr))
invokeHook(rt, "onerror", event)View on GitHub (pinned to 9f775e8a12)