From 123f354b920f154a209ea99f76b7b2ef3d9ebbab Mon Sep 17 00:00:00 2001 From: Ilya Priven Date: Wed, 24 Jan 2024 17:21:12 -0500 Subject: [PATCH] fix: wrap errors to improve async stack trace (#5987) --- lib/core/Axios.js | 23 +++++++++++- test/unit/adapters/http.js | 76 ++++++++++++++++++++++++++++++++++---- 2 files changed, 91 insertions(+), 8 deletions(-) diff --git a/lib/core/Axios.js b/lib/core/Axios.js index 465d765..486024c 100644 --- a/lib/core/Axios.js +++ b/lib/core/Axios.js @@ -35,7 +35,28 @@ class Axios { * * @returns {Promise} The Promise to be fulfilled */ - request(configOrUrl, config) { + async request(configOrUrl, config) { + try { + return await this._request(configOrUrl, config); + } catch (err) { + const dummy = {} + if (Error.captureStackTrace) { + Error.captureStackTrace(dummy) + } else { + dummy.stack = new Error().stack; + } + // slice off the Error: ... line + dummy.stack = dummy.stack.replace(/^.+\n/, ''); + // match without the 2 top stack lines + if (!err.stack.endsWith(dummy.stack.replace(/^.+\n.+\n/, ''))) { + err.stack += '\n' + dummy.stack + } + + throw err; + } + } + + _request(configOrUrl, config) { /*eslint no-param-reassign:0*/ // Allow for axios('example/url'[, config]) a la fetch API if (typeof configOrUrl === 'string') { diff --git a/test/unit/adapters/http.js b/test/unit/adapters/http.js index 67e01b1..bbfd093 100644 --- a/test/unit/adapters/http.js +++ b/test/unit/adapters/http.js @@ -53,6 +53,9 @@ function toleranceRange(positive, negative) { } } +const nodeVersion = process.versions.node.split('.').map(v => parseInt(v, 10)); +const nodeMajorVersion = nodeVersion[0]; + var noop = ()=> {}; const LOCAL_SERVER_URL = 'http://localhost:4444'; @@ -446,6 +449,57 @@ describe('supports http with nodejs', function () { }); }); + it('should wrap HTTP errors and keep stack', function (done) { + if (nodeMajorVersion <= 12) { + this.skip(); // node 12 support for async stack traces appears lacking + return; + } + server = http.createServer(function (req, res) { + res.statusCode = 400; + res.end(); + }).listen(4444, function () { + void assert.rejects( + async function findMeInStackTrace() { + await axios.head('http://localhost:4444/one') + }, + function (err) { + assert.equal(err.name, 'AxiosError') + assert.equal(err.isAxiosError, true) + const matches = [...err.stack.matchAll(/findMeInStackTrace/g)] + assert.equal(matches.length, 1, err.stack) + return true; + } + ).then(done).catch(done); + }); + }); + + it('should wrap interceptor errors and keep stack', function (done) { + if (nodeMajorVersion <= 12) { + this.skip(); // node 12 support for async stack traces appears lacking + return; + } + const axiosInstance = axios.create(); + axiosInstance.interceptors.request.use((res) => { + throw new Error('from request interceptor') + }); + server = http.createServer(function (req, res) { + res.end(); + }).listen(4444, function () { + void assert.rejects( + async function findMeInStackTrace() { + await axiosInstance.get('http://localhost:4444/one') + }, + function (err) { + assert.equal(err.name, 'Error') + assert.equal(err.message, 'from request interceptor') + const matches = [...err.stack.matchAll(/findMeInStackTrace/g)] + assert.equal(matches.length, 1, err.stack) + return true; + } + ).then(done).catch(done); + }); + }); + it('should preserve the HTTP verb on redirect', function (done) { server = http.createServer(function (req, res) { if (req.method.toLowerCase() !== "head") { @@ -1384,13 +1438,21 @@ describe('supports http with nodejs', function () { // call cancel() when the request has been sent, but a response has not been received source.cancel('Operation has been canceled.'); }).listen(4444, function () { - axios.get('http://localhost:4444/', { - cancelToken: source.token - }).catch(function (thrown) { - assert.ok(thrown instanceof axios.Cancel, 'Promise must be rejected with a CanceledError object'); - assert.equal(thrown.message, 'Operation has been canceled.'); - done(); - }); + void assert.rejects( + async function findMeInStackTrace() { + await axios.get('http://localhost:4444/', { + cancelToken: source.token + }); + }, + function (thrown) { + assert.ok(thrown instanceof axios.Cancel, 'Promise must be rejected with a CanceledError object'); + assert.equal(thrown.message, 'Operation has been canceled.'); + if (nodeMajorVersion > 12) { + assert.match(thrown.stack, /findMeInStackTrace/); + } + return true; + }, + ).then(done).catch(done); }); });