{"record":{"id":"3dc5f11c246da878","repo":"siyuan-note/siyuan","slug":"invalid-rpc-id","errorCode":null,"errorMessage":"invalid RPC ID","messagePattern":"invalid RPC ID","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/apicontract/plugin_rpc.go","lineNumber":17,"sourceCode":"package apicontract\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n)\n\ntype PluginRPCID struct{ value JSONValue }\n\nfunc (id PluginRPCID) MarshalJSON() ([]byte, error) { return json.Marshal(id.value) }\nfunc (id *PluginRPCID) UnmarshalJSON(data []byte) error {\n\tdata = bytes.TrimSpace(data)\n\tif !json.Valid(data) || len(data) == 0 {\n\t\treturn fmt.Errorf(\"invalid RPC ID\")\n\t}\n\tif data[0] != '\"' && data[0] != '-' && (data[0] < '0' || data[0] > '9') && string(data) != \"null\" {\n\t\treturn fmt.Errorf(\"invalid id field: must be string, number, null or omitted\")\n\t}\n\treturn json.Unmarshal(data, &id.value)\n}\n\ntype PluginRPCParams struct{ value JSONValue }\n\ntype PluginRPCRequestFields struct {\n\tJSONRPC string           `json:\"jsonrpc\" api:\"const=\\\"2.0\\\"\"`\n\tMethod  string           `json:\"method\"`\n\tParams  *PluginRPCParams `json:\"params\" api:\"optional\"`\n\tID      *PluginRPCID     `json:\"id\" api:\"optional\"`\n}\n\ntype PluginRPCSuccess struct {\n\tJSONRPC string      `json:\"jsonrpc\" api:\"const=\\\"2.0\\\"\"`","sourceCodeStart":1,"sourceCodeEnd":35,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/apicontract/plugin_rpc.go#L1-L35","documentation":"PluginRPCID wraps the JSON-RPC-style \"id\" field of plugin RPC requests. Its UnmarshalJSON first requires the raw value to be valid, non-empty JSON; otherwise it returns \"invalid RPC ID\". This catches syntactically broken or empty id payloads before the per-type member check (string/number/null).","triggerScenarios":"Unmarshalling a plugin RPC message whose id field is invalid JSON (e.g. raw bytes like `undefined`, a bare token, or truncated data), or an empty/whitespace-only value.","commonSituations":"A plugin builds the JSON-RPC message by string concatenation and emits a non-JSON id (undefined in JS serializes oddly or is dropped); truncated transport frames; hand-written test fixtures with invalid JSON.","solutions":["Send the id as a proper JSON value: a quoted string, a number, or null (or omit the field where allowed).","Use JSON.stringify / json.Marshal on the whole message instead of manual string building.","Validate the message body with a JSON parser before posting it to the RPC endpoint."],"exampleFix":"// before\n{\"jsonrpc\":\"2.0\",\"id\":undefined,\"method\":\"x\"} // -> {\"id\":} invalid\n// after\n{\"jsonrpc\":\"2.0\",\"id\":\"req-1\",\"method\":\"x\"}","handlingStrategy":"try-catch","validationCode":"function checkRpcId(id) { if (!(typeof id === \"string\" || typeof id === \"number\" || id === null)) throw new Error(\"rpc id must be string, number or null\"); }","typeGuard":"const isValidRpcId = (id) => typeof id === \"string\" || (typeof id === \"number\" && Number.isFinite(id)) || id === null;","tryCatchPattern":"try { const parsed = JSON.parse(msg); send(parsed); } catch (e) { if (String(e).includes(\"invalid RPC ID\")) { regenerateAndResendWithScalarId(); } else { throw e; } }","preventionTips":["Serialize the whole RPC message with JSON.stringify, never string concatenation.","Use a counter or uuid string as the id.","Reject non-scalar ids at message construction time."],"tags":["json","rpc","plugin"],"backgroundTag":"json-unmarshal-failed","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"}