Skip to content

Fix circular structure returned for unclosed tags - #224

Open
theRizwan wants to merge 1 commit into
nashwaan:masterfrom
theRizwan:fix/unclosed-tag-circular-parent
Open

theRizwan wants to merge 1 commit into
nashwaan:masterfrom
theRizwan:fix/unclosed-tag-circular-parent

Conversation

@theRizwan

Copy link
Copy Markdown

Fixes #223.

An unclosed tag left the parser's internal parent back-reference attached to the returned object, so the result was circular:

convert.xml2json('<a>');
// TypeError: Converting circular structure to JSON

convert.xml2js('<a>').elements[0].parent === result;   // true

That also contradicts the README, which documents parent as appearing only "by setting {addParent: true}".

Cause

lib/xml2js.js:250 attaches the reference with the comment "will be deleted in onEndElement() if !options.addParent". onEndElement is the only place that removes it, and it never runs for a tag that is never closed.

The sax error that should have surfaced can't: "Unclosed root tag" is raised inside end(), after write()'s entry-time error check, and onError (line 312) is error.note = error, which discards it. Hence the asymmetry — <a></b> throws a clean Unmatched closing tag: b because that fires during write(), while <a> does not.

The change

Unwinds anything left open after parsing, mirroring the cleanup already in onEndElement.

Deliberately conservative: it preserves the library's lenient "don't throw on unclosed tags" behaviour rather than touching onError, so nothing that parses today starts throwing. It terminates safely if the chain is broken, and addParent: true still attaches parent exactly as documented.

Tests

Existing suite unchanged: 2363 specs, 0 failures. Three specs added, covering the leak, serialisability, and that addParent: true still works — 2366 total.

Confirmed they catch it. Reverting only lib/xml2js.js:

Failures:
1) …unclosed tags leaving a circular parent reference should not leak the internal parent key when addParent is false
   Expected true to be false.

Note

Truncated XML is ordinary production input — a cut-off feed, a partial upload, a socket closed mid-response. The current failure surfaces as a JSON.stringify error far from the parse, or as an object that silently breaks deep equality, structured cloning and res.json().

onStartElement attaches an internal parent back-reference (xml2js.js:250)
that onEndElement removes when addParent is false. For an unclosed tag
onEndElement never runs, so the reference survives into the returned
object:

    convert.xml2json('<a>')
    // TypeError: Converting circular structure to JSON

    convert.xml2js('<a>').elements[0].parent === result   // true

That also contradicts the README, which documents `parent` as appearing
only when addParent is true.

The underlying sax error cannot surface: "Unclosed root tag" is raised
inside end(), after write()'s entry-time error check, and onError discards
it. Hence the asymmetry where `<a></b>` throws a clean "Unmatched closing
tag" but `<a>` does not.

Unwinds anything left open after parsing, mirroring the cleanup already in
onEndElement. Deliberately conservative: it preserves the lenient
"don't throw on unclosed tags" behaviour rather than changing onError, so
it is not a breaking change.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unclosed tag returns a circular object; xml2json throws "Converting circular structure to JSON"

1 participant