{"record":{"id":"a560f2bff87ee30f","repo":"siyuan-note/siyuan","slug":"websocket-is-closing","errorCode":null,"errorMessage":"WebSocket is closing","messagePattern":"WebSocket is closing","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"kernel/plugin/api_client.go","lineNumber":335,"sourceCode":"\t\t\tdoClose := func() {\n\t\t\t\tcloseOnce.Do(func() {\n\t\t\t\t\tcancel()\n\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()","sourceCodeStart":317,"sourceCodeEnd":353,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/plugin/api_client.go#L317-L353","documentation":"Returned by socket.open() when the WebSocket readyState is CLOSING (2). open() refuses to start or resolve because the connection is already shutting down; this rejects the open() promise.","triggerScenarios":"Calling open() after close() has transitioned readyState to CLOSING but before it reaches CLOSED. Happens in close-then-reconnect logic that races, or calling open() twice where the first triggered a close.","commonSituations":"Reconnect handlers that call open() from onclose without checking state, or a close initiated elsewhere (user navigates away) while open() is invoked.","solutions":["Check ws.readyState before calling open(); skip if it is CLOSING(2) or CLOSED(3).","Create a fresh socket() (new connection object) for reconnects rather than calling open() on a closing one.","Drive reconnects from onclose, gating on readyState === CLOSED."],"exampleFix":"// before\nws.close();\nawait ws.open(); // rejects: WebSocket is closing\n// after\nws.close();\n// for a new connection, create a new socket instead of reopening\nconst ws2 = await siyuan.client.socket('/ws/foo');\nawait ws2.open();","handlingStrategy":"validation","validationCode":"if (ws.readyState === 2 || ws.readyState === 3) {\n  // create a new socket instead of reopening\n} else {\n  await ws.open();\n}","typeGuard":"const isReusable = (ws) => ws.readyState === 0 || ws.readyState === 1;","tryCatchPattern":"try { await ws.open(); }\ncatch (e) { if (/WebSocket is closing/.test(String(e))) { /* reconnect via new socket() */ } }","preventionTips":["Treat a socket as single-use; reconnect by creating a new socket().","Gate open() on readyState.","Drive reconnect from onclose."],"tags":["client-socket","websocket","lifecycle","readystate"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}