Repository navigation
fix(core): bound AxiosError.toJSON by labeling agents instead of serializing them - #11362
princejain756 wants to merge 7 commits into
Conversation
|
@jasonsaayman I don't have permission to set labels. Could a maintainer apply |
Node http/https agents pin live socket, request, and response maps whose shared subgraphs were re-serialized once per reference path, so error.toJSON() could grow without bound and OOM the process for keep-alive pools (axios#11145). Swap class-instance values under httpAgent/httpsAgent for a bounded [ClassName] label before either the default or redact serializer walks the config. Only own properties are read, so inherited accessors are never invoked, non-agent values pass through, and the original config is not mutated.
81389d0 to
5898063
Compare
[Medium impact] Safe to merge based on the reviewed changes. The previously reported array-getter issue is addressed, and no blocking concerns remain. SummaryBounds agent data in
Reviews (7) · Last reviewed commit: "fix: skip named array properties in toJS..." · Reviewed by Greptile |
The shallow-copy sanitize left a hole: a config.self = config back-reference (interceptors can create it) still pointed at the original object, so the serializer walked the unsanitized agent pool through that path. Spreading also dropped inherited and non-enumerable toJSON hooks whenever an agent was present, changing snapshot output. Define the [ClassName] label on the config's own agent slots and restore them in a finally block. Every path into the config sees the label, toJSON-bearing sources keep their identity semantics, and accessor or non-configurable slots are never touched.
…tay bounded The in-place swap still leaked live agents in two cases: a frozen config rejects defineProperty so the slot kept the real agent, and a getter on the config observed the '[Agent]' label mid-serialization so derived values disappeared or crashed. Substitution now happens on read through a memoized proxy chain: no mutation at all, circular references resolve to the same proxy at any depth, getters and toJSON hooks run against the real config, and a level with a non-configurable non-writable agent slot gets a one-level descriptor clone so the label can be baked in without tripping proxy invariants.
|
Addressed in 633a977 by replacing the in-place swap entirely. Serialization now reads through a memoized proxy chain that substitutes on read, so nothing is ever written to the caller's config:
Validation: 66/66 AxiosError tests, 1710/1711 unit suite (the one failure is the pre-existing Symbol.iterator environment issue), eslint clean. |
The proxy chain hit two structural limits. A proxy get trap cannot report a substituted value for a non-writable non-configurable property, so any frozen object slot (agent or not) threw at serialize time. And bound reads through forEach or class-held references left the proxy chain and reached the raw config, re-exposing the live agent pool. Materialize the bounded view instead: clone each reached object level with its prototype, substitute '[Name]' labels for class instances under httpAgent/httpsAgent keys, evaluate accessors once against the live target so getters see the real agent, and pass through builtins and toJSON-bearing objects that need their internal slots. No proxies, no mutation, no bind.
|
Reworked again on a73f1b4. The proxy chain had a structural ceiling: a get trap cannot substitute a locked slot (frozen objects threw even without an agent), and bound reads through forEach or class-held references left the chain and reached the raw config. There is no bind call left either. The new pass materializes a thawed, prototype-preserving snapshot clone instead. Locked slots no longer matter since nothing is proxied; accessors evaluate once on the live target so getters still observe the real agent; builtins and toJSON-bearing objects pass through so internal slots stay intact; back-references through arrays or class instances memoize to the same clone, so the visited set drops them before the live agent is reached. New regression tests cover the three findings: frozen config with no agent, config.items = [config], and a class instance holding the config. |
…s in toJSON snapshot toJSON-bearing objects now pass through so custom hooks run against the real config instead of a clone missing agent internals and symbol state. Only enumerable slots are materialized so hidden accessors are never invoked, and keys matched by the redact list skip evaluation entirely since redactConfig replaces them wholesale. Set/Map/Date/RegExp/ Promise/Error subclasses join the pass-through list so their internal slots survive; Array and Object constructors are excluded from the native check so plain values still snapshot.
|
Round addressed on c3ba282.
New tests cover each finding: hook receiver fidelity, hidden accessor skip, redact-matched accessor skip, and the Set subclass. 72/72 AxiosError tests, 1716/1717 unit suite (same pre-existing env failure), eslint clean. |
|
Fixed on 84503ab. The early stop now only applies to keys on non-array objects, matching redactConfig, which never treats array indices as redactable. A numeric redact entry like '0' no longer leaves a raw element unbounded; the items[0].httpAgent case serializes to '[Agent]'. Also folded in a tightening from self review: arrays stored under httpAgent/httpsAgent no longer qualify as labelable agents, so structural values such as httpAgent: [] stay untouched. Regression test covers the indexed redact path; 73/73 focused tests pass. |
|
Fixed on 39c20bb. Named properties on array targets are now skipped before any value is read, since forEach-based serializers only visit index elements. A throwing secret getter on config.items no longer fires, while elements still copy and get bounded. Regression test covers it; 74/74 focused tests pass. |
Summary
Bounds
AxiosError.toJSON()so logging a failed request cannot balloon into the live Node agent socket pool and OOM the process. Fixes #11145.httpAgent/httpsAgentpin live socket and request maps whose shared subgraphs get re-serialized once per reference path. A 30-entry keep-alive pool reproduced the failure locally:toJSON()exhausted a 512 MB heap insidetoJSONObject.Implementation
toJSON()snapshotsconfiginto a thawed, prototype-preserving clone before either serializer (toJSONObjectorredactConfig) walks it:httpAgent/httpsAgentkeys are replaced by a'[Name]'label at any depth; plain values (functions, strings, booleans) pass through.WeakMapso the existing per-path visited check keeps working.Earlier designs
Round 1 used a shallow config copy; a
config.self = configcircle reached the unsanitized original and the copy lost inheritedtoJSONhooks. Round 2 used a lazy proxy chain; locked slots violate the Proxy invariant and bound reads throughforEach/class-held references escaped the chain. The snapshot clone covers both classes of failure.Validation
npm run test:vitest:unit: 1710/1711 (one pre-existing env failure infetch.test.jsunrelated to this change)npx eslint lib/core/AxiosError.js: clean