Repository: axios/axios
Pull request #6362 (2024-04-19 09:59:17)

fix(core/axios): handle un-writable error stack
## Issue

We've observed many reports of the following error on a production front-end after upgrading from `1.6.2` to `1.6.7`. The latest version `1.6.8` is also impacted.

```
TypeError: Attempted to assign to read-only property.
  at Axios$1.request(../../node_modules/.pnpm/axios@1.6.8/node_modules/axios/lib/core/Axios.js:54:24)
```

We've also noticed another similar error, which is probably triggered by a different type of browser:
```
TypeError: setting getter-only property "stack"
  at Axios$1.request(../../node_modules/.pnpm/axios@1.6.8/node_modules/axios/lib/core/Axios.js:54:31)
```

## Cause

Native network exceptions thrown by some browsers have an un-writable `stack` property. With current code, it results in the above error [triggered from this line of code](https://github.com/axios/axios/blob/751133eb9ed794c6f6634c52f4fe116e33bf5f09/lib/core/Axios.js#L51).

I suspect this bug was introduced by https://github.com/axios/axios/pull/6203.

## Fix

This PR adds a failing test (first commit) and fixes the issue (second commit).

Please let me know if you have any recommendations 🙂

DigitalBrainJS: Not sure, but there may be some cases when the field is not writable, but has a `falsy` value, or an error is thrown inside the setter. The user (or some third-party code on a custom platform) can override the descriptor in a custom error class with a custom setter that throws an error, but we will not be able to determine that it is unwritable by checking writable prop. It might be worth just wrapping the condition block with assignments with another try-catch block to swallow all possible errors. In addition, in this case, there will be less code.
alexandre-abrioux: @DigitalBrainJS Thanks for the suggestion! I've updated the code with a try-catch instead 🙂 The test is green ✅
alexandre-abrioux: @DigitalBrainJS Thanks again for your review! I updated the tests to handle all the cases we talked about, and wrapping the entire if-else block with the try-catch does the trick 👍

Patch:
diff --git a/lib/core/Axios.js b/lib/core/Axios.js
index 2713364..2765bbb 100644
--- a/lib/core/Axios.js
+++ b/lib/core/Axios.js
@@ -46,12 +46,15 @@ class Axios {
 
         // slice off the Error: ... line
         const stack = dummy.stack ? dummy.stack.replace(/^.+\n/, '') : '';
-
-        if (!err.stack) {
-          err.stack = stack;
-          // match without the 2 top stack lines
-        } else if (stack && !String(err.stack).endsWith(stack.replace(/^.+\n.+\n/, ''))) {
-          err.stack += '\n' + stack
+        try {
+          if (!err.stack) {
+            err.stack = stack;
+            // match without the 2 top stack lines
+          } else if (stack && !String(err.stack).endsWith(stack.replace(/^.+\n.+\n/, ''))) {
+            err.stack += '\n' + stack
+          }
+        } catch (e) {
+          // ignore the case where "stack" is an un-writable property
         }
       }
 


Test patch:
diff --git a/test/unit/core/Axios.js b/test/unit/core/Axios.js
new file mode 100644
index 0000000..8958ad1
--- /dev/null
+++ b/test/unit/core/Axios.js
@@ -0,0 +1,47 @@
+import Axios from "../../../lib/core/Axios.js";
+import assert from "assert";
+
+describe('Axios', function () {
+  describe("handle un-writable error stack", function () {
+    async function testUnwritableErrorStack(stackAttributes) {
+      const axios = new Axios({});
+      // mock axios._request to return an Error with an un-writable stack property
+      axios._request = () => {
+        const mockError = new Error("test-error");
+        Object.defineProperty(mockError, "stack", stackAttributes);
+        throw mockError;
+      }
+      try {
+        await axios.request("test-url", {})
+      } catch (e) {
+        assert.strictEqual(e.message, "test-error")
+      }
+    }
+
+    it('should support errors with a defined but un-writable stack', async function () {
+      await testUnwritableErrorStack({value: {}, writable: false})
+    });
+
+    it('should support errors with an undefined and un-writable stack', async function () {
+      await testUnwritableErrorStack({value: undefined, writable: false})
+    });
+
+    it('should support errors with a custom getter/setter for the stack property', async function () {
+      await testUnwritableErrorStack({
+        get: () => ({}),
+        set: () => {
+          throw new Error('read-only');
+        }
+      })
+    });
+
+    it('should support errors with a custom getter/setter for the stack property (null case)', async function () {
+      await testUnwritableErrorStack({
+        get: () => null,
+        set: () => {
+          throw new Error('read-only');
+        }
+      })
+    });
+  })
+});