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
- Rewrite the name to ^[a-z][a-z0-9-]*$, e.g. 'custom_my_field' -> 'custom-my-field'
- Prefix user-defined attributes with 'custom-' following SiYuan's convention for user block attributes
- 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
- Normalize generated names: lowercase, replace '_' and '.' with '-', ensure the first char is a-z
- Keep user-defined attributes under the 'custom-' prefix
- Remember names are lowercased server-side; don't rely on case to distinguish two attributes
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 editor action must not be empty
- attribute [ ] is only supported on tab containers
- block [ ] type is locked: expected , got
- block swap requires two non-document blocks
- Bookmark cannot be empty
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)