Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve axios stack traces #2387

Closed
rclmenezes opened this issue Sep 3, 2019 · 37 comments · Fixed by #5987
Closed

Improve axios stack traces #2387

rclmenezes opened this issue Sep 3, 2019 · 37 comments · Fixed by #5987

Comments

@rclmenezes
Copy link

Currently, if you use axios with async / await, you get a stack trace like this:

Request failed with status code 500
    at createError (/Users/rmenezes/code/seedfi/web/node_modules/axios/lib/core/createError.js:16:15)
    at settle (/Users/rmenezes/code/seedfi/web/node_modules/axios/lib/core/settle.js:17:12)
    at IncomingMessage.handleStreamEnd (/Users/rmenezes/code/seedfi/web/node_modules/axios/lib/adapters/http.js:237:11)
    at IncomingMessage.emit (events.js:208:15)
    at IncomingMessage.EventEmitter.emit (domain.js:471:20)
    at endReadableNT (_stream_readable.js:1154:12)
    at processTicksAndRejections (internal/process/task_queues.js:77:11

It doesn't show you much about what called Axios., which makes debugging quite difficult!

This is because settle calls createError:
https://github.com/axios/axios/blob/master/lib/core/settle.js#L12-L24

Which creates a new error with a brand new stack trace:
https://github.com/axios/axios/blob/master/lib/core/createError.js#L15-L18

Perhaps we can fix this by creating an error before the request goes out and stitching it to the new error's stack trace?

@jmicco
Copy link

jmicco commented Oct 19, 2019

This makes it super hard to debug bad responses from other servers when you are making lots of calls.

@abihf
Copy link

abihf commented Oct 24, 2019

Maybe axios can use https://stackoverflow.com/a/42755876

@WarisR
Copy link

WarisR commented Dec 2, 2019

@jmicco I don't understand why this makes it super hard to debug.
Is it still super hard if I use this pattern? https://stackoverflow.com/a/42755876 (merge stacktrace)
Could you please give me more example? Thank you.

@jmicco
Copy link

jmicco commented Dec 9, 2019 via email

@abihf
Copy link

abihf commented Dec 9, 2019

Hi @jmicco

It seems the problem is axios discard original error and create new error instead of reusing original error (with original stack trace). Please correct me if i'm wrong.

Edit: nodejs already support async stack trace: https://thecodebarbarian.com/async-stack-traces-in-node-js-12

@captealc
Copy link

captealc commented Jan 7, 2020

Bump up. Turning on async stacktraces on newest node does not help.
This is a real issue, e.g when you make multiple HTTP calls and all you get is an info that some failed somewhere.

@cesco69
Copy link

cesco69 commented Jan 8, 2020

Another way is implement a new option for customize error message, eg.:

axios({
  method: 'get',
  url: 'http://bit.ly/2mTM3nY',
  customErrorMessage: (response) =>  'Request at URL '+ response.config.url +' failed with status code ' + response.status
})
.then(response=> console.log(response))
.catch(error => console.log(error));

this just require a minimal implementation in https://github.com/axios/axios/blob/master/lib/core/settle.js

'use strict';

var createError = require('./createError');

/**
 * Resolve or reject a Promise based on response status.
 *
 * @param {Function} resolve A function that resolves the promise.
 * @param {Function} reject A function that rejects the promise.
 * @param {object} response The response.
 */
module.exports = function settle(resolve, reject, response) {
  var validateStatus = response.config.validateStatus;
  if (!validateStatus || validateStatus(response.status)) {
    resolve(response);
  } else {
    var customErrorMessage = response.config.customErrorMessage;
    var message = customErrorMessage ? customErrorMessage(response) : 'Request failed with status code ' + response.status;
    reject(createError(
      message,
      response.config,
      null,
      response.request,
      response
    ));
  }
};

@rafaelalmeidatk
Copy link

In case anyone needs a solution for this, I have managed to solve the stack trace problem storing the current stack trace in a variable and replacing it before throwing the error:

const { stack } = new Error();

return instance.get(endpoint, { params, ...config }).catch(error => {
  error.stack = stackTrace;
  throw error;
});

I explained everything here: rafaelalmeidatk/TIL#4

@sadokmtir
Copy link

no updates regarding this issue ?

@jekh
Copy link

jekh commented Jul 1, 2020

I've managed to work around this issue by using interceptors. I create an Error in a request interceptor, store it on a config property, then use its stack trace in the response interceptor's error handler. This is obviously less than ideal and it would be better to not clobber the stack trace in the first place.

const client = axios.create();

client.interceptors.request.use(config => {
 config.errorContext = new Error("Thrown at:");
 return config;
});

client.interceptors.response.use(undefined, async error => {
  const originalStackTrace = error.config?.errorContext?.stack;
  if (originalStackTrace) {
    error.stack = `${error.stack}\n${originalStackTrace}`;
  }

  throw error;
});

This results in pretty reasonable stack traces:

Error: getaddrinfo ENOTFOUND junk.blackhole
    at GetAddrInfoReqWrap.onlookup [as oncomplete] (dns.js:64:26)
Error: Thrown at:
    at /home/code/node/src/utils/axios.ts:35:36
    at processTicksAndRejections (internal/process/task_queues.js:97:5)
    at IntegrationClient.getReport (/home/code/node/src/clients/IntegrationClient.ts:80:24)
Error: Request failed with status code 404
    at createError (/home/code/node/node_modules/axios/lib/core/createError.js:16:15)
    at settle (/home/code/node/node_modules/axios/lib/core/settle.js:17:12)
    at IncomingMessage.handleStreamEnd (/home/code/node/node_modules/axios/lib/adapters/http.js:236:11)
    at IncomingMessage.emit (events.js:322:22)
    at IncomingMessage.EventEmitter.emit (domain.js:482:12)
    at endReadableNT (_stream_readable.js:1187:12)
    at processTicksAndRejections (internal/process/task_queues.js:84:21)
Error: Thrown at:
    at /home/code/node/src/utils/axios.ts:35:36
    at processTicksAndRejections (internal/process/task_queues.js:97:5)
    at IntegrationClient.getReport (/home/code/node/src/clients/IntegrationClient.ts:80:24)

@philios33
Copy link

As a personal rule of thumb, due to this issue I always use

validateStatus: () => true

and handle the result.status code myself, throwing proper errors if I need to.

@svsool
Copy link

svsool commented Dec 3, 2020

The interceptors' approach proposed by @jekh for some reason was not showing the exact method that initiated a request for me, but I managed to get a similar stack trace by patching post, put, patch, delete, get, head, options, request axios methods. It's implemented as part of axios-better-stacktrace plugin. I hope it helps until the original problem is fixed.

@margalit
Copy link

margalit commented Feb 1, 2021

The interceptors' approach proposed by @jekh for some reason was not showing the exact method that initiated a request for me, but I managed to get a similar stack trace by patching post, put, patch, delete, get, head, options, request axios methods. It's implemented as part of axios-better-stacktrace plugin. I hope it helps until the original problem is fixed.

This worked perfectly for us. Thank you @svsool!

@croche-gl
Copy link

Some updates in this???

@jasonsaayman
Copy link
Member

Please use the interceptor approach mentioned above, we will address this in the future however currently an interceptor is the most appropriate solution.

@Merlin-Taylor
Copy link

I have replaced native Promises with Bluebird in my application. Bluebird supports long stack traces and I now have decent stack traces across async boundaries, including calls to axios methods.

@Frondor
Copy link

Frondor commented Aug 22, 2021

Please use the interceptor approach mentioned above, we will address this in the future however currently an interceptor is the most appropriate solution.

Hi @jasonsaayman, I see this issue brings lots of headaches and a bad developer experience to those who fall into these scenarios without any knowledge about the need of such interceptor.

I don't think that's the best solution and imho, it degrades Axios as a good library for production-ready applications.

A fix should be as easy as implementing something like

var error = new Error(message);
return enhanceError(error, config, code, request, response);

to

var axiosError = enhanceError(new Error(message), config, code, request, response);
if (Error.captureStackTrace) Error.captureStackTrace(axiosError, createError);
return axiosError;

Yes, it MAY degrade performance (imo its negligible) for failing requests, but we all know this is better than having no input for proper debugging.

I can open a PR with this and discuss the approach if you want.

@binarykitchen
Copy link

@Frondor yes PR please

@PetrShchukin
Copy link

Hi all, for me neither interceptors not axios-better-stacktrace package didn't help. Once it enters into an interceptor it looses all stack trace.
This is what I see when I do console.log(new Error()) before axios request

Error: Hello getMaterials
    at EthosProxyAPIService.getMaterials (C:\_CR\crossreg-core\src\internal-http\ethos-proxy-api\ethos-proxy-api.service.ts:69:17)
    at SubjectsService.test (C:\_CR\registration-api\src\apps\subjects\subjects.service.ts:11:38)
    at SubjectsController.getTest2 (C:\_CR\registration-api\src\apps\subjects\subjects.controller.ts:16:32)
    at C:\_CR\registration-api\node_modules\@nestjs\core\router\router-execution-context.js:38:29
    at InterceptorsConsumer.intercept (C:\_CR\registration-api\node_modules\@nestjs\core\interceptors\interceptors-consumer.js:11:20)
    at C:\_CR\registration-api\node_modules\@nestjs\core\router\router-execution-context.js:46:60
    at processTicksAndRejections (internal/process/task_queues.js:93:5)
    at C:\_CR\registration-api\node_modules\@nestjs\core\router\router-proxy.js:9:17

And this is what I see in my first interceptor

Error: Hello
    at C:\_CR\crossreg-core\src\context\interceptor-correlation.service.ts:16:19
    at C:\_CR\crossreg-core\node_modules\lodash\lodash.js:5177:46
    at processTicksAndRejections (internal/process/task_queues.js:93:5)

@bmaupin
Copy link

bmaupin commented Feb 14, 2022

I'm running into an issue where @jekh's solution isn't working for me.

I've defined both of the interceptors as suggested. In addition, we're adding a correlation ID to every request, e.g.

    client.interceptors.request.use((config) => {
      config.headers["x-correlation-id"] = correlator.getId();
      return config;
    });

I'm currently debugging an issue where correlator.getId() returns undefined.
Unfortunately, even with jekh's correlators the error context isn't set and I get an error without any context, e.g.

error Invalid value "undefined" for header "x-correlation-id" 
TypeError [ERR_HTTP_INVALID_HEADER_VALUE]: Invalid value "undefined" for header "x-correlation-id"
    at ClientRequest.setHeader (_http_outgoing.js:564:3)
    at new ClientRequest (_http_client.js:262:14)
    at Object.request (http.js:94:10)
    at RedirectableRequest._performRequest (node_modules/follow-redirects/index.js:265:24)
    at new RedirectableRequest (node_modules/follow-redirects/index.js:61:8)
    at Object.request (node_modules/follow-redirects/index.js:456:14)
    at dispatchHttpRequest (node_modules/axios/lib/adapters/http.js:202:25)
    at new Promise (<anonymous>)
    at httpAdapter (node_modules/axios/lib/adapters/http.js:46:10)
    at dispatchRequest (node_modules/axios/lib/core/dispatchRequest.js:53:10)

I'm guessing because the header isn't set, somehow the error handling is different, and the error object has an empty config in the request interceptor error handler.

I did find a workaround: our axios client is actually in a class, so in reality the first interceptor looks more like this:

    this.client.interceptors.request.use((config) => {
      config.errorContext = new Error("Thrown at:");
      return config;
    });

Since the config ends up being empty in the request interceptor error handler, I just save the error context as a class variable instead, e.g.

    this.client.interceptors.request.use((config) => {
      this.errorContext = new Error("Thrown at:");
      return config;
    });

    this.client.interceptors.response.use(undefined, async (error) => {
      if (this.errorContext?.stack) {
        error.stack = `${error.stack}\n${this.errorContext.stack}`;
      }
      throw error;
    });

And now I have the error context once again:

error Invalid value "undefined" for header "x-correlation-id" 
TypeError [ERR_HTTP_INVALID_HEADER_VALUE]: Invalid value "undefined" for header "x-correlation-id"
    at ClientRequest.setHeader (_http_outgoing.js:564:3)
    at new ClientRequest (_http_client.js:262:14)
    at Object.request (http.js:94:10)
    at RedirectableRequest._performRequest (node_modules/follow-redirects/index.js:265:24)
    at new RedirectableRequest (node_modules/follow-redirects/index.js:61:8)
    at Object.request (node_modules/follow-redirects/index.js:456:14)
    at dispatchHttpRequest (node_modules/axios/lib/adapters/http.js:202:25)
    at new Promise (<anonymous>)
    at httpAdapter (node_modules/axios/lib/adapters/http.js:46:10)
    at dispatchRequest (node_modules/axios/lib/core/dispatchRequest.js:53:10)
Error: Thrown at:
    at clients/HttpClient.js:51:27
    at processTicksAndRejections (internal/process/task_queues.js:95:5)
    at async Object.findAccessPoints (services/data/accessPointDataService.js:26:18)
    at async Object.validateAccessPointForImporting (services/accessPointService.js:130:37)
    at async controllers/accessPointController.js:248:13
    at async Promise.all (index 48)

@bytenik
Copy link

bytenik commented Mar 30, 2022

I have replaced native Promises with Bluebird in my application. Bluebird supports long stack traces and I now have decent stack traces across async boundaries, including calls to axios methods.

How did you do this in a way that Axios then uses Bluebird promises?

@despreston
Copy link
Contributor

Please use the interceptor approach mentioned above, we will address this in the future however currently an interceptor is the most appropriate solution.

Hi @jasonsaayman, I see this issue brings lots of headaches and a bad developer experience to those who fall into these scenarios without any knowledge about the need of such interceptor.

I don't think that's the best solution and imho, it degrades Axios as a good library for production-ready applications.

A fix should be as easy as implementing something like

var error = new Error(message);
return enhanceError(error, config, code, request, response);

to

var axiosError = enhanceError(new Error(message), config, code, request, response);
if (Error.captureStackTrace) Error.captureStackTrace(axiosError, createError);
return axiosError;

Yes, it MAY degrade performance (imo its negligible) for failing requests, but we all know this is better than having no input for proper debugging.

I can open a PR with this and discuss the approach if you want.

@jasonsaayman I didnt see any PRs from the original commenter and I'm happy to complete this. Is this still relevant?

@jasonsaayman
Copy link
Member

You can for sure, please check master we made a whole load of changes to the way errors work for the next release

despreston added a commit to despreston/axios that referenced this issue Apr 25, 2022
Related to discussions here axios#2387

Attempt to capture the stack trace at the time the error is created in
order to maintain context about where the error originates from.
jasonsaayman added a commit that referenced this issue May 3, 2022
Related to discussions here #2387

Attempt to capture the stack trace at the time the error is created in
order to maintain context about where the error originates from.

Co-authored-by: Jay <jasonsaayman@gmail.com>
@doronhorwitz
Copy link

doronhorwitz commented May 30, 2023

Please use the interceptor approach mentioned above, we will address this in the future however currently an interceptor is the most appropriate solution.

Hi @jasonsaayman, I see this issue brings lots of headaches and a bad developer experience to those who fall into these scenarios without any knowledge about the need of such interceptor.
I don't think that's the best solution and imho, it degrades Axios as a good library for production-ready applications.
A fix should be as easy as implementing something like

var error = new Error(message);
return enhanceError(error, config, code, request, response);

to

var axiosError = enhanceError(new Error(message), config, code, request, response);
if (Error.captureStackTrace) Error.captureStackTrace(axiosError, createError);
return axiosError;

Yes, it MAY degrade performance (imo its negligible) for failing requests, but we all know this is better than having no input for proper debugging.
I can open a PR with this and discuss the approach if you want.

@jasonsaayman I didnt see any PRs from the original commenter and I'm happy to complete this. Is this still relevant?

Did @despreston's pull request (which was an implementation of @Frondor's suggestion) actually fix the problem? I'm running Node v18.16.0 and axios v1.4.0 and still getting stacktraces like what @rclmenezes was seeing at the beginning of this issue.

