siyuan-note/siyuan · error

parse document history failed

Error message

parse document history failed

What it means

A defensive check right after parsing: if loadTreeByData0 returns no error but also a nil tree, the snapshot contains no usable document tree. The kernel raises this distinct message so callers get a clear failure instead of a nil-pointer panic later in the rollback path.

Source

Thrown at kernel/model/history.go:367

			return
		}
		var decErr error
		// 历史路径格式:<historyDir>/<datePrefix>/<boxID>/<relativePath>
		// 用原始 boxID 解密(getRollbackBox 可能创建了新 box,密文仍属于原加密 box)
		filePath := parts[2]
		srcData, decErr = DecryptFile(origBoxID, filePath, dek, srcData)
		if decErr != nil {
			logging.LogErrorf("decrypt history [%s] failed: %s", srcPath, decErr)
			err = decErr
			return
		}
	}
	tree, parseErr := loadTreeByData0(srcData)
	if parseErr != nil {
		return fmt.Errorf("parse document history failed: %w", parseErr)
	}
	if tree == nil {
		return errors.New("parse document history failed")
	}
	if encrypted && tree.Root.ID+".sy" != filepath.Base(historyPath) {
		return errors.New("encrypted document history root ID does not match its filename")
	}
	if nil != tree {
		historyDir := filepath.Join(util.HistoryDir, parts[0])

		avNodes := tree.Root.ChildrenByType(ast.NodeAttributeView)
		for _, avNode := range avNodes {
			srcAvPath := filepath.Join(historyDir, "storage", "av", avNode.AttributeViewID+".json")
			// 加密笔记本的 AV 定义在笔记本级目录
			destAvPath := filepath.Join(util.DataDir, "storage", "av", avNode.AttributeViewID+".json")
			if IsEncryptedBox(boxID) {
				// 历史目录里 AV 也可能在 boxID 子目录下
				boxSrcAvPath := filepath.Join(historyDir, boxID, "storage", "av", avNode.AttributeViewID+".json")
				if !gulu.File.IsExist(boxSrcAvPath) {
					return fmt.Errorf("encrypted attribute view history is missing notebook context [%s]", avNode.AttributeViewID)
				}

View on GitHub (pinned to 8641553a1f)

Solutions

  1. Check the history file is non-empty and contains a document node; delete empty snapshots
  2. Roll back to a different history entry for the same document
  3. Regenerate history by editing and re-saving the document, then use the new snapshot

Example fix

// before
fetchPost("/api/history/rollbackDocHistory", {historyPath: emptySnapshotPath})
// after: skip empty snapshots when choosing an entry
const entries = historyList.filter(e => e.size > 0)
await fetchPost("/api/history/rollbackDocHistory", {historyPath: entries[0].path})
Defensive patterns

Strategy: validation

Validate before calling

const raw = await fetchPost("/api/file/getFile", {path: historyPath})
if (!raw || !raw.trim()) throw new Error("empty history snapshot, choose another entry")

Type guard

function isNonEmptySnapshot(data) {
  return typeof data === "string" && data.trim().length > 0
}

Try / catch

try {
  await fetchPost("/api/history/rollbackDocHistory", {historyPath})
} catch (e) {
  if (String(e.msg || e) === "parse document history failed") {
    // fall back to the next non-empty history entry
  }
}

Prevention

When it happens

Trigger: rollbackDocHistory loads a history snapshot whose parsed result is nil — e.g. an empty file (0 bytes) that parses without error, or a loader edge case returning (nil, nil) for degenerate input.

Common situations: Zero-byte or whitespace-only history .sy files created by interrupted writes; snapshots of documents that were empty at snapshot time in a format the loader cannot represent; stale history entries left after a failed indexing run.

Understand the failure class

Background: "must not be empty", "cannot be empty" — required-field validation errors across open-source libraries — this error's family across 41 libraries.

Related errors


AI-assisted analysis of siyuan-note/siyuan@8641553a1f (2026-09-11). Data as JSON: /api/errors/80d1f877321faa96. Report an issue: GitHub.