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

  1. Print/inspect the input XML around the position of the stray closing tag and remove or balance it.
  2. If assembling XML from fragments, ensure each opening tag has exactly one closing tag and there is a single root element.
  3. Validate the XML with a standalone parser (e.g. xmllint) before passing it to parseXml to pinpoint the imbalance.
  4. 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

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


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