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

  1. Locate the tag named in the message and check that its opening tag exists and properly encloses the content between the pair.
  2. Reorder or rename the closing tag so nesting follows strict LIFO order (last opened, first closed).
  3. Run the input through xmllint --noout to get a line/column report of the mismatch before retrying.
  4. 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

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


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/5d9764f3b2b12d84. Report an issue: GitHub.