siyuan-note/siyuan · error

WebSocket is not open

Error message

WebSocket is not open (state: %d)

What it means

send() was invoked while the socket ready state is CLOSING (2) or CLOSED (3). Data can only be written on an OPEN (1) connection, so the binding rejects with the numeric state for diagnosis. This is a WebSocket lifecycle guard, not a transport failure.

Solutions

  1. Check socket.readyState === 1 before sending; buffer or drop messages otherwise.
  2. Recreate the socket (connect + open) after it reaches CLOSED, then resend.
  3. Route sends through onopen so messages only go out on a live connection.

Example fix

// before
socket.send(JSON.stringify(msg));
// after
if (socket.readyState === 1) {
  socket.send(JSON.stringify(msg));
} else {
  pendingQueue.push(msg); // flush on next onopen
}
Defensive patterns

Strategy: type-guard

Validate before calling

function canSend(s) { return s.readyState === 1; }
if (!canSend(socket)) queueMessage(msg);

Type guard

const isOpen = (s) => s.readyState === 1;

Try / catch

try {
  socket.send(data);
} catch (e) {
  if (String(e).includes('WebSocket is not open')) {
    pendingQueue.push(data); // flush on next onopen
  }
}

Prevention

When it happens

Trigger: Calling socket.send(message) after close() was called, after onclose fired, or concurrently with a close in progress; also on a socket that never finished opening.

Common situations: Queued send callbacks executing after teardown; UI event handlers sending on a socket closed by kernel restart; missing readyState checks in send wrappers.

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/eea9d93a83cb6775. Report an issue: GitHub.

Appendix: source

Thrown at kernel/plugin/api_client.go:426

				var messageData []byte
				var opcode gws.Opcode
				if data := sendCall.Argument(0); isJsValueNotNull(data) {
					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())
					}
				}

				sendRunErr := p.worker.Run(func(rt *goja.Runtime) (_ any, err error) {
					state := WebSocketState(readyState.Load())
					if state == WebSocketReadyStateClosing || state == WebSocketReadyStateClosed {
						err = fmt.Errorf("WebSocket is not open (state: %d)", state)
						return
					}

					c := gwsConn.Load()
					if c == nil {
						err = fmt.Errorf("WebSocket not yet connected")
						return
					}

					updateBufferedAmount(rt, len(messageData))
					c.WriteAsync(opcode, messageData, func(writeErr error) {
						p.worker.Run(func(rt *goja.Runtime) (_ any, err error) {
							if writeErr == nil {
								updateBufferedAmount(rt, -len(messageData))
							} else {
								err = writeErr
							}
							return

View on GitHub (pinned to 9f775e8a12)