{"record":{"id":"ecd7f18451c00c3d","repo":"yewstack/yew","slug":"could-not-remove-attribute","errorCode":null,"errorMessage":"could not remove attribute","messagePattern":"could not remove attribute","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/yew/src/dom_bundle/btag/attributes.rs","lineNumber":209,"sourceCode":"    #[cfg(feature = \"hydration\")]\n    fn hydrate_set(el: &Element, key: &str, value: &AttributeOrProperty) {\n        match value {\n            AttributeOrProperty::Attribute(value) => {\n                debug_assert_eq!(\n                    el.get_attribute(key).as_deref(),\n                    Some(value.as_ref()),\n                    \"attribute `{key}` does not match the server-rendered value during hydration\",\n                );\n            }\n            AttributeOrProperty::Property(_) => Self::set(el, key, value),\n        }\n    }\n\n    fn remove(el: &Element, key: &str, old_value: &AttributeOrProperty) {\n        match old_value {\n            AttributeOrProperty::Attribute(_) => el\n                .remove_attribute(intern(key))\n                .expect(\"could not remove attribute\"),\n            AttributeOrProperty::Property(_) => {\n                let key = JsValue::from_str(key);\n                js_sys::Reflect::set(el.as_ref(), &key, &JsValue::UNDEFINED)\n                    .expect(\"could not remove property\");\n            }\n        }\n    }\n}\n\nimpl Apply for Attributes {\n    type Bundle = Self;\n    type Element = Element;\n\n    fn apply(self, _root: &BSubtree, el: &Element) -> Self {\n        #[expect(deprecated)]\n        match &self {\n            Self::Static(arr) => {\n                for (k, v) in arr.iter() {","sourceCodeStart":191,"sourceCodeEnd":227,"githubUrl":"https://github.com/yewstack/yew/blob/0e4a05472fac4e5fce1befe60fa4a1e43a36b6a3/packages/yew/src/dom_bundle/btag/attributes.rs#L191-L227","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Validate attribute names at the boundary where dynamic names are introduced (reject empty strings and space/quote/'<'/'>'/'/'/'=' characters) so they never reach the VTag","Log attribute keys when inserting into Attributes to catch malformed names at creation time, not at removal time","Where possible, use static html! attribute names and pass values, not names, dynamically"],"exampleFix":"// before\nlet attrs: Vec<(AttrValue, AttrValue)> = deserialize_from_config();\n\n// after\nfn is_valid_attr_name(name: &str) -> bool {\n    !name.is_empty() && !name.chars().any(|c| matches!(c, ' ' | '\"' | '\\'' | '<' | '>' | '/' | '=' ) || c.is_control())\n}\nlet attrs: Vec<(AttrValue, AttrValue)> = deserialize_from_config()\n    .into_iter().filter(|(k, _)| is_valid_attr_name(k)).collect();","handlingStrategy":"validation","validationCode":"fn is_valid_attr_name(name: &str) -> bool {\n    !name.is_empty()\n        && !name.chars().any(|c| matches!(c, ' ' | '\"' | '\\'' | '<' | '>' | '/' | '=') || c.is_control())\n}\n\n// filter at ingestion so bad names never enter the attribute set (and never reach removal)\nlet safe: Vec<_> = incoming_attrs.into_iter().filter(|(k, _)| is_valid_attr_name(k)).collect();","typeGuard":"fn safe_attr_name(name: &str) -> Option<&str> { is_valid_attr_name(name).then_some(name) }","tryCatchPattern":null,"preventionTips":["Validate attribute names once, at the point they are created — removal uses the stored name later","Do not feed raw external/config data into attribute maps without filtering","Log dropped names in debug builds to spot regressions in name construction"],"tags":["rust","wasm","yew","dom","attributes","runtime-panic"],"backgroundTag":"invalid-dom-attribute-name","analyzedSha":"0e4a05472fac4e5fce1befe60fa4a1e43a36b6a3","analyzedAt":"2026-08-22T21:16:31.212Z","schemaVersion":2},"datasetVersion":"2026-08-23T01:17:44.959Z"}