{"record":{"id":"fb08fa9d68651240","repo":"siyuan-note/siyuan","slug":"block-not-found-or-its-encrypted-notebook-is-locke","errorCode":null,"errorMessage":"block not found or its encrypted notebook is locked","messagePattern":"block not found or its encrypted notebook is locked","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/api/box_lease.go","lineNumber":71,"sourceCode":"}\n\n// holdEncryptedBlockRequests 按块的实际归属取得响应租约，覆盖省略笔记本参数的通用和批量读取。\nfunc holdEncryptedBlockRequests(c *gin.Context, boxID string, ids []string, allowMissing bool) error {\n\tif boxID != \"\" {\n\t\treturn holdEncryptedBoxRequest(c, boxID)\n\t}\n\tboxIDSet := map[string]struct{}{}\n\tfor _, id := range ids {\n\t\tif !ast.IsNodeIDPattern(id) {\n\t\t\tcontinue\n\t\t}\n\t\tblock := treenode.GetBlockTree(id)\n\t\tif block == nil {\n\t\t\tif allowMissing {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// 查不到的块直接拒绝，防止随后解锁的笔记本在第二次查找时被无租约访问。\n\t\t\treturn errors.New(\"block not found or its encrypted notebook is locked\")\n\t\t}\n\t\tif model.IsEncryptedBox(block.BoxID) {\n\t\t\tboxIDSet[block.BoxID] = struct{}{}\n\t\t}\n\t}\n\tboxIDs := make([]string, 0, len(boxIDSet))\n\tfor id := range boxIDSet {\n\t\tboxIDs = append(boxIDs, id)\n\t}\n\tsort.Strings(boxIDs)\n\tfor _, id := range boxIDs {\n\t\tif err := holdEncryptedBoxRequest(c, id); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n","sourceCodeStart":53,"sourceCodeEnd":89,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/api/box_lease.go#L53-L89","documentation":"When a request omits the notebook (box) ID, holdEncryptedBlockRequests resolves each block ID's owning notebook via treenode.GetBlockTree. If a block cannot be found in the block index, the request is rejected (unless allowMissing is set) so that an unlocked-then-relocked notebook is never read without a lease — the error deliberately conflates \"not found\" with \"in locked encrypted notebook\" to avoid leaking existence.","triggerScenarios":"Calling generic or batch block APIs (holdBlockRequest / holdBlockSwapReplayRequests paths) with an unknown, deleted, or malformed ID and no box parameter; or referencing a block whose notebook is an encrypted box currently locked, so GetBlockTree cannot resolve its lease.","commonSituations":"Scripts caching IDs deleted by another client or sync; typos in block IDs; batch requests where one item in the list is stale (the whole request is rejected); passing an ID from history/exported snapshots that no longer exists.","solutions":["Verify each ID exists before the call: /api/query/sql SELECT id, box_id FROM blocks WHERE id IN (...) and drop missing IDs (or fix them)","Pass the explicit notebook (box) ID in the request when known, so the lease is taken on the box instead of per-block resolution","Filter batch item lists to IDs that currently exist in the index; re-query after sync completes","Rebuild the index if blocks are known to exist but GetBlockTree misses them (index out of sync with .sy files)"],"exampleFix":"// before: one stale ID fails the whole batch\nawait fetchPost('/api/block/updateBlock', { ids: allIds, data });\n// after: keep only resolvable IDs\nconst known = await sql(\"SELECT id FROM blocks WHERE id IN (?)\", allIds);\nawait fetchPost('/api/block/updateBlock', { ids: known.map(b => b.id), data });","handlingStrategy":"validation","validationCode":"const known = await fetchPost('/api/query/sql', { stmt: `SELECT id FROM blocks WHERE id IN (${ids.map(i => `'${i}'`).join(',')})` });\nconst knownIds = new Set(known.map(r => r.id));\nconst missing = ids.filter(i => !knownIds.has(i));\nif (missing.length && !allowMissing) throw new Error(`unknown block IDs: ${missing.join(', ')}`);","typeGuard":null,"tryCatchPattern":"try {\n  await fetchPost('/api/block/batchUpdateBlock', { ids, data });\n} catch (e) {\n  if (String(e?.msg).includes('block not found or its encrypted notebook is locked')) {\n    const fresh = await sql(\"SELECT id FROM blocks\");\n    return retryWith(ids.filter(i => freshSet.has(i))); // drop stale IDs and retry\n  }\n  throw e;\n}","preventionTips":["Pass the explicit notebook (box) parameter when you know it, so leases bind to the box","Re-resolve IDs from the blocks table right before batch operations, especially after sync","Drop or report unknown IDs individually instead of failing the whole batch","Avoid IDs sourced from history or exported snapshots"],"tags":["encryption","blocks","stale-id","siyuan"],"backgroundTag":"record-not-found","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"}