{"record":{"id":"979b8bb963757f11","repo":"siyuan-note/siyuan","slug":"rpc-call-requires-a-request-or-error","errorCode":null,"errorMessage":"RPC call requires a request or error","messagePattern":"RPC call requires a request or error","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/plugin/rpc_contract.go","lineNumber":114,"sourceCode":"}\n\nfunc pluginRPCRequests(calls []apicontract.PluginRPCParsedCall) ([]*JsonRpcProcessingRequest, error) {\n\trequests := make([]*JsonRpcProcessingRequest, len(calls))\n\tfor i, call := range calls {\n\t\tif call.Error != nil {\n\t\t\tdata, err := json.Marshal(call.Error)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tvar failure JsonRpcErrorResponse\n\t\t\tif err := json.Unmarshal(data, &failure); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\trequests[i] = &JsonRpcProcessingRequest{Error: &failure}\n\t\t\tcontinue\n\t\t}\n\t\tif call.Request == nil {\n\t\t\treturn nil, fmt.Errorf(\"RPC call requires a request or error\")\n\t\t}\n\t\trequest := &JsonRpcRequest{JsonRpc: JsonRpcVersion, Method: call.Request.Method}\n\t\trequest.Params.Exists, request.ID.Exists = call.Request.ParamsPresent, call.Request.IDPresent\n\t\t// 只有 RPC 的参数和关联标识进入脚本运行时的动态值，方法和请求结构保持类型约束。\n\t\tparams, err := json.Marshal(call.Request.Params)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif err := json.Unmarshal(params, &request.Params.Value); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\trequest.Params.IsNull = call.Request.ParamsNull\n\t\tid, err := json.Marshal(call.Request.ID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif err := json.Unmarshal(id, &request.ID.Value); err != nil {\n\t\t\treturn nil, err","sourceCodeStart":96,"sourceCodeEnd":132,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/plugin/rpc_contract.go#L96-L132","documentation":"When converting an incoming JSON-RPC batch/plugin call into the internal processing-request list, each call must carry either a Request or an Error object. If a call has neither (and no error was already assigned), pluginRPCRequests rejects it with this message because a JSON-RPC call with no request payload and no error cannot be dispatched.","triggerScenarios":"Passing a JsonRpcCall (or batch element) to pluginRPCRequests where both Call.Request is nil and no Error was set — e.g. a hand-constructed call struct, a batch entry that only carried an ID, or a parser that dropped an empty request object.","commonSituations":"Building RPC call structs manually in tests or plugins; third-party clients sending malformed JSON-RPC batch entries like {\"jsonrpc\":\"2.0\",\"id\":1} with no method/params; version drift between the caller's serialization and the rpc_contract parser.","solutions":["Ensure every JSON-RPC call in the batch sets either Request (method/params) or Error before calling pluginRPCRequests","Fix the producing side (client or parser) so it never emits a call with both Request and Error nil","Log the offending call's raw JSON to identify which entry is malformed","Add a validation step on parsed calls before dispatch"],"exampleFix":"// before\ncalls := []JsonRpcCall{{ID: json.RawMessage(`1`)}}\nreqs, err := pluginRPCRequests(calls)\n// after\ncalls := []JsonRpcCall{{ID: json.RawMessage(`1`), Request: &JsonRpcCallRequest{Method: \"siyuan.someMethod\"}}}\nreqs, err := pluginRPCRequests(calls)","handlingStrategy":"validation","validationCode":"for i, call := range calls {\n    if call.Request == nil && call.Error == nil {\n        return fmt.Errorf(\"call %d has neither request nor error\", i)\n    }\n}","typeGuard":"func hasPayload(c JsonRpcCall) bool { return c.Request != nil || c.Error != nil }","tryCatchPattern":null,"preventionTips":["Always populate Request or Error when building JsonRpcCall structs","Validate parsed JSON-RPC batches before dispatch","Fuzz the parser with malformed batch entries in tests"],"tags":["rpc","json-rpc","validation"],"backgroundTag":"missing-required-argument","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"}