siyuan-note/siyuan · error
WebSocket upgrade cannot contain a body
Error message
WebSocket upgrade cannot contain a body
What it means
validateRawWebSocketHTTP enforces HTTP-level constraints on raw WebSocket endpoints. A 101 Switching Protocols response must have an empty body per RFC 6455, so any non-empty payload alongside status 101 is rejected. Valid outcomes are a bodyless 101 upgrade or a 200 with an empty body when EmptyClosedResponse is set.
Solutions
- Ensure the upgrade handler writes nothing to the response body before/with status 101
- Remove body bytes from the 101 response in the contract fixture
- Return 101 with headers only (Upgrade, Connection, Sec-WebSocket-Accept)
- Use a 200 empty-body closed response (allowed when EmptyClosedResponse is set) instead of a 101 with content
Example fix
// before
w.WriteHeader(http.StatusSwitchingProtocols)
w.Write([]byte("upgraded"))
// after
w.WriteHeader(http.StatusSwitchingProtocols) // no body for 101 Defensive patterns
Strategy: validation
Validate before calling
if (status === 101 && payload && payload.length > 0) throw new Error("101 must have empty body"); Type guard
function isValidUpgradeResponse(res) { return res.status !== 101 || !res.body || res.body.length === 0; } Try / catch
try { validateResponse(res); } catch (e) { if (String(e).includes("cannot contain a body")) { res.body = empty; validateResponse(res); } else throw e; } Prevention
- Never write to the response body before/with 101 Switching Protocols
- Strip bodies from 101 fixtures in contract tests
- Audit middleware for body writes on upgrade paths
- Use 200 + empty body (EmptyClosedResponse) for closed responses instead
When it happens
Trigger: ValidateHTTPResponse runs against a raw WebSocket endpoint where the recorded/actual 101 response includes a non-empty payload, e.g. a handshake handler that writes bytes before upgrading or a fixture that attaches a body to the upgrade response.
Common situations: Middleware or logging wrappers writing to the response body before hijacking the connection; test fixtures capturing a body for 101 responses; proxies injecting content into upgrade responses.
Related errors
- WebSocket handshake contains a body
- asset path [ ] does not match data path [ ]
- authentication probe returned HTTP " + response.status
- boot progress request returned HTTP " + response.status
- box and dataPath cannot be used together
AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19).
Data as JSON: /api/errors/3a8ec628e627ca22.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/apicontract/broadcast_protocol.go:50
return nil, fmt.Errorf("invalid raw SSE event declaration")
}
return &SSESchema{Raw: definition.Raw}, nil
}
func rawWebSocketSchema(definition *WebSocketDefinition) (*WebSocketSchema, error) {
if definition.Incoming != nil || definition.Outgoing != nil || definition.FailureStatus != 0 || !reflect.DeepEqual(definition.Raw, RawWebSocketOptions().WebSocket.Raw) {
return nil, fmt.Errorf("invalid raw WebSocket declaration")
}
return &WebSocketSchema{Incoming: &Schema{Type: "string", Format: "binary"}, Outgoing: &Schema{Type: "string", Format: "binary"}, Raw: definition.Raw}, nil
}
func validateRawWebSocketHTTP(schema *WebSocketSchema, status int, contentType string, payload []byte) (bool, error) {
if schema.Raw == nil {
return false, nil
}
if status == 101 {
if len(payload) != 0 {
return true, fmt.Errorf("WebSocket upgrade cannot contain a body")
}
return true, nil
}
if status == 200 && len(payload) == 0 && schema.Raw.EmptyClosedResponse {
return true, nil
}
media, _, _ := mime.ParseMediaType(contentType)
for _, allowed := range schema.Raw.UpgradeErrorStatuses {
if status == allowed && media == "text/plain" {
return true, nil
}
}
return false, nil
}
// ValidateRawSSEEvent 校验事件协议;事件数据是原始字节,不进行 JSON 解码或 Base64 转码。
func (b *Bundle) ValidateRawSSEEvent(method, path, name, id string, retry uint, payload []byte) error {
for _, endpoint := range b.Endpoints {View on GitHub (pinned to 9f775e8a12)