@emilio-notable
Copy link

emilio-notable commented May 30, 2023 via email

rajr5 pushed a commit to rajr5/axios that referenced this issue Jun 24, 2023
Related to discussions here axios/axios#2387

Attempt to capture the stack trace at the time the error is created in
order to maintain context about where the error originates from.

Co-authored-by: Jay <jasonsaayman@gmail.com>
@AndyOooh
Copy link

AndyOooh commented Aug 1, 2023

Please use the interceptor approach mentioned above, we will address this in the future however currently an interceptor is the most appropriate solution.

Hi @jasonsaayman, I see this issue brings lots of headaches and a bad developer experience to those who fall into these scenarios without any knowledge about the need of such interceptor.
I don't think that's the best solution and imho, it degrades Axios as a good library for production-ready applications.
A fix should be as easy as implementing something like

var error = new Error(message);
return enhanceError(error, config, code, request, response);

to

var axiosError = enhanceError(new Error(message), config, code, request, response);
if (Error.captureStackTrace) Error.captureStackTrace(axiosError, createError);
return axiosError;

Yes, it MAY degrade performance (imo its negligible) for failing requests, but we all know this is better than having no input for proper debugging.
I can open a PR with this and discuss the approach if you want.

@jasonsaayman I didnt see any PRs from the original commenter and I'm happy to complete this. Is this still relevant?

