siyuan-note/siyuan · error

The attribute name can only contain lowercase English…

Error message

The attribute name can only contain lowercase English letters, digits, and hyphens, and must start with a lowercase English letter [%s]

What it means

Thrown by model.SetBlockAttrs (POST /api/attr/setBlockAttrs) when an attribute name fails isValidAttrName (kernel/model/blockial.go:459) after lowercasing. SiYuan stores block attributes as kramdown IAL keys, so a name must start with a lowercase English letter and contain only lowercase letters, digits, and hyphens; a leading 'c' gets extra 'custom-' prefix handling (the bare prefix 'custom-' alone is rejected). Uppercase input is tolerated because the name is lowercased before validation, but underscores, dots, spaces, and non-ASCII characters are not.

Solutions

  1. Rewrite the name to ^[a-z][a-z0-9-]*$, e.g. 'custom_my_field' -> 'custom-my-field'
  2. Prefix user-defined attributes with 'custom-' following SiYuan's convention for user block attributes
  3. If names are generated dynamically, sanitize them before the call: lowercase, strip invalid characters, ensure the first char is a-z

Example fix

// before
await fetch('/api/attr/setBlockAttrs', {method:'POST', body: JSON.stringify({id, attrs: {'custom_my_field': 'v', 'data-X.Y': 'v'}})});

// after
await fetch('/api/attr/setBlockAttrs', {method:'POST', body: JSON.stringify({id, attrs: {'custom-my-field': 'v', 'custom-x-y': 'v'}})});
Defensive patterns

Strategy: validation

Validate before calling

const attrNameOk = (n) => /^[a-z][a-z0-9-]*$/.test(n) && n !== 'custom-';
const attrs = {'custom-my-field': 'v'};
if (!Object.keys(attrs).every(attrNameOk)) throw new Error('invalid attribute name');

Prevention

When it happens

Trigger: POST /api/attr/setBlockAttrs with a name containing '_', '.', '/', a space, or CJK characters; a name starting with a digit or hyphen; an empty name; or exactly 'custom-'. The rejected block ID is appended to the localized message.

Common situations: Scripts that copy HTML data-* names with underscores ('custom_my_field'), generate attribute names from unfiltered user input, or pass camelCase/dotted keys ('user.Score') straight through.

Related errors


AI-assisted analysis of siyuan-note/siyuan@afa823b6b4 (2026-08-18). Data as JSON: /api/errors/457a3bc8eda9be67. Report an issue: GitHub.

Appendix: source

Thrown at kernel/model/blockial.go:387

			}
		}
	}
	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)
		// 转换为小写再验证属性名
		if !isValidAttrName(lowerName) {
			err = errors.New(Conf.Language(25) + " [" + node.ID + "]")
			return
		}
		if lowerName == "data-task" {
			err = errors.New(`setting or removing [data-task] attribute is not allowed via this interface. Please use "/api/block/updateTaskListItemMarker" or "/api/block/batchUpdateTaskListItemMarker" to update the task list item marker`)
			return
		}
		if DocSortModeAttr == lowerName {
			if ast.NodeDocument != node.Type || IsBoxDoc(boxID, node.ID) {
				err = fmt.Errorf("attribute [%s] is only supported on regular document roots", DocSortModeAttr)
				return
			}
			if "" != value {
				sortMode, parseErr := strconv.Atoi(value)
				if nil != parseErr || !IsValidDocSortMode(sortMode) {
					err = fmt.Errorf("invalid document sort mode [%s]", value)
					return
				}
				value = strconv.Itoa(sortMode)

View on GitHub (pinned to afa823b6b4)