{"record":{"id":"dab0b9ae49861cc9","repo":"can1357/oh-my-pi","slug":"invalid-xml-unclosed-tag-stack-stack-length-1","errorCode":null,"errorMessage":"Invalid XML: unclosed tag ${stack[stack.length - 1].name}","messagePattern":"Invalid XML: unclosed tag (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/utils/src/docx/xml.ts","lineNumber":128,"sourceCode":"\t\t\tconst selfClosing = raw.endsWith(\"/\");\n\t\t\tconst tag = selfClosing ? raw.slice(0, -1).trim() : raw;\n\t\t\tconst whitespace = tag.search(/\\s/);\n\t\t\tconst name = whitespace === -1 ? tag : tag.slice(0, whitespace);\n\t\t\tconst attributes = new Map<string, string>();\n\t\t\tATTRIBUTE_PATTERN.lastIndex = whitespace === -1 ? tag.length : whitespace;\n\t\t\tfor (let match = ATTRIBUTE_PATTERN.exec(tag); match; match = ATTRIBUTE_PATTERN.exec(tag)) {\n\t\t\t\tattributes.set(match[1], decodeEntities(match[2] ?? match[3] ?? \"\"));\n\t\t\t}\n\t\t\tconst pending = { name, attributes, children: [] as XmlNode[] };\n\t\t\tif (selfClosing) {\n\t\t\t\tstack[stack.length - 1].children.push({ kind: \"element\", ...pending });\n\t\t\t} else {\n\t\t\t\tstack.push(pending);\n\t\t\t}\n\t\t}\n\t\toffset = end + 1;\n\t}\n\tif (stack.length !== 1) throw new Error(`Invalid XML: unclosed tag ${stack[stack.length - 1].name}`);\n\tconst roots = synthetic.children.filter((node): node is XmlElement => node.kind === \"element\");\n\tif (roots.length !== 1) throw new Error(\"Invalid XML: expected one root element\");\n\treturn roots[0];\n}\n\n/** Return direct element children, optionally filtered by local name. */\nexport function childElements(element: XmlElement, name?: string): XmlElement[] {\n\treturn element.children.filter(\n\t\t(node): node is XmlElement => node.kind === \"element\" && (name === undefined || localName(node.name) === name),\n\t);\n}\n\n/** Return the first direct child with the given local name. */\nexport function firstChild(element: XmlElement | undefined, name: string): XmlElement | undefined {\n\tif (!element) return undefined;\n\treturn element.children.find((node): node is XmlElement => node.kind === \"element\" && localName(node.name) === name);\n}\n","sourceCodeStart":110,"sourceCodeEnd":146,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/utils/src/docx/xml.ts#L110-L146","documentation":"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>`.","triggerScenarios":"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/>`.","commonSituations":"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.","solutions":["Close the tag named in the message before the document ends (add the missing `</name>` in the right position).","If the element should be empty, write it self-closing: `<name/>` instead of `<name>`.","Check whether the input was truncated (file read, HTTP body, stream) and fix the source to deliver the complete document.","Validate with xmllint --noout to find all unclosed tags at once."],"exampleFix":"// before: body never closed\nconst xml = `<w:document><w:body><w:p/></w:document>`;\nparseXml(xml); // throws: unclosed tag w:body\n\n// after\nconst xml = `<w:document><w:body><w:p/></w:body></w:document>`;\nparseXml(xml); // ok","handlingStrategy":"validation","validationCode":"function allTagsClosed(xml: string): boolean {\n  const stack: string[] = [];\n  for (const m of xml.matchAll(/<\\s*(\\/?)\\s*([\\w:.-]+)(?:\\s[^>]*)?(\\/?)\\s*>/g)) {\n    if (m[3] === \"/\") continue;\n    if (m[1]) stack.pop();\n    else stack.push(m[2]);\n  }\n  return stack.length === 0;\n}\n// call before: if (!allTagsClosed(xml)) throw new Error(\"unclosed tags\"); else parseXml(xml);","typeGuard":null,"tryCatchPattern":"try {\n  return parseXml(xml);\n} catch (err) {\n  if (err instanceof Error && err.message.includes(\"unclosed tag\")) {\n    // check for truncation: log xml.length, retry with complete source if available\n    throw new Error(`XML truncated (${err.message})`);\n  }\n  throw err;\n}","preventionTips":["Ensure streams/file reads complete before parsing; verify input ends with the root's closing tag.","Write empty elements self-closing (`<w:br/>`); this parser does not auto-close HTML-style void tags.","Add a CI check that generated documents parse with xmllint."],"tags":["xml","parsing","docx","malformed-input"],"backgroundTag":"xml-unclosed-tag","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}