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

  1. Reopen the notebook first: UI notebook panel or POST /api/notebook/openNotebook with the box id
  2. Wait for the open to complete before retrying the operation
  3. 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

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)