siyuan-note/siyuan · error

[ ] is not a box id

Error message

[%s] is not a box id

What it means

When a boxID was supplied (from the argument or the ?box= query), GetAssetAbsPathInBox validates it against ast.IsNodeIDPattern, the canonical notebook-ID format (20-char timestamp-random pattern). This error is returned when the box ID does not match that pattern, preventing lookups into a non-existent or malformed notebook directory that could double as a traversal vector.

Solutions

  1. Pass the notebook's real 20-character ID (available from Conf.Box names/IDs or listNotebooks API), not its name
  2. Trim whitespace and verify the ID against ast.IsNodeIDPattern before calling
  3. Strip or fix the ?box= query parameter if the malformed ID comes from an asset URL

Example fix

// before: name instead of ID
model.GetAssetAbsPathInBox("assets/img.png", "My Notebook")
// after: resolve the real box ID first
boxID := getBoxIDByName("My Notebook")
model.GetAssetAbsPathInBox("assets/img.png", boxID)
Defensive patterns

Strategy: validation

Validate before calling

import "github.com/88250/lute/ast"
if boxID != "" && !ast.IsNodeIDPattern(boxID) {
	return fmt.Errorf("invalid box id %q", boxID)
}

Type guard

func validBoxID(id string) bool { return id == "" || ast.IsNodeIDPattern(id) }

Try / catch

abs, err := model.GetAssetAbsPathInBox(ref, boxID)
if err != nil && strings.Contains(err.Error(), "is not a box id") {
	// resolve the real box ID from the notebook list and retry
	for _, b := range model.Conf.Boxes { if b.Name == wantedName { abs, err = model.GetAssetAbsPathInBox(ref, b.ID) } }
}

Prevention

When it happens

Trigger: Calling GetAssetAbsPathInBox("assets/img.png", boxID) where boxID is a notebook name, a title, an empty-but-non-nil string with whitespace, a truncated ID, or a value like "../x" embedded in the ?box= query parameter.

Common situations: Passing a human-readable notebook name instead of its ID; building the box ID from user input or a plugin config value; a corrupted stored link whose ?box= query was mangled; extracting the wrong capture group from a URL regex.

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


AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19). Data as JSON: /api/errors/6544da5e98e52067. Report an issue: GitHub.

Appendix: source

Thrown at kernel/model/assets.go:1230

// GetAssetAbsPathInBox 在指定 box 内解析资源绝对路径,不进行全局遍历。
// relativePath 必须以 assets/ 前缀开头,boxID 为空且路径没有 box 查询参数时只解析普通/全局资源,不遍历加密 box。
// 加密 box 直接从 <boxID>/assets/ 查找,不依赖后缀匹配。
func GetAssetAbsPathInBox(relativePath, boxID string) (string, error) {
	var err error
	relativePath, boxID, err = assetPathAndBox(relativePath, boxID)
	if err != nil {
		return "", err
	}
	relativePath = path.Clean(relativePath)
	if relativePath == "." || strings.HasPrefix(relativePath, "../") || relativePath == ".." || path.IsAbs(relativePath) {
		return "", fmt.Errorf("[%s] is not an asset path", relativePath)
	}
	if !strings.HasPrefix(relativePath, "assets/") {
		return "", fmt.Errorf("[%s] is not an asset path (must start with assets/)", relativePath)
	}
	if boxID != "" && !ast.IsNodeIDPattern(boxID) {
		return "", fmt.Errorf("[%s] is not a box id", boxID)
	}

	if boxID == "" {
		return GetAssetAbsPathWithOpt(relativePath, false)
	}

	p := filepath.Join(util.DataDir, boxID, relativePath)
	if gulu.File.IsExist(p) {
		if !gulu.File.IsSubPath(util.WorkspaceDir, p) {
			return "", fmt.Errorf("[%s] is not sub path of workspace", p)
		}
		// 解析符号链接/目录联接,防止软链接跳出资产根目录
		if realP, evalErr := filepath.EvalSymlinks(p); evalErr == nil && realP != p {
			if !gulu.File.IsSubPath(util.WorkspaceDir, realP) {
				return "", fmt.Errorf("symlink [%s] resolves outside workspace: [%s]", p, realP)
			}
			// 验证解析后的路径仍在 <boxID>/assets/ 或全局 data/assets/ 下
			expectedPrefix := filepath.Join(util.DataDir, "assets")

View on GitHub (pinned to 9f775e8a12)