siyuan-note/siyuan · error
boxID mismatch: param=%s, path=%s
Error message
boxID mismatch: param=%s, path=%s
What it means
writeAssetFile cross-validates the caller-supplied boxID against the box ID reverse-extracted from the target asset write path. If both are non-empty and point to different notebooks, the write is rejected to prevent cross-box writes (an asset intended for one box silently landing in another notebook's assets directory).
Source
Thrown at kernel/model/upload.go:563
_ = os.MkdirAll(assets, 0755)
return
}
assets = filepath.Join(util.DataDir, "assets")
}
}
return
}
// writeAssetFile 把 src 的内容写入 writePath。从 writePath 反查真实 boxID 决定是否加密——
// 不轻信传入的 boxID(调用方可能未传,或 assetsDirPath 指向加密笔记本但 id 为空)。
// 加密笔记本必须已解锁(DEK 在内存)才写入;加密但未解锁返回错误(fail-closed,避免明文落盘)。
// 非加密笔记本按 reader 直接写(走 filelock.WriteFileByReader 原路径,保留锁语义)。
func writeAssetFile(writePath string, src io.Reader, boxID, originalName string) (err error) {
// 从 writePath 反查真实 boxID,与传入 boxID 交叉校验
pathBoxID := ExtractBoxIDFromAssetsPath(writePath)
// 传入 boxID 与路径 box 都非空但不一致:路径指向另一个 box,拒绝(防跨 box 写入)
if boxID != "" && pathBoxID != "" && boxID != pathBoxID {
return fmt.Errorf("boxID mismatch: param=%s, path=%s", boxID, pathBoxID)
}
// 路径不在 box 下但传入的是加密 box:加密内容只能写 box 内,拒绝写全局 assets
if pathBoxID == "" && boxID != "" && IsEncryptedBox(boxID) {
return fmt.Errorf("encrypted box asset must be written inside the box directory, got global path: %s", writePath)
}
actualBoxID := pathBoxID
if actualBoxID == "" {
actualBoxID = boxID // 路径不在 box 下(如全局 assets),回退传入值
}
if actualBoxID != "" && IsEncryptedBox(actualBoxID) {
HoldBoxReadLock(actualBoxID)
defer ReleaseBoxReadLock(actualBoxID)
dek, dekErr := GetDEKIfUnlocked(actualBoxID)
if dekErr != nil {
// 加密笔记本未解锁:拒绝写入,避免明文落盘(深度防御,见 issue #18034)
return dekErr
}
// 已解锁的加密 box:全读 → 加密 → 落盘View on GitHub (pinned to 8641553a1f)
Solutions
- Check which notebook the writePath actually belongs to (ExtractBoxIDFromAssetsPath) and pass that box ID as the boxID parameter, or pass "" to defer entirely to the path
- If the asset really belongs to the boxID given, rebuild writePath under that box's assets directory before calling
- If paths and box IDs are derived dynamically, log both values and fix the upstream derivation so they stay consistent
Example fix
// before err := model.writeAssetFile(filepath.Join(otherBoxAssetsPath, name), src, boxID, name) // after pathBoxID := model.ExtractBoxIDFromAssetsPath(filepath.Join(otherBoxAssetsPath, name)) err := model.writeAssetFile(filepath.Join(otherBoxAssetsPath, name), src, pathBoxID, name)
Defensive patterns
Strategy: validation
Validate before calling
func canWriteAsset(writePath, boxID string) error {
pathBoxID := model.ExtractBoxIDFromAssetsPath(writePath)
if boxID != "" && pathBoxID != "" && boxID != pathBoxID {
return fmt.Errorf("asset path %s belongs to box %q, not %q", writePath, pathBoxID, boxID)
}
return nil
} Type guard
func boxMatchesPath(boxID, writePath string) bool {
pathBoxID := model.ExtractBoxIDFromAssetsPath(writePath)
return boxID == "" || pathBoxID == "" || boxID == pathBoxID
} Try / catch
if err := model.writeAssetFile(writePath, src, boxID, name); err != nil {
if strings.HasPrefix(err.Error(), "boxID mismatch") {
// recompute the correct box ID from writePath and retry once
pathBoxID := model.ExtractBoxIDFromAssetsPath(writePath)
err = model.writeAssetFile(writePath, src, pathBoxID, name)
}
return err
} Prevention
- Always derive the box ID from the resolved write path instead of passing a caller-cached value
- Pass "" for boxID when the target path is authoritative and you do not need cross-validation
- Add a unit test asserting the box ID and asset path come from the same notebook for every upload entry point
When it happens
Trigger: Calling writeAssetFile (directly or via Upload, InsertAssetBytes, insertLocalAssets, netAssets2LocalAssets0, importSYAssets) with a writePath under one notebook's assets directory while passing a different, non-empty boxID parameter.
Common situations: Plugin or API callers that cache a notebook ID from a previous operation but resolve the asset path from the currently-focused document in a different notebook; batch upload loops that reuse a stale boxID; code that hardcodes a default boxID while the assets path resolves to data/assets or another box.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- encrypted box asset must be written inside the box directory
- load tree failed: %s
- wrong layout type
- filter nesting depth exceeds the maximum allowed
- The top-level notebook document cannot be removed or moved
AI-assisted analysis of siyuan-note/siyuan@8641553a1f (2026-09-11).
Data as JSON: /api/errors/bd78e10aae4ccca3.
Report an issue: GitHub.