{"record":{"id":"6e8e728759e013ba","repo":"siyuan-note/siyuan","slug":"invalid-plugin-websocket-status","errorCode":null,"errorMessage":"invalid plugin WebSocket status","messagePattern":"invalid plugin WebSocket status","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/apicontract/plugin_service_protocol.go","lineNumber":158,"sourceCode":"\t\t\tknown = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif !known {\n\t\treturn fmt.Errorf(\"unknown plugin service mode: %s\", mode)\n\t}\n\tswitch mode {\n\tcase PluginServiceAdmission:\n\t\tif status != 400 && status != 404 && status != 500 && status != 503 {\n\t\t\treturn fmt.Errorf(\"undeclared plugin admission status\")\n\t\t}\n\tcase PluginServiceRedirect:\n\t\tif status != 201 && (status < 300 || status > 308) {\n\t\t\treturn fmt.Errorf(\"invalid plugin redirect status\")\n\t\t}\n\tcase PluginServiceWebSocket:\n\t\tif status != 101 && status != 400 && status != 500 {\n\t\t\treturn fmt.Errorf(\"invalid plugin WebSocket status\")\n\t\t}\n\tcase PluginServiceSSE:\n\t\tif status != 200 && status != 500 {\n\t\t\treturn fmt.Errorf(\"invalid plugin SSE status\")\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (b *Bundle) validatePluginServiceHTTPResponse(endpoint EndpointSchema, status int, contentType string, payload []byte) error {\n\tif status < 100 || status > 999 {\n\t\treturn fmt.Errorf(\"invalid plugin service HTTP status\")\n\t}\n\tif endpoint.Method == \"HEAD\" || status < 200 || status == 204 || status == 304 {\n\t\tif len(payload) > 0 {\n\t\t\treturn fmt.Errorf(\"plugin service response forbids a body\")\n\t\t}\n\t\treturn nil","sourceCodeStart":140,"sourceCodeEnd":176,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/apicontract/plugin_service_protocol.go#L140-L176","documentation":"WebSocket-mode plugin service responses may only use status 101 (switching protocols, successful handshake), 400, or 500; validatePluginServiceStatus rejects other statuses. This keeps the contract's websocket variant aligned with the HTTP upgrade handshake semantics.","triggerScenarios":"Calling StreamPluginService(PluginServiceWebSocket, status, serve) or Bundle.ValidatePluginServiceResponse with websocket mode and a status other than 101, 400, or 500 (e.g. 200, 403, or 426).","commonSituations":"Trying to answer a websocket handshake with a plain 200 OK; rejecting an upgrade with 401/403 (use 400 instead per the contract); emitting 426 Upgrade Required instead of the contract's allowed set.","solutions":["Use 101 for a successful websocket upgrade, or 400/500 for failures","Replace non-handshake rejections (401/403/426) with the contract-permitted 400","Serve non-upgrade logic through a different endpoint/variant instead of the websocket variant"],"exampleFix":"// before\nStreamPluginService(PluginServiceWebSocket, http.StatusUpgradeRequired, serve)\n// after\nStreamPluginService(PluginServiceWebSocket, 400, serve)","handlingStrategy":"validation","validationCode":"func validWebSocketStatus(status int) bool {\n\treturn status == 101 || status == 400 || status == 500\n}","typeGuard":null,"tryCatchPattern":"defer func() {\n\tif rec := recover(); rec != nil {\n\t\tlog.Printf(\"invalid websocket status: %v\", rec)\n\t}\n}() // around StreamPluginService(PluginServiceWebSocket, ...)","preventionTips":["Only 101 for upgrades, 400/500 for handshake failures","Do not apply HTTP auth codes (401/403) to websocket handshake responses; perform auth before upgrading","Test both the success (101) and failure (400/500) handshake paths"],"tags":["go","api-contract","plugin-service","websocket","http-status"],"backgroundTag":"unexpected-http-status","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"}