fix: don't crash on a closing tag with no matching opening tag - #861
Merged
amitguptagwl merged 1 commit intoAug 15, 2026
Merged
Conversation
A closing tag with no matching opening tag pops an empty tagsNodeStack, so currentNode became undefined and the next node crashed the parser with "TypeError: Cannot read properties of undefined (reading 'addChild')". Fall back to the root node instead, so parsing continues at root scope. That matches how the parser already handles the same input when nothing follows the stray closing tag.
hdimer
marked this pull request as ready for review
August 14, 2026 23:51
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Purpose / Goal
XMLParser.parse()crashes with an internalTypeErroron XML where a closing tag has no matching opening tag. No issue was raised for this, so per the template here is input / expected / actual.Input
Actual (v5.10.1)
Expected
{ b: "text" }— the same leniency the parser already applies to every other kind of malformed input when validation is not requested:parse("<a>"){"a":""}parse("<a></b>"){"a":""}parse("</a>"){}parse("<a/>tail</b>"){"a":"","#text":"tail"}parse("</a><b/>")Cause
In the closing-tag branch,
currentNode = this.tagsNodeStack.pop(). When the closing tag has no matching opening tag the stack is empty,pop()returnsundefined, and the next node dereferences it. It only surfaces when something follows the stray closing tag, which is why the last two rows above disagree.Fix
One line: fall back to the root node, so parsing continues at root scope.
This also fixes three sibling crashes from the same undefined:
</a><![CDATA[x]]>threw onreading 'tagname',</a><!--c-->onreading 'add', and</a><?pi?>onaddChild.I deliberately did not make this throw.
parse()is lenient by design and validation is opt-in viaparse(xml, true);validate("</rootNode>")already reportsClosing tag 'rootNode' has not been opened.Throwing here would also be a breaking change, since</a>,<a></a></b>and<a/>tail</b>all reach this same empty-stack pop today and return normally.Behaviour change
Only one, and it is the crash's sibling: text directly after a stray closing tag used to be silently dropped, and is now kept.
That matches
parse("<a/>tail</b>"), which already returns{"a":"","#text":"tail"}on v5.10.1 through the same root-node path. It is pinned by a test.To check the blast radius I ran 4,000 generated XML documents across 6 option sets (24,000 parses) against both builds: 24 rows went TypeError → parsed, 6 rows gained the root
#textabove, and nothing else changed. A wider sweep (11,110 token sequences × 10 option sets, includingpreserveOrder,captureMetaData,alwaysCreateTextNode,unpairedTags,stopNodes,transformTagName,updateTag) showed no structural output change at all.Two things I noticed but left alone rather than widen this PR:
&& currentNodehalf of thecaptureMetaDataguard just below is now unreachable, since the pop can no longer be falsy. Happy to remove it if you want.tagsNodeStack.pop()in the unpaired-tag branch, but I could only reach it with a pathological config (unpairedTags: ["!xml"]), which fails identically before and after. Different user story.Tests
Added to
spec/xmlParser_spec.js, covering the crash, the root-scope fallback, the CDATA variant and the negative case above. Each assertion fails on unfixedmasterand passes with the fix. Full suite: 325 specs, 0 failures.Perf
node benchmark/XmlParser.mjs, 3 interleaved rounds per build on the same machine (medians, requests/second):Honestly: this machine was too noisy to resolve a difference.
xml2js, which neither build touches, swung between 3,251 and 15,677 across the same runs, so treat the table as "no measurable change" rather than a win. The change adds one||on the closing-tag path and no allocation.Type
Used AI assistance on this; I reviewed and tested the change myself.