{"record":{"id":"10ff1afcf066868f","repo":"siyuan-note/siyuan","slug":"field-transactions-must-not-be-empty","errorCode":null,"errorMessage":"Field [transactions] must not be empty","messagePattern":"Field \\[transactions\\] must not be empty","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/apicontract/transaction_decode.go","lineNumber":36,"sourceCode":"\t\treturn avDecodeParsedRequest[TransactionHistoryRequest](reader, \"/api/transactions/undo\")\n\t}\n\tPerformRedo.decodeRequest = func(reader io.Reader) (TransactionHistoryRequest, error) {\n\t\treturn avDecodeParsedRequest[TransactionHistoryRequest](reader, \"/api/transactions/redo\")\n\t}\n\tPerformTransactions.decodeRequest = decodePerformTransactions\n}\n\nfunc decodePerformTransactions(reader io.Reader) (request PerformTransactionsRequest, err error) {\n\tfields, err := blockRequestFields(reader, \"/api/transactions\")\n\tif err != nil {\n\t\treturn request, err\n\t}\n\ttransactions, err := legacyField[[]json.RawMessage](fields, \"transactions\", \"Array\", true)\n\tif err != nil {\n\t\treturn request, err\n\t}\n\tif len(transactions) == 0 {\n\t\treturn request, fmt.Errorf(\"Field [transactions] must not be empty\")\n\t}\n\tif request.ReqID, err = legacyField[float64](fields, \"reqId\", \"Number\", true); err != nil {\n\t\treturn request, err\n\t}\n\tif request.App, err = legacyField[string](fields, \"app\", \"String\", false); err != nil {\n\t\treturn request, err\n\t}\n\tif request.Session, err = legacyField[string](fields, \"session\", \"String\", false); err != nil {\n\t\treturn request, err\n\t}\n\trequest.TransactionJSON, request.DecodeError = normalizeTransactionJSON(fields[\"transactions\"])\n\tif request.DecodeError == nil {\n\t\trequest.DecodeError = json.Unmarshal(request.TransactionJSON, &request.Transactions)\n\t}\n\treturn request, nil\n}\n\n// normalizeTransactionJSON 保留入口先按 JSON 数字读取、再绑定事务结构的数值语义。","sourceCodeStart":18,"sourceCodeEnd":54,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/apicontract/transaction_decode.go#L18-L54","documentation":"The performTransactions endpoint requires a non-empty `transactions` array. The legacy field decoder demands the key be present as an Array, and a present-but-empty array is explicitly rejected because a transaction batch with nothing in it has no meaning and would silently do nothing.","triggerScenarios":"POSTing /api/transaction with {\"transactions\": []}; clients building the payload but never pushing DoOperations into it; race where all operations were filtered out before sending.","commonSituations":"Optimistic-UI code that flushes an empty op queue; refactored clients omitting individual ops while still sending the envelope; test harnesses sending placeholder payloads.","solutions":["Include at least one transaction object with actions in the transactions array","Skip calling the endpoint entirely when the collected operations list is empty","Inspect the client op-queue logic to find why it produced zero operations","Check that operations are not being filtered out (e.g. by dedup/undo logic) before submit"],"exampleFix":"// before\nfetchPost(\"/api/transaction\", {transactions: [], reqId: 1})\n// after\nif (ops.length > 0) {\n  fetchPost(\"/api/transaction\", {transactions: [{doOperations: ops, undoOperations: undo}], reqId: 1})\n}","handlingStrategy":"validation","validationCode":"if (!Array.isArray(transactions) || transactions.length === 0) return; // skip call\nawait fetchPost(\"/api/transaction\", {transactions, reqId: Date.now()});","typeGuard":"const hasTransactions = (t) => Array.isArray(t) && t.length > 0;","tryCatchPattern":"try {\n  await fetchPost(\"/api/transaction\", payload);\n} catch (e) {\n  if (e.message.includes(\"must not be empty\")) console.warn(\"skipped empty transaction batch\");\n  else throw e;\n}","preventionTips":["Guard the submit path: never call the endpoint with an empty op queue","Verify operations were actually collected (not filtered out) before flushing"],"tags":["validation","empty-array","api"],"backgroundTag":"empty-required-field","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"}