siyuan-note/siyuan · error
Field [transactions] must not be empty
Error message
Field [transactions] must not be empty
What it means
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.
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
Example fix
// before
fetchPost("/api/transaction", {transactions: [], reqId: 1})
// after
if (ops.length > 0) {
fetchPost("/api/transaction", {transactions: [{doOperations: ops, undoOperations: undo}], reqId: 1})
} Defensive patterns
Strategy: validation
Validate before calling
if (!Array.isArray(transactions) || transactions.length === 0) return; // skip call
await fetchPost("/api/transaction", {transactions, reqId: Date.now()}); Type guard
const hasTransactions = (t) => Array.isArray(t) && t.length > 0;
Try / catch
try {
await fetchPost("/api/transaction", payload);
} catch (e) {
if (e.message.includes("must not be empty")) console.warn("skipped empty transaction batch");
else throw e;
} Prevention
- Guard the submit path: never call the endpoint with an empty op queue
- Verify operations were actually collected (not filtered out) before flushing
When it happens
Trigger: POSTing /api/transaction with {"transactions": []}; clients building the payload but never pushing DoOperations into it; race where all operations were filtered out before sending.
Common situations: Optimistic-UI code that flushes an empty op queue; refactored clients omitting individual ops while still sending the envelope; test harnesses sending placeholder payloads.
Understand the failure class
Background: "must not be empty", "cannot be empty" — required-field validation errors across open-source libraries — this error's family across 41 libraries.
Related errors
- Field [ ] must not be empty
- Field [srcs] must not be empty
- AI editor action must not be empty
- block [ ] type is locked: expected , got
- block swap requires two non-document blocks
AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19).
Data as JSON: /api/errors/10ff1afcf066868f.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/apicontract/transaction_decode.go:36
return avDecodeParsedRequest[TransactionHistoryRequest](reader, "/api/transactions/undo")
}
PerformRedo.decodeRequest = func(reader io.Reader) (TransactionHistoryRequest, error) {
return avDecodeParsedRequest[TransactionHistoryRequest](reader, "/api/transactions/redo")
}
PerformTransactions.decodeRequest = decodePerformTransactions
}
func decodePerformTransactions(reader io.Reader) (request PerformTransactionsRequest, err error) {
fields, err := blockRequestFields(reader, "/api/transactions")
if err != nil {
return request, err
}
transactions, err := legacyField[[]json.RawMessage](fields, "transactions", "Array", true)
if err != nil {
return request, err
}
if len(transactions) == 0 {
return request, fmt.Errorf("Field [transactions] must not be empty")
}
if request.ReqID, err = legacyField[float64](fields, "reqId", "Number", true); err != nil {
return request, err
}
if request.App, err = legacyField[string](fields, "app", "String", false); err != nil {
return request, err
}
if request.Session, err = legacyField[string](fields, "session", "String", false); err != nil {
return request, err
}
request.TransactionJSON, request.DecodeError = normalizeTransactionJSON(fields["transactions"])
if request.DecodeError == nil {
request.DecodeError = json.Unmarshal(request.TransactionJSON, &request.Transactions)
}
return request, nil
}
// normalizeTransactionJSON 保留入口先按 JSON 数字读取、再绑定事务结构的数值语义。View on GitHub (pinned to 9f775e8a12)