{"record":{"id":"cb4c55f6b61b4d71","repo":"siyuan-note/siyuan","slug":"websocket-handshake-contains-a-body","errorCode":null,"errorMessage":"WebSocket handshake contains a body","messagePattern":"WebSocket handshake contains a body","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/apicontract/plugin_service_protocol.go","lineNumber":230,"sourceCode":"\t\tvalid := json.Valid(payload)\n\t\tif tail, ok := strings.CutSuffix(string(payload), \");\"); ok {\n\t\t\tfor index, char := range tail {\n\t\t\t\tif char == '(' && json.Valid([]byte(tail[index+1:])) {\n\t\t\t\t\tvalid = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif !valid {\n\t\t\treturn fmt.Errorf(\"invalid plugin JSONP response\")\n\t\t}\n\tcase PluginServiceSecureJSON:\n\t\tif !json.Valid(payload) && !json.Valid([]byte(strings.TrimPrefix(string(payload), \"while(1);\"))) {\n\t\t\treturn fmt.Errorf(\"invalid plugin secure JSON response\")\n\t\t}\n\tcase PluginServiceWebSocket:\n\t\tif status == 101 && len(payload) != 0 {\n\t\t\treturn fmt.Errorf(\"WebSocket handshake contains a body\")\n\t\t}\n\tcase PluginServiceXML:\n\t\tdecoder := xml.NewDecoder(strings.NewReader(string(payload)))\n\t\tfor {\n\t\t\tif _, err := decoder.Token(); err != nil {\n\t\t\t\tif err == io.EOF {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\tcase PluginServiceYAML:\n\t\tvar value yaml.Node\n\t\tif err := yaml.Unmarshal(payload, &value); err != nil {\n\t\t\treturn err\n\t\t}\n\tcase PluginServiceTOML:\n\t\tvar value map[string]any","sourceCodeStart":212,"sourceCodeEnd":248,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/apicontract/plugin_service_protocol.go#L212-L248","documentation":"For PluginServiceWebSocket endpoints, ValidatePluginServiceResponse enforces that a successful HTTP 101 Switching Protocols handshake carries no response body. If the status is 101 and the payload is non-empty, the validator returns \"WebSocket handshake contains a body\". A 101 response with a body is malformed per RFC 6455 and would corrupt the frame stream that follows.","triggerScenarios":"Calling Bundle.ValidatePluginServiceResponse with mode PluginServiceWebSocket, status 101, and a non-empty payload — typically a handler that writes data to the connection before or during the upgrade, or a reverse proxy that appends an error body to an upgraded response.","commonSituations":"The plugin handler calls w.Write alongside the Hijack/upgrade sequence; a middleware (logging, compression) wraps the connection and injects bytes; an intermediary proxies an upstream 101 but attaches its own error text; the upgrade succeeds conditionally but buffered output is flushed anyway.","solutions":["Remove any w.Write/Flush calls on the connection when the upgrade to 101 succeeds","Ensure middleware wrapping the WebSocket route does not buffer or append body bytes","Check reverse-proxy configuration so 101 responses pass through without an injected error body","Return 400 or 500 with the body instead of 101 if you need to report an error before upgrading"],"exampleFix":"// before\nif upgraded { w.Write([]byte(\"hello\")) }\n// after\nif upgraded { /* write frames only, never body bytes on a 101 */ }","handlingStrategy":"try-catch","validationCode":"func handshakeIsClean(status int, payload []byte) bool { return status != 101 || len(payload) == 0 }","typeGuard":null,"tryCatchPattern":"if err := bundle.ValidatePluginServiceResponse(method, path, PluginServiceWebSocket, status, ct, payload); err != nil { if strings.Contains(err.Error(), \"WebSocket handshake contains a body\") { dropConnectionAndLog(err); return }; return err }","preventionTips":["Never call w.Write on a connection that returned 101; use frame writes only","Audit middleware chains on WebSocket routes for buffering/wrapping writers","Configure proxies to pass 101 upgrades through untouched","Return 4xx/5xx with a body when you need to report upgrade failures"],"tags":["websocket","http","protocol-violation","plugin-api"],"backgroundTag":"unexpected-response-shape","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"}