siyuan-note/siyuan · error
RPC message requires a response or notification
Error message
RPC message requires a response or notification
What it means
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.
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
Example fix
// before
var msg apicontract.PluginRPCMessage
json.NewEncoder(w).Encode(msg)
// after
msg := apicontract.RPCResponseMessage(apicontract.RPCSingleResponse(
apicontract.RPCSuccessReply(apicontract.PluginRPCSuccess{JSONRPC: "2.0", ID: id, Result: result})))
json.NewEncoder(w).Encode(msg) Defensive patterns
Strategy: type-guard
Validate before calling
func hasRPCPayload(m apicontract.PluginRPCMessage) bool { return m != (apicontract.PluginRPCMessage{}) } // zero value check before marshal Type guard
func isRPCMessageSet(m apicontract.PluginRPCMessage) bool { return m != apicontract.PluginRPCMessage{} } Try / catch
data, err := json.Marshal(msg)
if err != nil {
log.Printf("rpc message marshal failed: %v", err)
return err
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- RPC reply requires success or failure
- 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/72faebe172387a60.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/apicontract/plugin_rpc.go:73
notification *PluginRPCNotification
}
func RPCResponseMessage(response PluginRPCResponse) PluginRPCMessage {
return PluginRPCMessage{response: &response}
}
func RPCNotificationMessage(notification PluginRPCNotification) PluginRPCMessage {
return PluginRPCMessage{notification: ¬ification}
}
func (m PluginRPCMessage) MarshalJSON() ([]byte, error) {
if m.response != nil {
return json.Marshal(m.response)
}
if m.notification != nil {
return json.Marshal(m.notification)
}
return nil, fmt.Errorf("RPC message requires a response or notification")
}
type PluginRPCFailure struct {
JSONRPC string `json:"jsonrpc" api:"const=\"2.0\""`
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)View on GitHub (pinned to 9f775e8a12)