can1357/oh-my-pi · error · Error
Invalid XML: unclosed tag ${stack[stack.length - 1].name}
Error message
Invalid XML: unclosed tag ${stack[stack.length - 1].name} What it means
After scanning the entire input, parseXml verifies that every opened element was closed: only the synthetic root should remain on the stack. If any real elements remain open (stack.length !== 1), it throws naming the innermost unclosed tag. Self-closing tags (`<br/>`) are pushed and popped inline, so this error always means a genuine missing `</name>`.
Source
Thrown at packages/utils/src/docx/xml.ts:128
const selfClosing = raw.endsWith("/");
const tag = selfClosing ? raw.slice(0, -1).trim() : raw;
const whitespace = tag.search(/\s/);
const name = whitespace === -1 ? tag : tag.slice(0, whitespace);
const attributes = new Map<string, string>();
ATTRIBUTE_PATTERN.lastIndex = whitespace === -1 ? tag.length : whitespace;
for (let match = ATTRIBUTE_PATTERN.exec(tag); match; match = ATTRIBUTE_PATTERN.exec(tag)) {
attributes.set(match[1], decodeEntities(match[2] ?? match[3] ?? ""));
}
const pending = { name, attributes, children: [] as XmlNode[] };
if (selfClosing) {
stack[stack.length - 1].children.push({ kind: "element", ...pending });
} else {
stack.push(pending);
}
}
offset = end + 1;
}
if (stack.length !== 1) throw new Error(`Invalid XML: unclosed tag ${stack[stack.length - 1].name}`);
const roots = synthetic.children.filter((node): node is XmlElement => node.kind === "element");
if (roots.length !== 1) throw new Error("Invalid XML: expected one root element");
return roots[0];
}
/** Return direct element children, optionally filtered by local name. */
export function childElements(element: XmlElement, name?: string): XmlElement[] {
return element.children.filter(
(node): node is XmlElement => node.kind === "element" && (name === undefined || localName(node.name) === name),
);
}
/** Return the first direct child with the given local name. */
export function firstChild(element: XmlElement | undefined, name: string): XmlElement | undefined {
if (!element) return undefined;
return element.children.find((node): node is XmlElement => node.kind === "element" && localName(node.name) === name);
}
View on GitHub (pinned to 9690622007)
Solutions
- Close the tag named in the message before the document ends (add the missing `</name>` in the right position).
- If the element should be empty, write it self-closing: `<name/>` instead of `<name>`.
- Check whether the input was truncated (file read, HTTP body, stream) and fix the source to deliver the complete document.
- Validate with xmllint --noout to find all unclosed tags at once.
Example fix
// before: body never closed const xml = `<w:document><w:body><w:p/></w:document>`; parseXml(xml); // throws: unclosed tag w:body // after const xml = `<w:document><w:body><w:p/></w:body></w:document>`; parseXml(xml); // ok
Defensive patterns
Strategy: validation
Validate before calling
function allTagsClosed(xml: string): boolean {
const stack: string[] = [];
for (const m of xml.matchAll(/<\s*(\/?)\s*([\w:.-]+)(?:\s[^>]*)?(\/?)\s*>/g)) {
if (m[3] === "/") continue;
if (m[1]) stack.pop();
else stack.push(m[2]);
}
return stack.length === 0;
}
// call before: if (!allTagsClosed(xml)) throw new Error("unclosed tags"); else parseXml(xml); Try / catch
try {
return parseXml(xml);
} catch (err) {
if (err instanceof Error && err.message.includes("unclosed tag")) {
// check for truncation: log xml.length, retry with complete source if available
throw new Error(`XML truncated (${err.message})`);
}
throw err;
} Prevention
- Ensure streams/file reads complete before parsing; verify input ends with the root's closing tag.
- Write empty elements self-closing (`<w:br/>`); this parser does not auto-close HTML-style void tags.
- Add a CI check that generated documents parse with xmllint.
When it happens
Trigger: Calling parseXml/root/document/parseFootnotes on XML truncated before the end (e.g. `<w:document><w:body>...</w:document>` where `<w:body>` never closes), or a tag written as `<name>` that was intended to be self-closing `<name/>`.
Common situations: Streaming/partial reads that cut the document mid-way; a generator loop that forgets to emit the final closing tag; converting HTML-style void tags (`<br>`) that this strict parser does not auto-close; edits that delete a closing line.
Related errors
- Invalid XML: unterminated comment
- Invalid XML: unterminated CDATA section
- Invalid XML: unterminated processing instruction
- Invalid XML: unterminated declaration
- Invalid XML: unterminated tag
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/dab0b9ae49861cc9.
Report an issue: GitHub.