{"record":{"id":"80d1f877321faa96","repo":"siyuan-note/siyuan","slug":"parse-document-history-failed","errorCode":null,"errorMessage":"parse document history failed","messagePattern":"parse document history failed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/history.go","lineNumber":367,"sourceCode":"\t\t\treturn\n\t\t}\n\t\tvar decErr error\n\t\t// 历史路径格式：<historyDir>/<datePrefix>/<boxID>/<relativePath>\n\t\t// 用原始 boxID 解密（getRollbackBox 可能创建了新 box，密文仍属于原加密 box）\n\t\tfilePath := parts[2]\n\t\tsrcData, decErr = DecryptFile(origBoxID, filePath, dek, srcData)\n\t\tif decErr != nil {\n\t\t\tlogging.LogErrorf(\"decrypt history [%s] failed: %s\", srcPath, decErr)\n\t\t\terr = decErr\n\t\t\treturn\n\t\t}\n\t}\n\ttree, parseErr := loadTreeByData0(srcData)\n\tif parseErr != nil {\n\t\treturn fmt.Errorf(\"parse document history failed: %w\", parseErr)\n\t}\n\tif tree == nil {\n\t\treturn errors.New(\"parse document history failed\")\n\t}\n\tif encrypted && tree.Root.ID+\".sy\" != filepath.Base(historyPath) {\n\t\treturn errors.New(\"encrypted document history root ID does not match its filename\")\n\t}\n\tif nil != tree {\n\t\thistoryDir := filepath.Join(util.HistoryDir, parts[0])\n\n\t\tavNodes := tree.Root.ChildrenByType(ast.NodeAttributeView)\n\t\tfor _, avNode := range avNodes {\n\t\t\tsrcAvPath := filepath.Join(historyDir, \"storage\", \"av\", avNode.AttributeViewID+\".json\")\n\t\t\t// 加密笔记本的 AV 定义在笔记本级目录\n\t\t\tdestAvPath := filepath.Join(util.DataDir, \"storage\", \"av\", avNode.AttributeViewID+\".json\")\n\t\t\tif IsEncryptedBox(boxID) {\n\t\t\t\t// 历史目录里 AV 也可能在 boxID 子目录下\n\t\t\t\tboxSrcAvPath := filepath.Join(historyDir, boxID, \"storage\", \"av\", avNode.AttributeViewID+\".json\")\n\t\t\t\tif !gulu.File.IsExist(boxSrcAvPath) {\n\t\t\t\t\treturn fmt.Errorf(\"encrypted attribute view history is missing notebook context [%s]\", avNode.AttributeViewID)\n\t\t\t\t}","sourceCodeStart":349,"sourceCodeEnd":385,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/8641553a1f07374001902d3ce773285db1292b2d/kernel/model/history.go#L349-L385","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Check the history file is non-empty and contains a document node; delete empty snapshots","Roll back to a different history entry for the same document","Regenerate history by editing and re-saving the document, then use the new snapshot"],"exampleFix":"// before\nfetchPost(\"/api/history/rollbackDocHistory\", {historyPath: emptySnapshotPath})\n// after: skip empty snapshots when choosing an entry\nconst entries = historyList.filter(e => e.size > 0)\nawait fetchPost(\"/api/history/rollbackDocHistory\", {historyPath: entries[0].path})","handlingStrategy":"validation","validationCode":"const raw = await fetchPost(\"/api/file/getFile\", {path: historyPath})\nif (!raw || !raw.trim()) throw new Error(\"empty history snapshot, choose another entry\")","typeGuard":"function isNonEmptySnapshot(data) {\n  return typeof data === \"string\" && data.trim().length > 0\n}","tryCatchPattern":"try {\n  await fetchPost(\"/api/history/rollbackDocHistory\", {historyPath})\n} catch (e) {\n  if (String(e.msg || e) === \"parse document history failed\") {\n    // fall back to the next non-empty history entry\n  }\n}","preventionTips":["Filter history entries by size > 0 before offering rollback","Delete empty snapshots produced by interrupted writes","Verify snapshots after crash recovery"],"tags":["go","history","parse","nil"],"backgroundTag":"empty-required-field","analyzedSha":"8641553a1f07374001902d3ce773285db1292b2d","analyzedAt":"2026-09-11T16:08:28.414Z","contentChangedAt":"2026-09-11T16:08:28.414Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}