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

  1. Use RPCSuccessReply(apicontract.PluginRPCSuccess{...}) for success or RPCFailureReply(apicontract.RPCErrorResponse(code, msg, detail)) for failures
  2. Ensure every handler branch (including early returns and errors) produces a reply via one of the two constructors
  3. 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

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


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)