{"record":{"id":"3be9673ef83dec08","repo":"siyuan-note/siyuan","slug":"plugin-service-response-forbids-a-body","errorCode":null,"errorMessage":"plugin service response forbids a body","messagePattern":"plugin service response forbids a body","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/apicontract/plugin_service_protocol.go","lineNumber":174,"sourceCode":"\tcase PluginServiceWebSocket:\n\t\tif status != 101 && status != 400 && status != 500 {\n\t\t\treturn fmt.Errorf(\"invalid plugin WebSocket status\")\n\t\t}\n\tcase PluginServiceSSE:\n\t\tif status != 200 && status != 500 {\n\t\t\treturn fmt.Errorf(\"invalid plugin SSE status\")\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (b *Bundle) validatePluginServiceHTTPResponse(endpoint EndpointSchema, status int, contentType string, payload []byte) error {\n\tif status < 100 || status > 999 {\n\t\treturn fmt.Errorf(\"invalid plugin service HTTP status\")\n\t}\n\tif endpoint.Method == \"HEAD\" || status < 200 || status == 204 || status == 304 {\n\t\tif len(payload) > 0 {\n\t\t\treturn fmt.Errorf(\"plugin service response forbids a body\")\n\t\t}\n\t\treturn nil\n\t}\n\t// 原始文件、代理及插件自选媒体允许任意字节，具体分支由 ValidatePluginServiceResponse 校验。\n\treturn nil\n}\n\nfunc (b *Bundle) ValidatePluginServiceResponse(method, path string, mode PluginServiceMode, status int, contentType string, payload []byte) error {\n\tvar found bool\n\tfor _, endpoint := range b.Endpoints {\n\t\tif endpoint.Method == method && endpoint.Path == path && endpoint.PluginService != nil {\n\t\t\tfound = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif !found {\n\t\treturn fmt.Errorf(\"unregistered plugin service: %s %s\", method, path)\n\t}","sourceCodeStart":156,"sourceCodeEnd":192,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/apicontract/plugin_service_protocol.go#L156-L192","documentation":"HTTP rules forbid response bodies for HEAD requests, any status below 200 (1xx), 204 No Content, and 304 Not Modified. This error means the plugin service produced bytes for such a response, which would corrupt the HTTP framing (e.g. a body after 204 confuses clients and proxies).","triggerScenarios":"Writing payload bytes with http.ResponseWriter for a HEAD request, or replying with status 204/304 (or a 1xx) while still emitting a body in a plugin-service endpoint.","commonSituations":"A handler writes a JSON error body but sets the status to 204; a shared middleware writes a body before the handler decides on 304; framework helpers that always render a body regardless of method/status.","solutions":["Skip all body writes when the request method is HEAD or the status is <200, 204, or 304","Set the body-less status only after ensuring no bytes were already written","Return an empty payload ([]byte(nil)) for these responses in tests/validation"],"exampleFix":"// before\nw.WriteHeader(http.StatusNoContent)\nw.Write([]byte(\"{}\"))\n// after\nw.WriteHeader(http.StatusNoContent) // no body written","handlingStrategy":"validation","validationCode":"func bodyForbidden(method string, status int) bool {\n\treturn method == http.MethodHead || status < 200 || status == http.StatusNoContent || status == http.StatusNotModified\n}\nif bodyForbidden(endpoint.Method, status) && len(payload) > 0 {\n\tpayload = nil // drop body before validation/writing\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Check method/status before any w.Write call","Avoid helpers that unconditionally render a body","Add recorder-based tests asserting empty bodies for HEAD/204/304"],"tags":["go","http","plugin-service","response-body"],"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"}