{"record":{"id":"cc2a9dcfed776e5a","repo":"siyuan-note/siyuan","slug":"websocket-is-closed","errorCode":null,"errorMessage":"WebSocket is closed","messagePattern":"WebSocket is closed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/plugin/api_client.go","lineNumber":338,"sourceCode":"\t\t\t\t})\n\t\t\t}\n\n\t\t\tws_open := rt.ToValue(func(openCall goja.FunctionCall, rt *goja.Runtime) goja.Value {\n\t\t\t\topenPromise, openResolve, openReject := rt.NewPromise()\n\n\t\t\t\topenRunErr := p.worker.Run(func(rt *goja.Runtime) (_ any, err error) {\n\t\t\t\t\tstate := WebSocketState(readyState.Load())\n\t\t\t\t\tswitch state {\n\t\t\t\t\tcase WebSocketReadyStateOpen:\n\t\t\t\t\t\tif resolveErr := openResolve(nil); resolveErr != nil {\n\t\t\t\t\t\t\tlogging.LogErrorf(\"[plugin:%s] siyuan.client.socket.open resolve: %v\", p.Name, resolveErr)\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn\n\t\t\t\t\tcase WebSocketReadyStateClosing:\n\t\t\t\t\t\terr = fmt.Errorf(\"WebSocket is closing\")\n\t\t\t\t\t\treturn\n\t\t\t\t\tcase WebSocketReadyStateClosed:\n\t\t\t\t\t\terr = fmt.Errorf(\"WebSocket is closed\")\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\t\taddOpenPromise(openResolve, openReject)\n\t\t\t\t\topenOnce.Do(func() {\n\t\t\t\t\t\tgo func() {\n\t\t\t\t\t\t\tconn, _, dialErr := gws.NewClient(h, &gws.ClientOption{\n\t\t\t\t\t\t\t\tAddr:          wsURL,\n\t\t\t\t\t\t\t\tRequestHeader: wsHeader,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\tif dialErr != nil {\n\t\t\t\t\t\t\t\tp.worker.Run(func(rt *goja.Runtime) (_ any, _ error) {\n\t\t\t\t\t\t\t\t\tsetReadyState(rt, WebSocketReadyStateClosed)\n\n\t\t\t\t\t\t\t\t\tevent := rt.NewObject()\n\t\t\t\t\t\t\t\t\tevent.Set(\"type\", rt.ToValue(\"error\"))\n\t\t\t\t\t\t\t\t\tevent.Set(\"error\", rt.NewGoError(dialErr))\n\t\t\t\t\t\t\t\t\tinvokeHook(rt, \"onerror\", event)","sourceCodeStart":320,"sourceCodeEnd":356,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/plugin/api_client.go#L320-L356","documentation":"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.","triggerScenarios":"Calling socket.open() on a socket whose connection already terminated — after onclose fired, after the kernel shut down, or after an explicit close() completed.","commonSituations":"Reconnect timers firing on a stale socket object; plugin suspends/resumes where the socket closed in the background; kernel restart killing all connections.","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."],"exampleFix":"// before\nif (socket.readyState === 3) { socket.open(); } // rejects\n// after\nif (socket.readyState === 3) {\n  socket = client.socket.connect('/ws');\n  socket.open();\n}","handlingStrategy":"type-guard","validationCode":"if (socket.readyState === 3) { socket = client.socket.connect(path); }","typeGuard":"const isOpenable = (s) => s.readyState !== 2 && s.readyState !== 3; // or: recreate if CLOSED","tryCatchPattern":"try {\n  socket.open();\n} catch (e) {\n  if (String(e).includes('WebSocket is closed')) {\n    socket = client.socket.connect(path);\n    socket.open();\n  }\n}","preventionTips":["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"],"tags":["websocket","lifecycle","reconnect"],"backgroundTag":"invalid-state-transition","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"}