can1357/oh-my-pi · error · Error
Invalid XML: expected one root element
Error message
Invalid XML: expected one root element
What it means
A well-formed XML document must contain exactly one top-level element. parseXml attaches all parsed top-level elements to a synthetic root and, after a successful scan, requires that exactly one element child exists; zero or multiple roots throw this error. Text, comments, and declarations around the root are tolerated, but a second element sibling is not.
Source
Thrown at packages/utils/src/docx/xml.ts:130
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);
}
/** Return an attribute by qualified or namespace-independent name. */
export function attribute(element: XmlElement | undefined, name: string): string | undefined {View on GitHub (pinned to 9690622007)
Solutions
- Wrap multiple top-level elements in a single container element (e.g. `<w:root>...</w:root>`).
- If the input is empty, check upstream generation — an empty template or failed render often produces no root at all.
- If you actually need to parse a fragment with multiple roots, parse each fragment separately or wrap it first.
- Sanity-check the input with xmllint, which reports the same 'extra content' class of problem.
Example fix
// before: two sibling roots const xml = `<w:sectPr/><w:body/>`; parseXml(xml); // throws: expected one root element // after: single wrapped root const xml = `<w:document><w:sectPr/><w:body/></w:document>`; parseXml(xml); // ok
Defensive patterns
Strategy: validation
Validate before calling
function hasSingleRootElement(xml: string): boolean {
let depth = 0, roots = 0;
for (const m of xml.matchAll(/<\s*(\/?)\s*([\w:.-]+)(?:\s[^>]*)?(\/?)\s*>/g)) {
if (m[3] === "/") { if (depth === 0) roots++; continue; }
if (m[1]) { depth--; } else { if (depth === 0) roots++; depth++; }
}
return roots === 1;
}
// call before: if (!hasSingleRootElement(xml)) xml = `<root>${xml}</root>`; else parseXml(xml); Try / catch
try {
return parseXml(xml);
} catch (err) {
if (err instanceof Error && err.message === "Invalid XML: expected one root element") {
return parseXml(`<w:wrap>${xml}</w:wrap>`); // fallback: wrap fragments
}
throw err;
} Prevention
- Wrap concatenated fragments in a single container element before parsing.
- Reject or trim empty/whitespace-only input before calling parseXml.
- Treat every parsed document as one root plus metadata — assert root name after parsing to catch swapped inputs.
When it happens
Trigger: Calling parseXml/root/document/parseFootnotes on input with zero elements (empty string or only text/whitespace), or two or more sibling top-level elements such as `<a/><b/>` or a concatenated pair of full documents `<doc1/><doc2/>`.
Common situations: Concatenating several DOCX fragments each having its own root; parsing an empty/blank response from a generator or template with no body; passing a document-fragment collection instead of a whole document.
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/0aa5bf0c337a1ac2.
Report an issue: GitHub.