siyuan-note/siyuan · error
invalid notebook ID
Error message
invalid notebook ID
What it means
hasEncryptedNotebookDeleteHistory checks whether any delete-operation history snapshot exists for a given notebook/box ID. It validates the boxID with ast.IsNodeIDPattern and returns the sentinel error 'invalid notebook ID' when the ID is not a well-formed 22-character SiYuan node ID. This guards the filesystem scan from being driven by malformed identifiers.
Source
Thrown at kernel/model/crypto.go:823
for _, boxEntry := range boxEntries {
if !boxEntry.IsDir() || !ast.IsNodeIDPattern(boxEntry.Name()) {
continue
}
encrypted, checkErr := isEncryptedHistoryBoxDir(filepath.Join(snapshotDir, boxEntry.Name()))
if checkErr != nil {
return nil, checkErr
}
if encrypted {
ret = append(ret, filepath.Join(snapshotDir, boxEntry.Name()))
}
}
}
return ret, nil
}
func hasEncryptedNotebookDeleteHistory(boxID string) (bool, error) {
if !ast.IsNodeIDPattern(boxID) {
return false, errors.New("invalid notebook ID")
}
entries, err := os.ReadDir(util.HistoryDir)
if err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, fmt.Errorf("read history dir failed: %w", err)
}
deleteSuffix := "-" + HistoryOpDelete
for _, entry := range entries {
if !entry.IsDir() || !strings.HasSuffix(entry.Name(), deleteSuffix) {
continue
}
boxDir := filepath.Join(util.HistoryDir, entry.Name(), boxID)
if !filelock.IsExist(boxDir) {
continue
}View on GitHub (pinned to 8641553a1f)
Solutions
- Print/log the offending boxID and compare with a valid node ID (22 chars matching IsNodeIDPattern)
- Fix the source of the ID: read it from the notebook's actual .siyuan/conf.json or the notebook list API instead of a hand-maintained config
- Guard callers with an IsNodeIDPattern check before invoking, and fail early with a clear message
- If the ID came from legacy data, re-derive it via treenoteook/notebook listing rather than trusting stored values
Example fix
// before
hasEncryptedNotebookDeleteHistory(notebook.Name)
// after
if !ast.IsNodeIDPattern(notebook.ID) { return fmt.Errorf("bad boxID %q", notebook.ID) }
hasEncryptedNotebookDeleteHistory(notebook.ID) Defensive patterns
Strategy: validation
Validate before calling
func validBoxID(id string) bool { return ast.IsNodeIDPattern(id) }
if !validBoxID(boxID) { return fmt.Errorf("refusing: %q is not a node ID", boxID) } Type guard
func isNodeID(s string) bool { return len(s) == 22 && ast.IsNodeIDPattern(s) } Try / catch
ok, err := hasEncryptedNotebookDeleteHistory(boxID)
if err != nil && strings.Contains(err.Error(), "invalid notebook ID") {
return fmt.Errorf("caller passed bad boxID %q", boxID)
} Prevention
- Always source boxIDs from the notebook list / .siyuan/conf.json, never from names
- Never truncate or reformat IDs when persisting them in configs
- Add an IsNodeIDPattern assertion at call sites in scripts
- Keep IDs as opaque strings — no trimming, casing, or extension mangling
When it happens
Trigger: Calling hasEncryptedNotebookDeleteHistory (directly or via recoverMasterPasswordMigration) with an empty string, a truncated ID, a path fragment, or any string that is not a valid node-ID pattern — e.g. after a config read returns a placeholder boxID, or a test/caller passes a notebook name instead of its ID.
Common situations: Passing a notebook name/slug from configuration instead of the boxID; trimming or truncating IDs when persisting them; older or hand-edited config storing the wrong identifier; tests probing invalid-input behavior.
Understand the failure class
Background: "invalid id" errors: invalid identifier format — why libraries reject IDs before lookup, and how to fix them — this error's family across 37 libraries.
Related errors
- invalid frontend capability ID: %s
- invalid AI editor action ID
- invalid AI editor action data
- duplicate AI editor action ID [%s]
- invalid appearance ID
AI-assisted analysis of siyuan-note/siyuan@8641553a1f (2026-09-11).
Data as JSON: /api/errors/01c040afafe2af93.
Report an issue: GitHub.