666ghj/MiroFish · error · StarHistoryError

generated SVG is not valid XML

Error message

generated SVG is not valid XML

What it means

Raised by `_validate_svg` (scripts/star_history.py:1087) when `xml.etree.ElementTree.fromstring` rejects the payload — the bytes decoded fine and contain no forbidden directives, but the document is not well-formed XML (unclosed tags, mismatched tags, stray `&`, malformed attributes).

Source

Thrown at scripts/star_history.py:1087

def _validate_svg(payload: bytes) -> None:
    try:
        decoded_payload = payload.decode("utf-8", errors="strict")
    except UnicodeDecodeError as exc:
        raise StarHistoryError("generated SVG must be strict UTF-8") from exc
    if decoded_payload.startswith("\ufeff") or "\x00" in decoded_payload:
        raise StarHistoryError("generated SVG must be canonical UTF-8")
    upper_payload = decoded_payload.upper()
    if (
        "<!DOCTYPE" in upper_payload
        or "<!ENTITY" in upper_payload
        or "<?" in decoded_payload
    ):
        raise StarHistoryError("generated SVG contains forbidden XML directives")
    try:
        root = ET.fromstring(decoded_payload)
    except ET.ParseError as exc:
        raise StarHistoryError("generated SVG is not valid XML") from exc
    svg_namespace = "{http://www.w3.org/2000/svg}"
    if root.tag != f"{svg_namespace}svg":
        raise StarHistoryError("generated SVG root is invalid")
    allowed_attributes: dict[str, set[str]] = {
        "svg": {
            "viewBox",
            "width",
            "height",
            "preserveAspectRatio",
            "role",
            "aria-labelledby",
        },
        "title": {"id"},
        "desc": {"id"},
        "rect": {
            "x",
            "y",
            "width",

View on GitHub (pinned to b5b53acc57)

Solutions

  1. If you forked the text generation, escape all interpolated values: `xml.sax.saxutils.escape(value)` and `quoteattr(value)` for attributes.
  2. Test round-trip in CI: `render_svg(...)` then `ET.fromstring(payload)` immediately — catches escaping regressions at build time.
  3. If validating an external file, re-run the renderer instead of repairing the broken file; canonical output always parses.
  4. Locate the defect with `ET.fromstring` traceback line/column numbers, which point at the exact malformed construct.

Example fix

# before: raw interpolation breaks XML on '&' or '<'
title_text = f"{owner}/{name} Star History"

# after: escape dynamic text
from xml.sax.saxutils import escape
title_text = escape(f"{owner}/{name} Star History")
Defensive patterns

Strategy: try-catch

Validate before calling

import xml.etree.ElementTree as ET

def is_well_formed_xml(payload: bytes) -> bool:
    try:
        ET.fromstring(payload)
        return True
    except ET.ParseError:
        return False

Try / catch

try:
    _validate_svg(payload)
except StarHistoryError as exc:
    if "not valid XML" in str(exc):
        raise ValueError(f"SVG is malformed: {exc.__cause__}") from exc
    raise

Prevention

When it happens

Trigger: Any ET.ParseError while parsing the decoded SVG: unescaped `&` in text (e.g. 'AT&T'), truncated output from a bad write, hand-string-concatenated fragments, or fork code that builds tags via f-strings without escaping.

Common situations: Forks that interpolate repository names or usernames containing `<`, `>`, `&`, or quotes directly into SVG text; partial writes from crashed processes; templates split across concatenation points that lose a closing tag.

Related errors


AI-assisted analysis of 666ghj/MiroFish@b5b53acc57 (2026-08-14). Data as JSON: /api/errors/fcdae89cb87f6b5e. Report an issue: GitHub.