siyuan-note/siyuan · error
WebSocket handshake contains a body
Error message
WebSocket handshake contains a body
What it means
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.
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
Example fix
// before
if upgraded { w.Write([]byte("hello")) }
// after
if upgraded { /* write frames only, never body bytes on a 101 */ } Defensive patterns
Strategy: try-catch
Validate before calling
func handshakeIsClean(status int, payload []byte) bool { return status != 101 || len(payload) == 0 } Try / catch
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 } Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
- SSL/TLS and certificate errors — how TLS handshakes and certificate validation fail.
Related errors
- plugin WebSocket control frame exceeds 125 bytes
- undeclared plugin WebSocket frame
- failed to convert request value to object
- failed to read response body
- WebSocket is not open
AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19).
Data as JSON: /api/errors/cb4c55f6b61b4d71.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/apicontract/plugin_service_protocol.go:230
valid := json.Valid(payload)
if tail, ok := strings.CutSuffix(string(payload), ");"); ok {
for index, char := range tail {
if char == '(' && json.Valid([]byte(tail[index+1:])) {
valid = true
break
}
}
}
if !valid {
return fmt.Errorf("invalid plugin JSONP response")
}
case PluginServiceSecureJSON:
if !json.Valid(payload) && !json.Valid([]byte(strings.TrimPrefix(string(payload), "while(1);"))) {
return fmt.Errorf("invalid plugin secure JSON response")
}
case PluginServiceWebSocket:
if status == 101 && len(payload) != 0 {
return fmt.Errorf("WebSocket handshake contains a body")
}
case PluginServiceXML:
decoder := xml.NewDecoder(strings.NewReader(string(payload)))
for {
if _, err := decoder.Token(); err != nil {
if err == io.EOF {
break
}
return err
}
}
case PluginServiceYAML:
var value yaml.Node
if err := yaml.Unmarshal(payload, &value); err != nil {
return err
}
case PluginServiceTOML:
var value map[string]anyView on GitHub (pinned to 9f775e8a12)