siyuan-note/siyuan · warning

WebSocket is not open (state: %d)

Error message

WebSocket is not open (state: %d)

What it means

Returned by the WebSocket port.send() path when the socket readyState is CLOSING (2) or CLOSED (3) at send time. The kernel refuses to enqueue a WriteAsync in those states and rejects the send Promise with the current state number.

Source

Thrown at kernel/plugin/plugin.go:1144

			sendRunErr := p.worker.Run(func(rt *goja.Runtime) (_ any, err error) {
				var messageData []byte
				var opcode gws.Opcode
				if len(sendCall.Arguments) >= 1 {
					data := sendCall.Argument(0)
					if arrayBuffer, ok := data.Export().(goja.ArrayBuffer); ok {
						opcode = gws.OpcodeBinary
						b := arrayBuffer.Bytes()
						messageData = make([]byte, len(b))
						copy(messageData, b) // ArrayBuffer.Bytes() points into JS engine memory; copy before async send
					} else {
						opcode = gws.OpcodeText
						messageData = []byte(data.String())
					}
				}

				state := WebSocketState(readyState.Load())
				if state == WebSocketReadyStateClosing || state == WebSocketReadyStateClosed {
					err = fmt.Errorf("WebSocket is not open (state: %d)", state)
					return
				}

				updatePortBufferedAmount(rt, len(messageData))
				socket.WriteAsync(opcode, messageData, func(writeErr error) {
					p.worker.Run(func(rt *goja.Runtime) (_ any, err error) {
						if writeErr == nil {
							updatePortBufferedAmount(rt, -len(messageData))
						} else {
							err = writeErr
						}
						return
					}, func(rt *goja.Runtime, result any, err error) {
						if lo.IsNil(err) {
							if resolveErr := sendResolve(result); resolveErr != nil {
								logging.LogErrorf("[plugin:%s] ws server port.send resolve: %v", p.Name, resolveErr)
							}
						} else {

View on GitHub (pinned to 251596fc0d)

Solutions

  1. Check port.readyState === 1 (OPEN) before calling port.send.
  2. Stop send loops on onclose/onerror; drain queued sends before reopening.
  3. Catch the rejection and treat state 2/3 as a non-fatal 'connection gone' signal.

Example fix

// before
port.send(JSON.stringify(msg));

// after
if (port.readyState !== 1) return;
port.send(JSON.stringify(msg)).catch((e) => {
  if (/state: [23]/.test(String(e))) return; // already closing/closed
  throw e;
});
Defensive patterns

Strategy: validation

Validate before calling

const OPEN = 1; if (port.readyState !== OPEN) { /* skip or buffer */ return; }

Type guard

function isPortOpen(port: { readyState: number }): boolean { return port.readyState === 1; }

Try / catch

try { await port.send(payload); } catch (e) { if (/state: [23]/.test(String(e))) return; throw e; }

Prevention

When it happens

Trigger: Plugin calls port.send(...) after onclose fired, or a client disconnect/cancel races with the send so the state transitions to closing before the worker runs.

Common situations: Sending from a timer/loop without checking readyState, or sending in onclose/onerror handlers after the connection is already shutting down.

Related errors


AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12). Data as JSON: /api/errors/deb7a70358425a2a. Report an issue: GitHub.