Did @despreston's pull request (which was an implementation of @Frondor's suggestion) actually fix the problem? I'm running Node v18.16.0 and axios v1.4.0 and still getting stacktraces like what @rclmenezes was seeing at the beginning of this issue.

Same for me running same versions of Node and axios. When making identical requests made with fetch gives me the stack trace I need but with axios 1.4.0 I only get the library stack.
Is there some config that needs to be set?

Stack trace:

Error: getaddrinfo ENOTFOUND sonplaceholder.typicode.com
at AxiosError.from (/home/bunny/projects/axiosTest/node_modules/axios/dist/node/axios.cjs:836:14)
at RedirectableRequest.handleRequestError (/home/bunny/projects/axiosTest/node_modules/axios/dist/node/axios.cjs:3010:25)
at RedirectableRequest.emit (node:events:513:28)
at eventHandlers. (/home/bunny/projects/axiosTest/node_modules/follow-redirects/index.js:14:24)
at ClientRequest.emit (node:events:513:28)
at TLSSocket.socketErrorListener (node:_http_client:502:9)
at TLSSocket.emit (node:events:513:28)
at emitErrorNT (node:internal/streams/destroy:151:8)
at emitErrorCloseNT (node:internal/streams/destroy:116:3)
at process.processTicksAndRejections (node:internal/process/task_queues:82:21)

