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

  1. 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)
  2. Pass the explicit notebook (box) ID in the request when known, so the lease is taken on the box instead of per-block resolution
  3. Filter batch item lists to IDs that currently exist in the index; re-query after sync completes
  4. 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

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


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)