{"record":{"id":"9ab49abfc0b9ff62","repo":"siyuan-note/siyuan","slug":"box-mismatch-caller-specified-s-but-url-has","errorCode":null,"errorMessage":"box mismatch: caller specified [%s] but URL has [%s]","messagePattern":"box mismatch: caller specified \\[(.+?)\\] but URL has \\[(.+?)\\]","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/assets.go","lineNumber":1059,"sourceCode":"\t}\n\tif parsed.IsAbs() || parsed.Host != \"\" || !strings.HasPrefix(strings.TrimPrefix(parsed.Path, \"/\"), \"assets/\") {\n\t\treturn false\n\t}\n\text := strings.ToLower(path.Ext(parsed.Path))\n\treturn ext == \".html\" || ext == \".htm\"\n}\n\nfunc assetPathAndBox(relativePath, defaultBoxID string) (cleanPath, boxID string, err error) {\n\trelativePath = strings.TrimSpace(relativePath)\n\tboxID = defaultBoxID\n\tif idx := strings.Index(relativePath, \"?\"); idx >= 0 {\n\t\tquery := relativePath[idx+1:]\n\t\trelativePath = relativePath[:idx]\n\t\tif values, parseErr := url.ParseQuery(query); parseErr == nil {\n\t\t\tif queryBoxID := strings.TrimSpace(values.Get(\"box\")); queryBoxID != \"\" {\n\t\t\t\tif defaultBoxID != \"\" && defaultBoxID != queryBoxID {\n\t\t\t\t\t// 调用方指定了 boxID 但 URL 里是另一个 box：拒绝，防止解析到错误 box\n\t\t\t\t\terr = fmt.Errorf(\"box mismatch: caller specified [%s] but URL has [%s]\", defaultBoxID, queryBoxID)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tboxID = queryBoxID\n\t\t\t}\n\t\t}\n\t}\n\tcleanPath = filepath.ToSlash(relativePath)\n\treturn\n}\n\n// GetAssetAbsPathInBox 在指定 box 内解析资源绝对路径，不进行全局遍历。\n// relativePath 必须以 assets/ 前缀开头，boxID 为空且路径没有 box 查询参数时只解析普通/全局资源，不遍历加密 box。\n// 加密 box 直接从 <boxID>/assets/ 查找，不依赖后缀匹配。\nfunc GetAssetAbsPathInBox(relativePath, boxID string) (string, error) {\n\tvar err error\n\trelativePath, boxID, err = assetPathAndBox(relativePath, boxID)\n\tif err != nil {\n\t\treturn \"\", err","sourceCodeStart":1041,"sourceCodeEnd":1077,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/model/assets.go#L1041-L1077","documentation":"Thrown by assetPathAndBox (kernel/model/assets.go:1059, used by GetAssetAbsPathInBox) when the caller supplies a non-empty defaultBoxID AND the asset relativePath contains a `?box=<id>` query parameter whose value differs from the caller's box. It prevents a URL from silently redirecting asset resolution to a different notebook than the caller intended.","triggerScenarios":"Calling model.GetAssetAbsPathInBox(relativePath, boxID) where relativePath is something like `assets/file.png?box=20250101000000-aaaaaaa` and the boxID argument is a different valid notebook ID. This happens when a document from notebook A embeds an asset carrying notebook B's box query param and the caller (e.g. export.go, mcp/tools/image.go) scopes resolution to A.","commonSituations":"Cross-notebook copy/paste where the asset link retained the source notebook's `?box=` param; inconsistent caller code that mixes the block's box with a URL query built elsewhere; manually crafted asset URLs.","solutions":["Make the caller consistent: pass the same boxID that appears in the URL, or strip the `?box=` query before calling.","If the URL's box is authoritative, call GetAssetAbsPathInBox with an empty boxID and let the query param drive resolution.","If the caller's box is authoritative, strip the query string via model.AssetPathWithoutQuery before resolving.","Audit the document/source that produced the mismatched box param — it may indicate a copy/paste bug."],"exampleFix":"// before\nabs, err := model.GetAssetAbsPathInBox(assetRef, tree.Box)\n// where assetRef = \"assets/img.png?box=20240101000000-aaaaaaa\" but tree.Box = \"20250202000000-bbbbbbb\"\n\n// after — URL box is authoritative\nabs, err := model.GetAssetAbsPathInBox(assetRef, \"\")\n// or — caller box is authoritative\nabs, err := model.GetAssetAbsPathInBox(model.AssetPathWithoutQuery(assetRef), tree.Box)","handlingStrategy":"validation","validationCode":"// Make box sourcing unambiguous before calling GetAssetAbsPathInBox.\n// Option A: let the URL drive — pass empty box.\n// Option B: let the caller drive — strip the query.\nif callerBox != \"\" {\n    clean := model.AssetPathWithoutQuery(assetRef)\n    return model.GetAssetAbsPathInBox(clean, callerBox)\n}\nreturn model.GetAssetAbsPathInBox(assetRef, \"\")","typeGuard":null,"tryCatchPattern":"if _, err := model.GetAssetAbsPathInBox(ref, box); err != nil && strings.Contains(err.Error(), \"box mismatch\") {\n    // reconcile caller box vs URL box; pick one authoritative source and retry once\n}","preventionTips":["Decide upfront whether the caller or the URL is authoritative for the box and be consistent.","When copying/pasting across notebooks, rewrite or strip the ?box= param to match the destination.","Avoid building asset URLs by concatenating a stale ?box= param."],"tags":["assets","notebooks","validation","query-params","box-id"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}