siyuan-note/siyuan · error
notebook closed
Error message
notebook closed
What it means
Sentinel error model.ErrBoxClosed (kernel/model/tree.go:203). The notebook exists in configuration but is closed (unmounted), so operations that need a mounted notebook refuse to run - GetBoxByID-style lookups return it from mount.go:55, and import/doc tests assert it for closed notebooks.
Solutions
- Reopen the notebook first: UI notebook panel or POST /api/notebook/openNotebook with the box id
- Wait for the open to complete before retrying the operation
- Guard with errors.Is(err, model.ErrBoxClosed) and surface a 'notebook is closed' message instead of a generic failure
Defensive patterns
Strategy: try-catch
Validate before calling
// Pre-check state before heavy operations:
// GET /api/notebook/lsNotebooks -> notebooks[].closed
// If closed, POST /api/notebook/openNotebook {"notebook": boxID} first and wait for it to finish opening. Type guard
func isBoxClosed(err error) bool {
return errors.Is(err, model.ErrBoxClosed)
} Try / catch
if err := importIntoBox(boxID, ...); err != nil {
if errors.Is(err, model.ErrBoxClosed) {
// open the notebook, wait, then retry once
}
} Prevention
- Open the notebook and wait for it to finish mounting before batch imports
- Check the closed flag from lsNotebooks in job preconditions
- Distinguish closed (recoverable by opening) from not-found (stale id) in error handling
When it happens
Trigger: Closing a notebook in the UI (or via /api/notebook/closeNotebook) and then issuing doc/import/file operations that still reference its boxID; calling import into a notebook that is configured but never opened.
Common situations: Background jobs or plugins holding box IDs of notebooks the user closed to save startup time; batch import scripts not checking notebook state first.
Related errors
AI-assisted analysis of siyuan-note/siyuan@afa823b6b4 (2026-08-18).
Data as JSON: /api/errors/747f91c4c1683d02.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/model/tree.go:203
// 从绝对路径推导 box 内相对路径作为 AAD
relPath := filepath.ToSlash(strings.TrimPrefix(localPath, filepath.Join(util.DataDir, boxID)+string(os.PathSeparator)))
if data, err = DecryptFile(boxID, relPath, dek, data); err != nil {
logging.LogErrorf("decrypt tree [path=%s] failed: %s", localPath, err)
return
}
}
ret, err = dataparser.ParseJSONWithoutFix(data, luteEngine.ParseOptions)
if err != nil {
logging.LogErrorf("parse json to tree [%s] failed: %s", localPath, err)
return
}
return
}
var (
ErrBoxNotFound = errors.New("notebook not found")
ErrBoxClosed = errors.New("notebook closed")
ErrBlockNotFound = errors.New("block not found")
ErrTreeNotFound = errors.New("tree not found")
ErrIndexing = errors.New("indexing")
ErrBoxUnindexed = errors.New("notebook unindexed")
ErrInvalidID = errors.New("invalid id")
)
func LoadTreeByBlockIDWithReindex(id string) (ret *parse.Tree, err error) {
return LoadTreeByBlockIDWithReindexInBox(id, "")
}
// LoadTreeByBlockIDWithReindexInBox 与 LoadTreeByBlockIDWithReindex 一致,但按 boxID 路由 blocktree 查询。
func LoadTreeByBlockIDWithReindexInBox(id, boxID string) (ret *parse.Tree, err error) {
if "" == id {
logging.LogWarnf("block id is empty")
return nil, ErrTreeNotFound
}
View on GitHub (pinned to afa823b6b4)