{"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/251596fc0de2f9528c00c224252fd073a99973f4/kernel/model/block_update.go#L67-L103","documentation":"Returned by buildBlockUpdateOperations when len(inputs) < 1. The function is the entry point for assembling block-update operations; an empty input slice has nothing to apply, so it rejects immediately with errors.New before doing any work.","triggerScenarios":"API or kernel call with an empty inputs array; client-side gather loop produced zero entries due to a UI bug; upstream filter that removed all inputs but still forwarded the call.","commonSituations":"Frontend selection cleared before the request fired; batch caller not skipping the empty case; misrouted call where another endpoint should have handled the no-op.","solutions":["Skip the API call entirely when inputs is empty - it is a no-op by definition.","Add a guard at the caller: if len(inputs) == 0 return nil without invoking the kernel.","Audit the client gather logic to understand why an empty batch was submitted."],"exampleFix":"// before\n_, _, err := buildBlockUpdateOperations(inputs, resolver, loader)\n\n// after\nif len(inputs) == 0 {\n    return nil, nil, nil\n}\n_, _, err := buildBlockUpdateOperations(inputs, resolver, loader)","handlingStrategy":"validation","validationCode":"if len(inputs) == 0 {\n    return nil, nil, nil // no-op\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always short-circuit empty batches at the caller.","Audit client-side gather logic to never submit an empty array.","Treat an empty input as a successful no-op, not an error path."],"tags":["block-update","validation","input-validation"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}