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

  1. Ensure the upgrade handler writes nothing to the response body before/with status 101
  2. Remove body bytes from the 101 response in the contract fixture
  3. Return 101 with headers only (Upgrade, Connection, Sec-WebSocket-Accept)
  4. 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

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


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)