can1357/oh-my-pi · error · Error
Invalid XML: unexpected closing tag
Error message
Invalid XML: unexpected closing tag
What it means
This lightweight DOCX XML parser (parseXml in packages/utils/src/docx/xml.ts) maintains a stack of open elements while scanning tags. When it encounters a closing tag (`</name>`) while only the synthetic root remains on the stack (stack.length === 1), there is no matching open element to close — the document has an extra/unbalanced closing tag. The parser is deliberately strict, treating any structural imbalance as invalid XML rather than tolerating it.
Source
Thrown at packages/utils/src/docx/xml.ts:98
continue;
}
if (source.startsWith("<?", lessThan)) {
const end = source.indexOf("?>", lessThan + 2);
if (end === -1) throw new Error("Invalid XML: unterminated processing instruction");
offset = end + 2;
continue;
}
if (source.startsWith("<!", lessThan)) {
const end = source.indexOf(">", lessThan + 2);
if (end === -1) throw new Error("Invalid XML: unterminated declaration");
offset = end + 1;
continue;
}
const end = source.indexOf(">", lessThan + 1);
if (end === -1) throw new Error("Invalid XML: unterminated tag");
const raw = source.slice(lessThan + 1, end).trim();
if (raw.startsWith("/")) {
if (stack.length === 1) throw new Error("Invalid XML: unexpected closing tag");
const closingName = raw.slice(1).trim();
const completed = stack.pop();
if (!completed || completed.name !== closingName)
throw new Error(`Invalid XML: mismatched closing tag ${closingName}`);
stack[stack.length - 1].children.push({
kind: "element",
name: completed.name,
attributes: completed.attributes,
children: completed.children,
});
} else {
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)) {View on GitHub (pinned to 9690622007)
Solutions
- Print/inspect the input XML around the position of the stray closing tag and remove or balance it.
- If assembling XML from fragments, ensure each opening tag has exactly one closing tag and there is a single root element.
- Validate the XML with a standalone parser (e.g. xmllint) before passing it to parseXml to pinpoint the imbalance.
- If the input comes from a template, fix the template so the whole document has exactly one root element wrapped once.
Example fix
// before: duplicated closer from string concatenation const xml = `<w:document><w:body></w:body></w:document></w:document>`; parseXml(xml); // throws // after: balanced single root const xml = `<w:document><w:body></w:body></w:document>`; parseXml(xml); // ok
Defensive patterns
Strategy: validation
Validate before calling
function hasBalancedClosingTags(xml: string): boolean {
let depth = 0;
for (const m of xml.matchAll(/<\s*(\/?)\s*([\w:.-]+)(?:\s[^>]*)?(\/?)\s*>/g)) {
if (m[3] === "/") continue; // self-closing
depth += m[1] ? -1 : 1;
if (depth < 0) return false; // closing tag with nothing open
}
return depth >= 0;
}
// call before: if (!hasBalancedClosingTags(xml)) fixInputFirst(); else parseXml(xml); Try / catch
try {
const doc = parseXml(xml);
} catch (err) {
if (err instanceof Error && err.message.startsWith("Invalid XML")) {
// surface malformed-input to caller / log xml for inspection
} else {
throw err;
}
} Prevention
- Build XML with a serializer instead of string concatenation so tags stay balanced.
- Validate generated DOCX XML with xmllint in tests before feeding it to parseXml.
- Never splice fragments that carry their own root-level closing tags into another document.
When it happens
Trigger: Calling parseXml (directly or via the exported `root`, `document`, or `parseFootnotes` helpers) on a string containing a closing tag after the top-level element has already been closed, e.g. `<a><b/></a></a>` or two sibling root elements `<a/><b/>` (the second element's closing tag fires after stack returns to the synthetic root).
Common situations: Hand-built or template-generated DOCX XML with a duplicated closing tag; string concatenation that appends `</w:document>` twice; extracting a fragment from a larger document and leaving a trailing closing tag; naive regex-based XML assembly that drops an opening tag but keeps its closer.
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/226dfbdd38c23e64.
Report an issue: GitHub.