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

  1. Create a new socket via client.socket.connect(path) and open that instead of reopening the closed instance.
  2. Track the onclose event and rebuild the socket in that handler.
  3. 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

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


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)