can1357/oh-my-pi · error · Error

Invalid XML: unterminated tag

Error message

Invalid XML: unterminated tag

What it means

Regular tags must be closed with ">". When the parser finds a "<" that starts neither a comment, CDATA, processing instruction, nor declaration, and no ">" follows before end-of-input, it throws this error. The parser then continues to tag matching, where an unexpected closing tag also throws ("Invalid XML: unexpected closing tag").

Source

Thrown at packages/utils/src/docx/xml.ts:95

			if (end === -1) throw new Error("Invalid XML: unterminated CDATA section");
			stack[stack.length - 1].children.push({ kind: "text", value: source.slice(lessThan + 9, end) });
			offset = end + 3;
			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);

View on GitHub (pinned to 9690622007)

Solutions

  1. Escape literal "<" in text content as "&lt;" (and "&" as "&amp;") before embedding data in XML.
  2. Ensure every opened tag is complete: check the XML ends cleanly with matching close tags.
  3. Check for truncation — compare entry sizes or re-download/re-extract the file.
  4. Fix the XML-generating code to serialize complete elements and validate output before conversion.

Example fix

// before
const xml = `<doc><text>5 < 10`; // raw < and unclosed tag

// after
const xml = `<doc><text>5 &lt; 10</text></doc>`;
parseXml(xml);
Defensive patterns

Strategy: validation

Validate before calling

function escapeXmlText(s: string): string {
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
const xml = `<doc><text>${escapeXmlText(userText)}</text></doc>`;
if (!xml.trim().endsWith(">") || (xml.match(/</g)?.length ?? 0) !== (xml.match(/>/g)?.length ?? 0)) {
  throw new Error("XML has unbalanced angle brackets");
}

Try / catch

try {
  const doc = parseXml(xml);
} catch (err) {
  if (err instanceof Error && err.message === "Invalid XML: unterminated tag") {
    // reject the input or repair truncation before retrying
  } else throw err;
}

Prevention

When it happens

Trigger: Calling parseXml() on XML with an unclosed tag — input truncated mid-element (e.g. "<w:p"), a stray "<" in text that wasn't escaped, or generated XML missing the final ">".

Common situations: Raw "<" or "&" characters in document text without XML escaping; truncated downloads or archive entries; template bugs dropping the closing bracket of the last element.

Related errors


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