@jasonsaayman
Copy link
Member

will have a look

@jedkass
Copy link

jedkass commented Sep 25, 2023

@jasonsaayman Were you possibly able to confirm @despreston's change? Does 1.5.0 address this in full?

@miso-belica
Copy link

@jedkass for me the stack traces are still quite useless. At least in Sentry with Axios v1.5.0. I am running Node.js v20.7.0.

Screenshot of stack trace from Sentry

image

@jedkass
Copy link

jedkass commented Sep 25, 2023

Ah that's a shame. @jasonsaayman Shouldn't we reopen this issue in that case? Or is this being tracked elsewhere?

@jasonsaayman
Copy link
Member

reopening, that sucks

@jasonsaayman jasonsaayman reopened this Sep 29, 2023
@andrew0
Copy link

andrew0 commented Oct 6, 2023

I also ran into this issue and was able to get more usable stack traces by applying this patch to axios@1.5.0:

diff --git a/dist/node/axios.cjs b/dist/node/axios.cjs
index adce59e4733b2520cff2c7354411fb3d32dfdd75..224ba7d01dc3a53b3e62ede6566275864f53a669 100644
--- a/dist/node/axios.cjs
+++ b/dist/node/axios.cjs
@@ -3779,7 +3779,19 @@ class Axios {
    *
    * @returns {Promise} The Promise to be fulfilled
    */
-  request(configOrUrl, config) {
+  async request(configOrUrl, config) {
+    try {
+      return await this._dispatchRequest(configOrUrl, config);
+    } catch (err) {
+      if (err instanceof Error) {
+        Error.captureStackTrace(err, Axios.prototype.request);
+      }
+
+      throw err;
+    }
+  }
+
+  _dispatchRequest(configOrUrl, config) {
     /*eslint no-param-reassign:0*/
     // Allow for axios('example/url'[, config]) a la fetch API
     if (typeof configOrUrl === 'string') {

The same patch could be applied to lib/core/Axios.js if you're using ESM.

After applying that patch and running this script:

const axios = require('axios');

async function makeRequest() {
  await axios.get('https://httpstat.us/500');
}

async function main() {
  await makeRequest();
}

main().catch((err) => {
  console.error(err.stack);
});

I get this stacktrace:

AxiosError: Request failed with status code 500
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async makeRequest (test.js:4:3)
    at async main (test.js:8:3)

@ikonst
Copy link
Contributor

ikonst commented Oct 6, 2023

This is a pretty good place to add it, given how request can call all those interceptors. But we should probably attach the original exception as a cause.

@ikonst
Copy link
Contributor

ikonst commented Oct 6, 2023

Also, not sure how standard Error.captureStackTrace is. It's in V8 but not in Firefox.

@ikonst
Copy link
Contributor

ikonst commented Oct 6, 2023

@andrew0 made #5987 based on your suggestion

@rickgm
Copy link

rickgm commented Oct 26, 2023

@andrew0 made #5987 based on your suggestion

@jasonsaayman will you consider merging this?

@jedkass
Copy link

jedkass commented Jan 24, 2024

@jasonsaayman @andrew0 It looks like @ikonst's PR has been idle for a little bit since its last review. Is there any chance we might be able to get another round of review on it? 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet