yewstack/yew · error

could not remove attribute

Error message

could not remove attribute

What it means

AttributeWriter::remove (packages/yew/src/dom_bundle/btag/attributes.rs:209) calls Element::remove_attribute during diffing when an attribute disappears from a VTag, and unwraps with .expect("could not remove attribute"). remove_attribute throws InvalidCharacterError for names that are not valid attribute names — the same rule as set_attribute. Since removal uses the previously-set name, this panic means an invalid name was stored in the attribute set earlier and only blows up when the attribute is later removed during reconciliation.

Source

Thrown at packages/yew/src/dom_bundle/btag/attributes.rs:209

    #[cfg(feature = "hydration")]
    fn hydrate_set(el: &Element, key: &str, value: &AttributeOrProperty) {
        match value {
            AttributeOrProperty::Attribute(value) => {
                debug_assert_eq!(
                    el.get_attribute(key).as_deref(),
                    Some(value.as_ref()),
                    "attribute `{key}` does not match the server-rendered value during hydration",
                );
            }
            AttributeOrProperty::Property(_) => Self::set(el, key, value),
        }
    }

    fn remove(el: &Element, key: &str, old_value: &AttributeOrProperty) {
        match old_value {
            AttributeOrProperty::Attribute(_) => el
                .remove_attribute(intern(key))
                .expect("could not remove attribute"),
            AttributeOrProperty::Property(_) => {
                let key = JsValue::from_str(key);
                js_sys::Reflect::set(el.as_ref(), &key, &JsValue::UNDEFINED)
                    .expect("could not remove property");
            }
        }
    }
}

impl Apply for Attributes {
    type Bundle = Self;
    type Element = Element;

    fn apply(self, _root: &BSubtree, el: &Element) -> Self {
        #[expect(deprecated)]
        match &self {
            Self::Static(arr) => {
                for (k, v) in arr.iter() {

View on GitHub (pinned to 0e4a05472f)

Solutions

  1. Validate attribute names at the boundary where dynamic names are introduced (reject empty strings and space/quote/'<'/'>'/'/'/'=' characters) so they never reach the VTag
  2. Log attribute keys when inserting into Attributes to catch malformed names at creation time, not at removal time
  3. Where possible, use static html! attribute names and pass values, not names, dynamically

Example fix

// before
let attrs: Vec<(AttrValue, AttrValue)> = deserialize_from_config();

// after
fn is_valid_attr_name(name: &str) -> bool {
    !name.is_empty() && !name.chars().any(|c| matches!(c, ' ' | '"' | '\'' | '<' | '>' | '/' | '=' ) || c.is_control())
}
let attrs: Vec<(AttrValue, AttrValue)> = deserialize_from_config()
    .into_iter().filter(|(k, _)| is_valid_attr_name(k)).collect();
Defensive patterns

Strategy: validation

Validate before calling

fn is_valid_attr_name(name: &str) -> bool {
    !name.is_empty()
        && !name.chars().any(|c| matches!(c, ' ' | '"' | '\'' | '<' | '>' | '/' | '=') || c.is_control())
}

// filter at ingestion so bad names never enter the attribute set (and never reach removal)
let safe: Vec<_> = incoming_attrs.into_iter().filter(|(k, _)| is_valid_attr_name(k)).collect();

Type guard

fn safe_attr_name(name: &str) -> Option<&str> { is_valid_attr_name(name).then_some(name) }

Prevention

When it happens

Trigger: A dynamically built invalid attribute name was applied earlier (or reached the VTag some other way) and a subsequent render removes that attribute, invoking remove_attribute with the bad name.

Common situations: Spread/rest-props components passing through a malformed key that survives until the element re-renders without it; attribute maps loaded from external data (JSON config, CMS output) containing names with spaces or empty strings; keys assembled by string concatenation with empty segments.

Related errors


AI-assisted analysis of yewstack/yew@0e4a05472f (2026-08-22). Data as JSON: /api/errors/ecd7f18451c00c3d. Report an issue: GitHub.