a-b-street/abstreet · error
render_line( )
Error message
render_line({}): {} What it means
render_line builds an SVG string wrapping the line's text, then parses it with usvg; if usvg::Tree::from_str fails it panics with the contents and the parser error. This usually means the generated <svg>/<text> markup is invalid, typically because text content broke the XML.
Solutions
- Sanitize/escape the text (XML entities) before passing to render_line; htmlescape::encode_minimal is used elsewhere in this codebase for curvey.
- Strip control characters and invalid XML chars from the string.
- Log the failing `contents` from the panic message and test the string in isolation.
Example fix
// before canvas.draw_text(serde_json::to_string(&value).unwrap()); // after canvas.draw_text(&htmlescape::encode_minimal(&serde_json::to_string(&value).unwrap()));
Defensive patterns
Strategy: validation
Validate before calling
fn is_safe_label(s: &str) -> bool {
s.chars().all(|c| !c.is_control()) && xml::Writer::new(Vec::new()).write(&s).is_ok()
} Try / catch
// Sanitize at the call site: canvas.draw_text(&htmlescape::encode_minimal(&raw));
Prevention
- Always XML-escape dynamic strings before text rendering calls
- Strip control characters and lone surrogates from user input
- Add a unit test rendering adversarial strings (angles, ampersands, control chars)
When it happens
Trigger: Calling render_line (directly or via inner_render/inner_wrap_to_pixels) with text contents containing characters that break the generated SVG markup — unescaped XML, invalid control characters, or malformed fmt sequences; also usvg internal parse failures.
Common situations: Rendering user-supplied strings or logs containing raw '<'/'>'/'&' or invalid control bytes; extremely large strings; text with embedded NUL bytes from binary data.
Related errors
AI-assisted analysis of a-b-street/abstreet@0964f29315 (2026-09-13).
Data as JSON: /api/errors/0df9c7ad7f7b6258.
Report an issue: GitHub.
Appendix: source
Thrown at widgetry/src/text.rs:512
if span.underlined {
"text-decoration=\"underline\""
} else {
""
},
if let Some(c) = span.outline_color {
format!("stroke=\"{}\"", c.as_hex())
} else {
String::new()
},
htmlescape::encode_minimal(&span.text)
)
.unwrap();
}
write!(&mut svg, "{}</text></svg>", contents).unwrap();
let mut svg_tree = match usvg::Tree::from_str(&svg, &usvg::Options::default()) {
Ok(t) => t,
Err(err) => panic!("render_line({}): {}", contents, err),
};
svg_tree.convert_text(&assets.fontdb.borrow());
let mut batch = GeomBatch::new();
match crate::svg::add_svg_inner(&mut batch, svg_tree, tolerance) {
Ok(_) => batch,
Err(err) => {
error!("render_line({}): {}", contents, err);
// We'll just wind up with a blank line
batch
}
}
}
pub trait TextExt {
fn text_widget(self, ctx: &EventCtx) -> Widget;
fn batch_text(self, ctx: &EventCtx) -> Widget;
}
View on GitHub (pinned to 0964f29315)