{"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":"warning","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/251596fc0de2f9528c00c224252fd073a99973f4/kernel/plugin/api_client.go#L320-L356","documentation":"Returned by socket.open() when readyState is CLOSED (3). The connection has finished closing; open() will not transition it back to open.","triggerScenarios":"Calling open() on a socket whose close handshake completed, or after the dial failed and readyState was set to CLOSED.","commonSituations":"Reconnect logic that calls open() on the original socket object instead of creating a new one; or calling open() after a failed dial.","solutions":["Create a new connection via siyuan.client.socket(path) and open() that, rather than reopening the closed one.","Check readyState === CLOSED and branch to recreate."],"exampleFix":"// before\n// ws is already closed\nawait ws.open(); // rejects: WebSocket is closed\n// after\nconst ws = await siyuan.client.socket('/ws/foo');\nawait ws.open();","handlingStrategy":"validation","validationCode":"if (ws.readyState === 3) {\n  ws = await siyuan.client.socket(path); // recreate\n}\nawait ws.open();","typeGuard":"const isClosed = (ws) => ws.readyState === 3;","tryCatchPattern":"try { await ws.open(); }\ncatch (e) { if (/WebSocket is closed/.test(String(e))) { ws = await siyuan.client.socket(path); await ws.open(); } }","preventionTips":["Never reopen a closed socket; recreate it.","Track readyState in your connection manager."],"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"}