can1357/oh-my-pi · error · Error
Invalid XML: mismatched closing tag ${closingName}
Error message
Invalid XML: mismatched closing tag ${closingName} What it means
parseXml pops the innermost open element when it sees a closing tag and compares the popped element's name with the closing tag's name. If they differ (e.g. `<a><b></a></b>`) or the stack was empty (`completed` is undefined, meaning the closing tag has no open element), it throws this mismatched-closing-tag error. It enforces proper XML nesting: tags must close in exact reverse order of opening.
Source
Thrown at packages/utils/src/docx/xml.ts:102
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)) {
attributes.set(match[1], decodeEntities(match[2] ?? match[3] ?? ""));
}
const pending = { name, attributes, children: [] as XmlNode[] };
if (selfClosing) {View on GitHub (pinned to 9690622007)
Solutions
- Locate the tag named in the message and check that its opening tag exists and properly encloses the content between the pair.
- Reorder or rename the closing tag so nesting follows strict LIFO order (last opened, first closed).
- Run the input through xmllint --noout to get a line/column report of the mismatch before retrying.
- If building XML programmatically, switch to a serializer/builder that tracks nesting instead of concatenating strings.
Example fix
// before: wrong nesting order const xml = `<w:p><w:r></w:p></w:r>`; parseXml(xml); // throws: mismatched closing tag w:p // after: close in reverse order of opening const xml = `<w:p><w:r></w:r></w:p>`; parseXml(xml); // ok
Defensive patterns
Strategy: validation
Validate before calling
function nestingIsLifo(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]) {
if (stack.pop() !== m[2]) return false;
} else {
stack.push(m[2]);
}
}
return true;
}
// call before: if (!nestingIsLifo(xml)) throw new Error("bad nesting"); else parseXml(xml); Try / catch
try {
return parseXml(xml);
} catch (err) {
if (err instanceof Error && err.message.includes("mismatched closing tag")) {
throw new Error(`Malformed XML nesting: ${err.message}`);
}
throw err;
} Prevention
- Close tags in strict reverse order of opening; lint templates that emit XML.
- Use a builder API (element/append helpers) rather than hand-written tag strings.
- Run xmllint --noout on generated documents in CI to catch mismatches early.
When it happens
Trigger: Calling parseXml/root/document/parseFootnotes on XML where a closing tag's name differs from the currently innermost open element, e.g. `<w:p><w:r></w:p></w:r>`, or a closing tag appears with no matching opening tag at all (the template-literal message then interpolates the closer's name).
Common situations: Incorrectly nested markup generated by template concatenation (`<b>...</i>`); missing an opening tag while keeping its closer; DOCX XML edited by hand or by a script that reorders elements; copy-paste of partial fragments into a document body.
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/5d9764f3b2b12d84.
Report an issue: GitHub.