{"record":{"id":"ba9a0aaba5689bb6","repo":"siyuan-note/siyuan","slug":"empty-plugin-response-contains-a-body","errorCode":null,"errorMessage":"empty plugin response contains a body","messagePattern":"empty plugin response contains a body","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/apicontract/plugin_service_protocol.go","lineNumber":205,"sourceCode":"\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}\n\tif err := validatePluginServiceStatus(mode, status); err != nil {\n\t\treturn err\n\t}\n\tif 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\tswitch mode {\n\tcase PluginServiceEmpty:\n\t\tif len(payload) != 0 {\n\t\t\treturn fmt.Errorf(\"empty plugin response contains a body\")\n\t\t}\n\tcase PluginServiceJSON, PluginServiceASCIIJSON, PluginServiceIndentedJSON, PluginServicePureJSON:\n\t\tif !json.Valid(payload) {\n\t\t\treturn fmt.Errorf(\"invalid plugin JSON response\")\n\t\t}\n\tcase PluginServiceJSONP:\n\t\tvalid := json.Valid(payload)\n\t\tif tail, ok := strings.CutSuffix(string(payload), \");\"); ok {\n\t\t\tfor index, char := range tail {\n\t\t\t\tif char == '(' && json.Valid([]byte(tail[index+1:])) {\n\t\t\t\t\tvalid = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif !valid {\n\t\t\treturn fmt.Errorf(\"invalid plugin JSONP response\")\n\t\t}","sourceCodeStart":187,"sourceCodeEnd":223,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/apicontract/plugin_service_protocol.go#L187-L223","documentation":"Empty-mode plugin service responses must have zero-length payloads. This error means the handler declared an empty response (PluginServiceEmpty) but the validated payload contains bytes, contradicting the declared variant.","triggerScenarios":"Calling Bundle.ValidatePluginServiceResponse with mode PluginServiceEmpty and a non-empty payload for a non-HEAD, non-204/304 status; a handler that uses the empty responder but still writes data.","commonSituations":"Switching a handler from JSON to empty responses without removing the final c.JSON/body write; middleware appending bytes after the handler; test fixtures that reuse a populated body for an empty-mode case.","solutions":["Remove the body write so the handler emits nothing for empty-mode responses","If a body is intended, declare the response with the matching variant (e.g. PluginServiceJSON or PluginServiceString)","In tests, pass nil/empty payload when validating empty-mode responses"],"exampleFix":"// before\nrespond(PluginServiceEmpty, 200, []byte(\"ok\"))\n// after\nrespond(PluginServiceEmpty, 200, nil)","handlingStrategy":"validation","validationCode":"if mode == apicontract.PluginServiceEmpty && len(payload) != 0 {\n\tpayload = nil\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["When declaring empty responses, remove all body writes including middleware-added ones","Map each handler to exactly one response variant and keep payload construction consistent","Add tests asserting zero-length bodies for empty-mode endpoints"],"tags":["go","api-contract","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"}