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

  1. Build the message with RPCResponseMessage(RPCSingleResponse(...)) or RPCNotificationMessage(PluginRPCNotification{...}) instead of declaring a bare PluginRPCMessage value
  2. Check for control-flow paths where neither builder is called before serialization and add a default branch
  3. 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

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


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: &notification}
}

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)