siyuan-note/siyuan · error
block not found or its encrypted notebook is locked
Error message
block not found or its encrypted notebook is locked
What it means
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.
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)
Example fix
// before: one stale ID fails the whole batch
await fetchPost('/api/block/updateBlock', { ids: allIds, data });
// after: keep only resolvable IDs
const known = await sql("SELECT id FROM blocks WHERE id IN (?)", allIds);
await fetchPost('/api/block/updateBlock', { ids: known.map(b => b.id), data }); Defensive patterns
Strategy: validation
Validate before calling
const known = await fetchPost('/api/query/sql', { stmt: `SELECT id FROM blocks WHERE id IN (${ids.map(i => `'${i}'`).join(',')})` });
const knownIds = new Set(known.map(r => r.id));
const missing = ids.filter(i => !knownIds.has(i));
if (missing.length && !allowMissing) throw new Error(`unknown block IDs: ${missing.join(', ')}`); Try / catch
try {
await fetchPost('/api/block/batchUpdateBlock', { ids, data });
} catch (e) {
if (String(e?.msg).includes('block not found or its encrypted notebook is locked')) {
const fresh = await sql("SELECT id FROM blocks");
return retryWith(ids.filter(i => freshSet.has(i))); // drop stale IDs and retry
}
throw e;
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: Record Not Found Errors: "not found", RecordNotFound, and "was not found" — what they mean and how to fix them — this error's family across 28 libraries.
Related errors
- block not found
- block is not a task list item
- Conf.Language(388) with escaped relative path…
- Content block with id
- encrypted notebook is locked, please unlock it first
AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19).
Data as JSON: /api/errors/fb08fa9d68651240.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/api/box_lease.go:71
}
// holdEncryptedBlockRequests 按块的实际归属取得响应租约,覆盖省略笔记本参数的通用和批量读取。
func holdEncryptedBlockRequests(c *gin.Context, boxID string, ids []string, allowMissing bool) error {
if boxID != "" {
return holdEncryptedBoxRequest(c, boxID)
}
boxIDSet := map[string]struct{}{}
for _, id := range ids {
if !ast.IsNodeIDPattern(id) {
continue
}
block := treenode.GetBlockTree(id)
if block == nil {
if allowMissing {
continue
}
// 查不到的块直接拒绝,防止随后解锁的笔记本在第二次查找时被无租约访问。
return errors.New("block not found or its encrypted notebook is locked")
}
if model.IsEncryptedBox(block.BoxID) {
boxIDSet[block.BoxID] = struct{}{}
}
}
boxIDs := make([]string, 0, len(boxIDSet))
for id := range boxIDSet {
boxIDs = append(boxIDs, id)
}
sort.Strings(boxIDs)
for _, id := range boxIDs {
if err := holdEncryptedBoxRequest(c, id); err != nil {
return err
}
}
return nil
}
View on GitHub (pinned to 9f775e8a12)