Repository: axios/axios (js)

Issue:
Error.toJSON() should exclude httpsAgent
#### Describe the bug

if the `httpsAgent` property is set, it is not excluded from `error.toJSON()` resulting in a circular reference error

```none
TypeError: Converting circular structure to JSON
    --> starting at object with constructor 'Object'
    |     property 'httpsAgent' -> object with constructor 'Agent'
    |     property 'sockets' -> object with constructor 'Object'
    |     ...
    |     property 'errored' -> object with constructor 'Object'
    --- property 'config' closes the circle
    at JSON.stringify (<anonymous>)
```

#### To Reproduce

```js
const agent = new https.Agent({  
  rejectUnauthorized: false
});

axios.get("some-dodgy-url", {
  httpsAgent: agent
}).catch((err) => {
  console.error(err.toJSON())
})
```

#### Expected behavior

The error object should be serialisable as JSON without throwing circular reference errors.

#### Environment
 - Axios 0.26.0

#### Additional context/Screenshots

See [this StackOverflow post](https://stackoverflow.com/q/71150003/283366)

Pull request:
Fixed AxiosError.toJSON method to avoid circular references;
- Added `toJSONObject` util;
- Fixed AxiosError.toJSON method to avoid circular references;
Closes #4486;

Patch:
diff --git a/lib/core/AxiosError.js b/lib/core/AxiosError.js
index 1539e9a..7141a8c 100644
--- a/lib/core/AxiosError.js
+++ b/lib/core/AxiosError.js
@@ -45,7 +45,7 @@ utils.inherits(AxiosError, Error, {
       columnNumber: this.columnNumber,
       stack: this.stack,
       // Axios
-      config: this.config,
+      config: utils.toJSONObject(this.config),
       code: this.code,
       status: this.response && this.response.status ? this.response.status : null
     };
diff --git a/lib/utils.js b/lib/utils.js
index 849d044..2267723 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -592,6 +592,37 @@ const toFiniteNumber = (value, defaultValue) => {
   return Number.isFinite(value) ? value : defaultValue;
 }
 
+const toJSONObject = (obj) => {
+  const stack = new Array(10);
+
+  const visit = (source, i) => {
+
+    if (isObject(source)) {
+      if (stack.indexOf(source) >= 0) {
+        return;
+      }
+
+      if(!('toJSON' in source)) {
+        stack[i] = source;
+        const target = isArray(source) ? [] : {};
+
+        forEach(source, (value, key) => {
+          const reducedValue = visit(value, i + 1);
+          !isUndefined(reducedValue) && (target[key] = reducedValue);
+        });
+
+        stack[i] = undefined;
+
+        return target;
+      }
+    }
+
+    return source;
+  }
+
+  return visit(obj, 0);
+}
+
 export default {
   isArray,
   isArrayBuffer,
@@ -637,5 +668,6 @@ export default {
   toFiniteNumber,
   findKey,
   global: _global,
-  isContextDefined
+  isContextDefined,
+  toJSONObject
 };


Test patch:
diff --git a/test/unit/utils/utils.js b/test/unit/utils/utils.js
index 57a183a..73f3788 100644
--- a/test/unit/utils/utils.js
+++ b/test/unit/utils/utils.js
@@ -23,4 +23,30 @@ describe('utils', function (){
       assert.equal(utils.isFormData(new FormData()), true);
     });
   });
+
+  describe('toJSON', function (){
+    it('should convert to a plain object without circular references', function () {
+      const obj= {a: [0]}
+      const source = {x:1, y:2, obj};
+      source.circular1 = source;
+      obj.a[1] = obj;
+
+      assert.deepStrictEqual(utils.toJSONObject(source), {
+        x: 1, y:2, obj: {a: [0]}
+      });
+    });
+
+    it('should use objects with defined toJSON method without rebuilding', function () {
+      const objProp = {};
+      const obj= {objProp, toJSON(){
+        return {ok: 1}
+      }};
+      const source = {x:1, y:2, obj};
+
+      const jsonObject = utils.toJSONObject(source);
+
+      assert.strictEqual(jsonObject.obj.objProp, objProp);
+      assert.strictEqual(JSON.stringify(jsonObject), JSON.stringify({x: 1, y:2, obj: {ok: 1}}))
+    });
+  });
 });