{"record":{"id":"3a8ec628e627ca22","repo":"siyuan-note/siyuan","slug":"websocket-upgrade-cannot-contain-a-body","errorCode":null,"errorMessage":"WebSocket upgrade cannot contain a body","messagePattern":"WebSocket upgrade cannot contain a body","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/apicontract/broadcast_protocol.go","lineNumber":50,"sourceCode":"\t\treturn nil, fmt.Errorf(\"invalid raw SSE event declaration\")\n\t}\n\treturn &SSESchema{Raw: definition.Raw}, nil\n}\n\nfunc rawWebSocketSchema(definition *WebSocketDefinition) (*WebSocketSchema, error) {\n\tif definition.Incoming != nil || definition.Outgoing != nil || definition.FailureStatus != 0 || !reflect.DeepEqual(definition.Raw, RawWebSocketOptions().WebSocket.Raw) {\n\t\treturn nil, fmt.Errorf(\"invalid raw WebSocket declaration\")\n\t}\n\treturn &WebSocketSchema{Incoming: &Schema{Type: \"string\", Format: \"binary\"}, Outgoing: &Schema{Type: \"string\", Format: \"binary\"}, Raw: definition.Raw}, nil\n}\n\nfunc validateRawWebSocketHTTP(schema *WebSocketSchema, status int, contentType string, payload []byte) (bool, error) {\n\tif schema.Raw == nil {\n\t\treturn false, nil\n\t}\n\tif status == 101 {\n\t\tif len(payload) != 0 {\n\t\t\treturn true, fmt.Errorf(\"WebSocket upgrade cannot contain a body\")\n\t\t}\n\t\treturn true, nil\n\t}\n\tif status == 200 && len(payload) == 0 && schema.Raw.EmptyClosedResponse {\n\t\treturn true, nil\n\t}\n\tmedia, _, _ := mime.ParseMediaType(contentType)\n\tfor _, allowed := range schema.Raw.UpgradeErrorStatuses {\n\t\tif status == allowed && media == \"text/plain\" {\n\t\t\treturn true, nil\n\t\t}\n\t}\n\treturn false, nil\n}\n\n// ValidateRawSSEEvent 校验事件协议；事件数据是原始字节，不进行 JSON 解码或 Base64 转码。\nfunc (b *Bundle) ValidateRawSSEEvent(method, path, name, id string, retry uint, payload []byte) error {\n\tfor _, endpoint := range b.Endpoints {","sourceCodeStart":32,"sourceCodeEnd":68,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/apicontract/broadcast_protocol.go#L32-L68","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nw.WriteHeader(http.StatusSwitchingProtocols)\nw.Write([]byte(\"upgraded\"))\n// after\nw.WriteHeader(http.StatusSwitchingProtocols) // no body for 101","handlingStrategy":"validation","validationCode":"if (status === 101 && payload && payload.length > 0) throw new Error(\"101 must have empty body\");","typeGuard":"function isValidUpgradeResponse(res) { return res.status !== 101 || !res.body || res.body.length === 0; }","tryCatchPattern":"try { validateResponse(res); } catch (e) { if (String(e).includes(\"cannot contain a body\")) { res.body = empty; validateResponse(res); } else throw e; }","preventionTips":["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"],"tags":["websocket","http","protocol"],"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"}