siyuan-note/siyuan · error
RPC reply requires success or failure
Error message
RPC reply requires success or failure
What it means
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.
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
Example fix
// before var reply apicontract.PluginRPCReply return apicontract.RPCSingleResponse(reply) // after reply := apicontract.RPCFailureReply(apicontract.RPCErrorResponse(-32601, "Method not found", method)) return apicontract.RPCSingleResponse(reply)
Defensive patterns
Strategy: type-guard
Validate before calling
reply := buildReply(...)
if reply == (apicontract.PluginRPCReply{}) { reply = apicontract.RPCFailureReply(apicontract.RPCErrorResponse(-32603, "Internal error", "")) } Type guard
func replyIsSet(r apicontract.PluginRPCReply) bool { return r != apicontract.PluginRPCReply{} } Try / catch
data, err := json.Marshal(reply)
if err != nil {
return apicontract.RPCSingleResponse(apicontract.RPCFailureReply(
apicontract.RPCErrorResponse(-32603, "Internal error", err.Error())))
} Prevention
- 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
When it happens
Trigger: Marshaling a PluginRPCReply created without RPCSuccessReply()/RPCFailureReply(), or marshaling a PluginRPCResponse whose single reply is a zero value.
Common situations: 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.
Related errors
- RPC message requires a response or notification
- RPC batch response must not be empty
- marshal box conf [ ] failed
- marshal master password migration failed
- 346
AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19).
Data as JSON: /api/errors/84f8e3148b5f2519.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/apicontract/plugin_rpc.go:96
Error *PluginRPCError `json:"error"`
ID PluginRPCID `json:"id"`
}
type PluginRPCReply struct {
success *PluginRPCSuccess
failure *PluginRPCFailure
}
func RPCSuccessReply(result PluginRPCSuccess) PluginRPCReply { return PluginRPCReply{success: &result} }
func RPCFailureReply(result PluginRPCFailure) PluginRPCReply { return PluginRPCReply{failure: &result} }
func (r PluginRPCReply) MarshalJSON() ([]byte, error) {
if r.success != nil {
return json.Marshal(r.success)
}
if r.failure != nil {
return json.Marshal(r.failure)
}
return nil, fmt.Errorf("RPC reply requires success or failure")
}
type PluginRPCResponse struct {
single *PluginRPCReply
batch []PluginRPCReply
}
func RPCSingleResponse(reply PluginRPCReply) PluginRPCResponse {
return PluginRPCResponse{single: &reply}
}
func RPCBatchResponse(replies []PluginRPCReply) PluginRPCResponse {
return PluginRPCResponse{batch: replies}
}
func (r PluginRPCResponse) MarshalJSON() ([]byte, error) {
if r.single != nil {
return json.Marshal(r.single)
}
if len(r.batch) == 0 {View on GitHub (pinned to 9f775e8a12)