{"record":{"id":"72faebe172387a60","repo":"siyuan-note/siyuan","slug":"rpc-message-requires-a-response-or-notification","errorCode":null,"errorMessage":"RPC message requires a response or notification","messagePattern":"RPC message requires a response or notification","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/apicontract/plugin_rpc.go","lineNumber":73,"sourceCode":"\tnotification *PluginRPCNotification\n}\n\nfunc RPCResponseMessage(response PluginRPCResponse) PluginRPCMessage {\n\treturn PluginRPCMessage{response: &response}\n}\n\nfunc RPCNotificationMessage(notification PluginRPCNotification) PluginRPCMessage {\n\treturn PluginRPCMessage{notification: &notification}\n}\n\nfunc (m PluginRPCMessage) MarshalJSON() ([]byte, error) {\n\tif m.response != nil {\n\t\treturn json.Marshal(m.response)\n\t}\n\tif m.notification != nil {\n\t\treturn json.Marshal(m.notification)\n\t}\n\treturn nil, fmt.Errorf(\"RPC message requires a response or notification\")\n}\n\ntype PluginRPCFailure struct {\n\tJSONRPC string          `json:\"jsonrpc\" api:\"const=\\\"2.0\\\"\"`\n\tError   *PluginRPCError `json:\"error\"`\n\tID      PluginRPCID     `json:\"id\"`\n}\n\ntype PluginRPCReply struct {\n\tsuccess *PluginRPCSuccess\n\tfailure *PluginRPCFailure\n}\n\nfunc RPCSuccessReply(result PluginRPCSuccess) PluginRPCReply { return PluginRPCReply{success: &result} }\nfunc RPCFailureReply(result PluginRPCFailure) PluginRPCReply { return PluginRPCReply{failure: &result} }\nfunc (r PluginRPCReply) MarshalJSON() ([]byte, error) {\n\tif r.success != nil {\n\t\treturn json.Marshal(r.success)","sourceCodeStart":55,"sourceCodeEnd":91,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/apicontract/plugin_rpc.go#L55-L91","documentation":"PluginRPCMessage is an outbound JSON-RPC 2.0 wire message that must carry exactly one of: a response (reply to a call) or a notification. MarshalJSON returns this error when neither was set, meaning the zero-value struct is being serialized. The library enforces the JSON-RPC 2.0 requirement that every sent message be one of these two forms.","triggerScenarios":"Serializing a PluginRPCMessage constructed without RPCResponseMessage() or RPCNotificationMessage(), e.g. declaring a variable `var msg PluginRPCMessage` and passing it to json.Marshal or writing it to a response writer.","commonSituations":"Zero-value struct declarations in Go (very easy since PluginRPCMessage's constructors are the only way to populate the private fields), copy-pasted message construction where the builder call was deleted, or conditional code paths that skip both builder calls.","solutions":["Build the message with RPCResponseMessage(RPCSingleResponse(...)) or RPCNotificationMessage(PluginRPCNotification{...}) instead of declaring a bare PluginRPCMessage value","Check for control-flow paths where neither builder is called before serialization and add a default branch","If the message is legitimately empty, skip writing it instead of marshaling a zero value"],"exampleFix":"// before\nvar msg apicontract.PluginRPCMessage\njson.NewEncoder(w).Encode(msg)\n// after\nmsg := apicontract.RPCResponseMessage(apicontract.RPCSingleResponse(\n    apicontract.RPCSuccessReply(apicontract.PluginRPCSuccess{JSONRPC: \"2.0\", ID: id, Result: result})))\njson.NewEncoder(w).Encode(msg)","handlingStrategy":"type-guard","validationCode":"func hasRPCPayload(m apicontract.PluginRPCMessage) bool { return m != (apicontract.PluginRPCMessage{}) } // zero value check before marshal","typeGuard":"func isRPCMessageSet(m apicontract.PluginRPCMessage) bool { return m != apicontract.PluginRPCMessage{} }","tryCatchPattern":"data, err := json.Marshal(msg)\nif err != nil {\n    log.Printf(\"rpc message marshal failed: %v\", err)\n    return err\n}","preventionTips":["Always construct PluginRPCMessage via RPCResponseMessage or RPCNotificationMessage, never as a bare variable","Never write a code path that can serialize an unset message; return early instead","Add a unit test that marshals every handler's return value"],"tags":["json-rpc","serialization","go","zero-value"],"backgroundTag":"mutually-exclusive-options","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"}