{"record":{"id":"c6d72ecc0d9d4003","repo":"siyuan-note/siyuan","slug":"block-updates-are-empty","errorCode":null,"errorMessage":"block updates are empty","messagePattern":"block updates are empty","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"kernel/model/block_update.go","lineNumber":85,"sourceCode":"\tfor _, queued := range takeQueuedTransactions() {\n\t\tflushTx(queued)\n\t}\n\n\toperations, rootIDs, err := build(inputs)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\ttransaction := &Transaction{DoOperations: operations}\n\tif err = performTxSyncLocked(transaction); err != nil {\n\t\treturn nil, nil, err\n\t}\n\treturn []*Transaction{transaction}, rootIDs, nil\n}\n\nfunc buildBlockUpdateOperations(inputs []BlockUpdateInput, resolveTree blockUpdateTreeResolver, loadTree blockUpdateTreeLoader) (operations []*Operation, rootIDs []string, err error) {\n\tif 1 > len(inputs) {\n\t\treturn nil, nil, errors.New(\"block updates are empty\")\n\t}\n\n\tluteEngine := util.NewLute()\n\trootIDSet := map[string]struct{}{}\n\ttreeCache := map[blockUpdateTreeKey]*parse.Tree{}\n\tfor _, input := range inputs {\n\t\tif !ast.IsNodeIDPattern(input.ID) {\n\t\t\treturn nil, nil, fmt.Errorf(\"invalid block ID [%s]\", input.ID)\n\t\t}\n\n\t\tdata, dataTree, parseErr := parseBlockUpdateData(input.Data, input.DataType, luteEngine)\n\t\tif parseErr != nil {\n\t\t\treturn nil, nil, parseErr\n\t\t}\n\n\t\tvar oldTree *parse.Tree\n\t\tvar cacheKey blockUpdateTreeKey\n\t\thasCacheKey := false","sourceCodeStart":67,"sourceCodeEnd":103,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/model/block_update.go#L67-L103","documentation":"buildBlockUpdateOperations (the engine behind BuildBlockUpdateOperations) rejects the request up front when the inputs array is empty, returning \"block updates are empty\". Batch block updates require at least one BlockUpdateInput; there is nothing to validate or transact otherwise.","triggerScenarios":"Calling the block update API with an empty operations/inputs list — e.g. a caller that filtered out all pending updates locally, or passed an uninitialized array.","commonSituations":"Plugins that accumulate edits into a batch and flush only when something changed, but flush unconditionally; clients serializing an empty selection or clipboard state.","solutions":["Check inputs.length (or operations.length) > 0 before invoking the update API and skip the call when empty.","Ensure your local filter/validation isn't stripping all entries before the call.","Guard the flush call site with a flag that tracks whether any update was actually queued."],"exampleFix":"// before\nawait fetchPost('/api/block/updateBlock', { operations: pending });\n// after\nif (pending.length > 0) {\n  await fetchPost('/api/block/updateBlock', { operations: pending });\n}","handlingStrategy":"validation","validationCode":"function assertNonEmpty(updates) {\n  if (!Array.isArray(updates) || updates.length === 0) {\n    throw new SkipUpdate('nothing to update');\n  }\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Guard batch flush calls with a length check.","Track a dirty flag instead of flushing on a timer regardless of pending work.","Ensure filters that trim invalid updates also cancel the flush when they empty the batch."],"tags":["empty-input","batch-update","api","kernel"],"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"}