{"record":{"id":"84f8e3148b5f2519","repo":"siyuan-note/siyuan","slug":"rpc-reply-requires-success-or-failure","errorCode":null,"errorMessage":"RPC reply requires success or failure","messagePattern":"RPC reply requires success or failure","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/apicontract/plugin_rpc.go","lineNumber":96,"sourceCode":"\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)\n\t}\n\tif r.failure != nil {\n\t\treturn json.Marshal(r.failure)\n\t}\n\treturn nil, fmt.Errorf(\"RPC reply requires success or failure\")\n}\n\ntype PluginRPCResponse struct {\n\tsingle *PluginRPCReply\n\tbatch  []PluginRPCReply\n}\n\nfunc RPCSingleResponse(reply PluginRPCReply) PluginRPCResponse {\n\treturn PluginRPCResponse{single: &reply}\n}\nfunc RPCBatchResponse(replies []PluginRPCReply) PluginRPCResponse {\n\treturn PluginRPCResponse{batch: replies}\n}\nfunc (r PluginRPCResponse) MarshalJSON() ([]byte, error) {\n\tif r.single != nil {\n\t\treturn json.Marshal(r.single)\n\t}\n\tif len(r.batch) == 0 {","sourceCodeStart":78,"sourceCodeEnd":114,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/apicontract/plugin_rpc.go#L78-L114","documentation":"PluginRPCReply wraps either a success result or a JSON-RPC error object; exactly one must be set via RPCSuccessReply() or RPCFailureReply(). MarshalJSON throws this error when the reply struct was built neither way, i.e. it is the zero value. A JSON-RPC reply must contain either \"result\" or \"error\", never neither.","triggerScenarios":"Marshaling a PluginRPCReply created without RPCSuccessReply()/RPCFailureReply(), or marshaling a PluginRPCResponse whose single reply is a zero value.","commonSituations":"Constructing PluginRPCReply{...} directly with composite literal syntax (impossible to set private fields), forgetting to return an error reply in an error branch of an RPC handler, or refactor moving reply construction out of a function leaving it unset.","solutions":["Use RPCSuccessReply(apicontract.PluginRPCSuccess{...}) for success or RPCFailureReply(apicontract.RPCErrorResponse(code, msg, detail)) for failures","Ensure every handler branch (including early returns and errors) produces a reply via one of the two constructors","Avoid declaring PluginRPCReply variables without initializing them"],"exampleFix":"// before\nvar reply apicontract.PluginRPCReply\nreturn apicontract.RPCSingleResponse(reply)\n// after\nreply := apicontract.RPCFailureReply(apicontract.RPCErrorResponse(-32601, \"Method not found\", method))\nreturn apicontract.RPCSingleResponse(reply)","handlingStrategy":"type-guard","validationCode":"reply := buildReply(...)\nif reply == (apicontract.PluginRPCReply{}) { reply = apicontract.RPCFailureReply(apicontract.RPCErrorResponse(-32603, \"Internal error\", \"\")) }","typeGuard":"func replyIsSet(r apicontract.PluginRPCReply) bool { return r != apicontract.PluginRPCReply{} }","tryCatchPattern":"data, err := json.Marshal(reply)\nif err != nil {\n    return apicontract.RPCSingleResponse(apicontract.RPCFailureReply(\n        apicontract.RPCErrorResponse(-32603, \"Internal error\", err.Error())))\n}","preventionTips":["Use RPCSuccessReply/RPCFailureReply exclusively; treat the zero PluginRPCReply as a bug","Ensure error branches return an RPCFailureReply rather than skipping the reply","Cover all handler branches with marshal tests"],"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"}