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

  1. Close the tag named in the message before the document ends (add the missing `</name>` in the right position).
  2. If the element should be empty, write it self-closing: `<name/>` instead of `<name>`.
  3. Check whether the input was truncated (file read, HTTP body, stream) and fix the source to deliver the complete document.
  4. 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

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


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