feat: handle circular structure serialization#7275
feat: handle circular structure serialization#7275leandro-costa-oliveira wants to merge 3 commits into
Conversation
0716583 to
1ea6f49
Compare
1ea6f49 to
ccf2192
Compare
notadev99
left a comment
There was a problem hiding this comment.
Real problem — axios-error objects with circular refs hitting res.json is common. A few things to work through before this changes a core contract:
Default behavior change. Today res.json throws on a circular structure; this makes it silently return 200 with "[Circular]" substituted. Some apps rely on that throw to surface the bug rather than ship malformed data to a client. Is silent recovery the right default, or should it be opt-in (an app setting / option) so existing throw behavior is preserved by default? That feels like a maintainer call to settle before the implementation details.
Error detection is string-based. isCircularJsonError matches /circular structure/i on the message, which is tied to the current V8 wording — if that string ever changes it silently stops catching and falls back to a raw throw. Not fatal on Node, but fragile; a comment noting the dependency would help.
Reimplemented array-replacer semantics. hasReplacerKey and the array branch re-derive what JSON.stringify already does natively with an array replacer, and matching its exact semantics (nested keys, the always-included top-level "") is easy to get subtly wrong. Is the array-replacer path needed for the fix, or could the fallback stay narrower?
Tests. The single case covers a top-level self-reference. Since the custom replacer also handles user replacer functions, array replacers, and nesting, it would be worth a case for each — especially a user-supplied replacer plus circular data, which is the trickiest path.
Totally agree, changed to be opt-in, keeping the default behavior unchaged, to avoid breaking peoples projetcs.
Added a comment, mentioning that is dependent on v8 engine. Also the tests should cover it if there's some change on the copy.
Removed.
Added more test cases, having coverage for default behavior that should throw the error and the new opt-in behavior handling the circular dependency. |
notadev99
left a comment
There was a problem hiding this comment.
Thanks for turning this around so fast — this addresses everything cleanly:
- Opt-in via
json circularwith the default left as-throw is the right call; existing apps keep their current behavior. - Handling the array-replacer case by throwing rather than reimplementing its semantics is a nice simplification — it keeps native
JSON.stringifybehavior intact on that path. - The test matrix now covers the paths that matter: default-throws, opt-in basic, nested, and function-replacer + circular. The default-throws case in particular pins the contract so it can't silently regress later.
One non-blocking follow-up: json circular is a new app setting, so it'll want a History.md/changelog entry and a docs line alongside json escape / json replacer / json spaces so people can actually discover it. Otherwise this looks good from my side.
Done, log entry on History.md added. :) |
|
Looks good — the History.md line is clear and correctly scopes it to both |
Problem
res.jsoninlib/response.jscallsstringifywhich fails if a object with circular structure is passed.This is pretty common when you have calls using
axioslib, the axios errors include the response object which have circular structure. Since a bunch a libs rely on axios, this is a pretty common cause of issues.Example of Stack Trace
Doing a
soapcall withrouting-controllerson top ofexpress, fails to parse the error.Tests
Added tests in
test/res.jsonandtest/res.jsonpsimulating the circular structure.