{"record":{"id":"deb7a70358425a2a","repo":"siyuan-note/siyuan","slug":"websocket-is-not-open-state-d-deb7a7","errorCode":null,"errorMessage":"WebSocket is not open (state: %d)","messagePattern":"WebSocket is not open \\(state: (.+?)\\)","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/plugin/plugin.go","lineNumber":1145,"sourceCode":"\t\t\tsendRunErr := p.worker.Run(func(rt *goja.Runtime) (_ any, err error) {\n\t\t\t\tvar messageData []byte\n\t\t\t\tvar opcode gws.Opcode\n\t\t\t\tif len(sendCall.Arguments) >= 1 {\n\t\t\t\t\tdata := sendCall.Argument(0)\n\t\t\t\t\tif arrayBuffer, ok := data.Export().(goja.ArrayBuffer); ok {\n\t\t\t\t\t\topcode = gws.OpcodeBinary\n\t\t\t\t\t\tb := arrayBuffer.Bytes()\n\t\t\t\t\t\tmessageData = make([]byte, len(b))\n\t\t\t\t\t\tcopy(messageData, b) // ArrayBuffer.Bytes() points into JS engine memory; copy before async send\n\t\t\t\t\t} else {\n\t\t\t\t\t\topcode = gws.OpcodeText\n\t\t\t\t\t\tmessageData = []byte(data.String())\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tstate := WebSocketState(readyState.Load())\n\t\t\t\tif state == WebSocketReadyStateClosing || state == WebSocketReadyStateClosed {\n\t\t\t\t\terr = fmt.Errorf(\"WebSocket is not open (state: %d)\", state)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tupdatePortBufferedAmount(rt, len(messageData))\n\t\t\t\tsocket.WriteAsync(opcode, messageData, func(writeErr error) {\n\t\t\t\t\tp.worker.Run(func(rt *goja.Runtime) (_ any, err error) {\n\t\t\t\t\t\tif writeErr == nil {\n\t\t\t\t\t\t\tupdatePortBufferedAmount(rt, -len(messageData))\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\terr = writeErr\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn\n\t\t\t\t\t}, func(rt *goja.Runtime, result any, err error) {\n\t\t\t\t\t\tif lo.IsNil(err) {\n\t\t\t\t\t\t\tif resolveErr := sendResolve(result); resolveErr != nil {\n\t\t\t\t\t\t\t\tlogging.LogErrorf(\"[plugin:%s] ws server port.send resolve: %v\", p.Name, resolveErr)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {","sourceCodeStart":1127,"sourceCodeEnd":1163,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/plugin/plugin.go#L1127-L1163","documentation":"The plugin WebSocket port's send path checks the tracked readyState before writing. If the port is already CLOSING (1) or CLOSED (2) — i.e. the underlying connection has been upgraded away, closed by the client, or closed by the server — any send attempt returns 'WebSocket is not open (state: %d)' instead of writing. It is the kernel-side equivalent of the DOM 'InvalidStateError: connection is closing' behavior.","triggerScenarios":"Calling port.send (or writing from port.onmessage/onopen handlers) after port.close() was called, after the client disconnected, or after the server tore down the connection; sending from a stale async callback that fires after close.","commonSituations":"Plugin keeps a reference to port and sends heartbeat/progress messages after the client navigated away; async work (setTimeout/promise) completes after the WS client closed; race between onclose firing and an in-flight send loop.","solutions":["Check port.readyState === 1 (OPEN) before every send, including inside async callbacks","Track close lifecycle: set a local flag in port.onclose and stop all send loops when it fires","Wrap sends in try/catch and abort the associated async work (clear timers, cancel promises) on failure","For long-lived streams, ping/verify the connection or re-establish it instead of blind sends"],"exampleFix":"// before\nsetInterval(() => port.send(JSON.stringify(progress)), 1000);\n// after\nconst timer = setInterval(() => {\n  if (port.readyState !== 1) { clearInterval(timer); return; }\n  try { port.send(JSON.stringify(progress)); } catch (e) { clearInterval(timer); }\n}, 1000);","handlingStrategy":"try-catch","validationCode":"function canSend(port) { return port && port.readyState === 1; }","typeGuard":"const isWsOpen = (port) => port && typeof port.send === \"function\" && port.readyState === 1;","tryCatchPattern":"try { port.send(msg); } catch (e) { if (String(e).includes(\"WebSocket is not open\")) { stopSendLoop(); scheduleReconnect(); } else { throw e; } }","preventionTips":["Check readyState before every send, especially in async callbacks","Stop timers/promises that send on the port when onclose fires","Treat the port as one-shot: no sends after close for any reason","Add a reconnect path instead of reusing a closed port object"],"tags":["websocket","plugin-api","lifecycle","race-condition"],"backgroundTag":"websocket-not-open","analyzedSha":"9f775e8a12daef8255556097396f9b2739078892","analyzedAt":"2026-09-19T03:17:15.984Z","contentChangedAt":"2026-09-19T03:17:15.984Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}