siyuan-note/siyuan · error

Conf.Language(390)

Error message

Conf.Language(390)

What it means

setNodeAttrs0 rejects setting the 'bookmark' or 'tags' attribute (case-insensitive) on a block inside an encrypted notebook, returning Conf.Language(390): 'Encrypted notebooks do not support bookmarks and tags, please remove the %s attribute of this block first'. Encrypted notebooks are isolated islands without the global SQLite aggregation these attributes depend on.

Solutions

  1. Remove the 'bookmark'/'tags' keys from the attribute map before calling when the target box is encrypted
  2. Keep bookmark/tag workflows out of encrypted notebooks; use unencrypted notebooks for those blocks
  3. Partition your batch by notebook: apply full attrs to normal notebooks, filtered attrs to encrypted ones
  4. Check the box type first (e.g. via workspace notebook list / IsEncryptedBox semantics) and warn the user instead of failing

Example fix

// before
await fetchPost('/api/attr/setBlockAttrs', {id, attrs: {bookmark: 'b', custom: 'x'}});
// after
if (isEncryptedBox(boxOf(id))) delete attrs.bookmark, delete attrs.tags;
await fetchPost('/api/attr/setBlockAttrs', {id, attrs});
Defensive patterns

Strategy: validation

Validate before calling

const FORBIDDEN = ['bookmark', 'tags'];
for (const k of Object.keys(attrs)) {
  if (FORBIDDEN.includes(k.toLowerCase()) && isEncryptedBox(boxId)) delete attrs[k];
}

Type guard

null

Try / catch

try { await setBlockAttrs(id, attrs); } catch (e) { if (/bookmark|tags/.test(String(e))) { delete forbiddenKeys(attrs); await setBlockAttrs(id, attrs); } else throw e; }

Prevention

When it happens

Trigger: POST /api/attr/setBlockAttrs (also batchSetBlockAttrs, setNodeAttrs with tx, or any path like updateBoundBlockAvsAttribute / doRemoveAttrViewView that funnels into setNodeAttrs0) with nameValues containing key 'bookmark' or 'tags' (any casing) while the block's box is an encrypted box (IsBoxEncrypted(boxID)).

Common situations: Plugin or script bulk-setting custom+standard attributes across all notebooks, some encrypted; drag/drop or sync flows copying attrs into encrypted notebooks; attribute-view bound blocks trying to write tags.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


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

Appendix: source

Thrown at kernel/model/blockial.go:370

	if oldAttrs[DocSortModeAttr] != newAttrs[DocSortModeAttr] && ast.NodeDocument == node.Type {
		cache.RemoveDocIALInBox(tree.Path, tree.Box)
		cache.PutDocIALInBox(tree.Path, tree.Box, newAttrs)
	}
	pushBlockAttrs(oldAttrs, node)
	pushDocSortModeChanged(oldAttrs, node, tree)
	return
}

func setNodeAttrs0(node *ast.Node, nameValues map[string]string, boxID string) (oldAttrs map[string]string, err error) {
	if err = treenode.ValidateTabsAttrs(node, nameValues); nil != err {
		return
	}
	// 加密笔记本不支持书签和标签(依赖全局 SQLite 聚合,加密笔记本是孤岛)
	if IsEncryptedBox(boxID) && boxID != "" {
		for name := range nameValues {
			switch strings.ToLower(name) {
			case "bookmark", "tags":
				err = errors.New(fmt.Sprintf(Conf.Language(390), name))
				return
			}
		}
	}
	oldAttrs = parse.IAL2Map(node.KramdownIAL)
	newAttrsUnEsc := parse.IAL2MapUnEsc(node.KramdownIAL)
	for name := range nameValues {
		if "fold" == strings.ToLower(name) {
			delete(newAttrsUnEsc, "heading-fold")
			break
		}
	}

	for name, value := range nameValues {
		value = util.RemoveInvalidRetainCtrl(value)
		value = strings.TrimSpace(value)
		lowerName := strings.ToLower(name)
		// 转换为小写再验证属性名

View on GitHub (pinned to 9f775e8a12)