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
Fix broken timeout behavior in Node.js environment #2874
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
var success = false, failure = false; | ||
var error; | ||
|
||
axios.get('http://10.255.255.1/', { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this ip have special meaning? Can we use a non-existed host or something else?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. It's a one of well-known private address, which can be used for non-routable address.
In general, Dropping TCP SYN packet is common way to simulate connect timeout artificially.
Using http://google.com:81
as request URL will result same effect because Google's Firewall drops TCP SYN packet, but i thought using Google's URL is not reliable because Google can change their firewall policy in someday, which is unpredictable. So that's why i use non-routable address as request url.
Please see https://stackoverflow.com/questions/100841/artificially-create-a-connection-timeout-error for further details.
I'll add some comments to test code for explanation.
reject(createError('timeout of ' + config.timeout + 'ms exceeded', config, 'ECONNABORTED', req)); | ||
}); | ||
|
||
reject(createError('timeout of ' + config.timeout + 'ms exceeded', config, 'ETIMEDOUT', req)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ECONNABORTED is changed as ETIMEDOUT. Do you share the same idea with #1543?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ETIMEOUT on server side different with browser. the http request on nodejs while be block the js until the poll finished (successed or failed),. so the settimeout can not be executed when connection failed.
you can read the detail from here : https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, it was a unintended change. Sorry about that.
Anyway, Yes. I think it should be a ETIMEDOUT
.
As a axios consumer, There's no reliable ways to know given request was timed out or not. Current error does not help consumers to detect timeouts easily. Currently, It only can be done by matching error message like below:
const isTimeoutError = /timed?out/i.test(error.message);
// or
const isTimeoutError = error.message.includes("timeout");
I think it is not reliable way to match timeout errors.
Another reason why we should change error code is for clarity. ETIMEDOUT
error code is more clearer than ECONNABORTED
. because timeout was really happened so that's why we aborted request. I also think ECONNABORTED
is not actual root cause of given error.
Final reason why we should change error code is for consistency. Node.js built-in modules use ETIMEDOUT
error code for timeout errors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we think this would be a breaking change tho? Current users of axios might expect it to be ECONNABORTED
and might rely on that for their own implementations.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, It would be a breaking change. If user relied to ECONNABORTED
error code only. it would break their applications.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@chinesedfan Any plans to start using semver based versioning?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@fishcharlie Yes, next version should be 0.20 at least, which means breaking change because it is less than 1.0. And 0.19.1 is not a standard version.
// At this time, if we have a large number of request, nodejs will hang up some socket on background. and the number will up and up. | ||
// And then these socket which be hang up will devoring CPU little by little. | ||
// ClientRequest.setTimeout will be fired on the specify milliseconds, and can make sure that abort() will be fired after connect. | ||
req.setTimeout(config.timeout, function handleRequestTimeout() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If dropping connect timeout, users will complain about server problems that fixed by #1752. Maybe we should consider #1739 together, and got#timeout already gives a good example.
To keep things simple, we can only support partial kinds of timeout at the beginning.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, HTTPRequest#setTimeout
cannot detect connect timeout. I'm not sure it's intended or just a bug of Node.js core. Current HTTPRequest#setTimeout
implementation won't call Socket#setTimeout
internally if connect timeout was happened. Please refer to https://github.com/nodejs/node/blob/v13.12.0/lib/_http_client.js#L808-L812 . You'll notice Socket#setTimeout
will not be called until connection establish.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's a proof that HTTPRequest#setTimeout
cannot detect connect timeout due to described reason:
As you can see. timeout
event wasn't emitted, elapsed 1 minutes either. (macOS system default connect timeout)
It results to axios too. latest axios release (which is #1752 changes applied) cannot detect connect timeout either:
As you can see. axios v0.19.2 cannot detect connect timeout and elapsed 1 minutes (macOS system default connect timeout)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At this time, I do not support this PR. I'm not confident that the problems brought up in #1752 are sufficiently solved in this PR.
The PR specifically mentions a situation where with a large number of requests, it has the potential to choke the CPU. I do not see any tests or proof that this PR fixes that issue. Simply reverting the changes that PR makes, doesn't address the core issue.
Even after reading @mooyoul's comment on the main issue, although it discusses timeouts, nothing addressed the issue brought up by the original PR, and no indication was given that this fixes that issue.
I believe @ryouaki should give some input here and on #2710 if possible. It would have been nice if #1752 had some type of unit test or something that we could test against to attempt different solutions. That way we could have had a reproducible example of the issue @ryouaki was running into with more information. Maybe that is something to consider making a requirement before merging a PR in the future @chinesedfan?
Thank you for you replay. we should know that the event loop is different from nodejs and browser. on nodejs side, the connection event will poll until event finished and block js . so setTimeout can not be invoked on nodejs side. you can read the detail from https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/ About the test case.... I do not know how to write them .becasue only when the remote server is block (it can be connected and not failed, but do not response you) |
Hello all, I've spent time to investigate concerns about @fishcharlie pointed out. According to @ryouak (who proposed PR #1752) changed axios to use I was curious how So, first step was understanding how Here's a code snippet from Node.js core (or see https://github.com/nodejs/node/blob/v13.12.0/lib/_http_client.js#L789-L815) ClientRequest.prototype.setTimeout = function setTimeout(msecs, callback) {
if (this._ended) {
return this;
}
listenSocketTimeout(this);
msecs = getTimerDuration(msecs, 'msecs');
if (callback) this.once('timeout', callback);
if (this.socket) {
setSocketTimeout(this.socket, msecs);
} else {
this.once('socket', (sock) => setSocketTimeout(sock, msecs));
}
return this;
};
function setSocketTimeout(sock, msecs) {
if (sock.connecting) {
sock.once('connect', function() {
sock.setTimeout(msecs);
});
} else {
sock.setTimeout(msecs);
}
} As you can see.
That's all. Then how Here's a code snippet from Node.js core (or see https://github.com/nodejs/node/blob/v13.12.0/lib/internal/stream_base_commons.js#L224-L254) function setStreamTimeout(msecs, callback) {
if (this.destroyed)
return;
this.timeout = msecs;
// Type checking identical to timers.enroll()
msecs = getTimerDuration(msecs, 'msecs');
// Attempt to clear an existing timer in both cases -
// even if it will be rescheduled we don't want to leak an existing timer.
clearTimeout(this[kTimeout]);
if (msecs === 0) {
if (callback !== undefined) {
if (typeof callback !== 'function')
throw new ERR_INVALID_CALLBACK(callback);
this.removeListener('timeout', callback);
}
} else {
this[kTimeout] = setUnrefTimeout(this._onTimeout.bind(this), msecs);
if (this[kSession]) this[kSession][kUpdateTimer]();
if (callback !== undefined) {
if (typeof callback !== 'function')
throw new ERR_INVALID_CALLBACK(callback);
this.once('timeout', callback);
}
}
return this;
} As you can see, It creates a timer and update timer at every stream read/write completion. Anyway, It's still not clear how As you can see. As i earlier mentioned, As of my understanding, libuv does not block TCP connect at all. So calling Also, I've read the link which was @ryouaki actually faced: From mentioned issue, @ryouaki mentioned Also, According to his blog article (which was mentioned in upper linked issue):
So, my conclusion are:
|
d3d272c
to
05bb253
Compare
thank you for your comment
this is because connection is system's behavior and is sync. it will block the current thread unless you use another thread. So js only has one thread, so connection will block js. this is the flow below from nodejs.org
so this is why setTimeout for axios about timeout on server side can not work well. |
Have you read all of my comment? I know how Node's Event Loops works. I am not sure why are you mentioning about "system's behavior" and "sync". But to clarify, I would like to tell you some facts. The difference behavior between The
So, It is still not clear why we must use |
》》The poll phase processes I/O callbacks. It does not do actual I/O operations. It just calls I/O callbacks - Just like fs.readFile callback. Once again, All network operations are performed by libuv, in non-blocking manner. are you sure? The connection will block thread unless you do some thing with another thread. because TCP is blocked. For my case:
**but for my system is not. some request failed but blocked. I found that some timer did not execute and still hang up, because the number of [success] and [failed] is less than the number we [received] ** After I change HTTPRequest#setTimeout to Socket#setTimeout and analysis the logs which we report after change., the number of [success] and [failed] is always the same as number we [received] So I think for xhr HTTPRequest#setTimeout is ok, but for http. I do not think so. |
Thanks for your long discussions. After reading codes of sindresorhus/got, which provides lots for timeouts, I prefer @mooyoul's decision currently. And @ryouaki, would you mind explaining following two questions?
I think we are comparing
It was auto-translated from your post. Did that mean your server problem was caused by vue-router SSR? |
@chinesedfan @mooyoul @fishcharlie yesterday, I got the problem again with my new project.
So I think this is the same reason that the timer is blocked by TCP connection when no response from api which you requested. |
Have you ever considered using ClientRequest's constructor's timeout option to implement the so called Call Timeout? @mooyoul |
Hello! 👋 \n\nThis pull request is being automatically marked as stale because it has not been updated in a while. Please confirm that the issue is still present and reproducible. If no updates or new comments are received the pull request will be closed in a few days. \n\nThanks |
Please can you fix the conflicts in this branch, update it to the latest master, and let me know so I can review it fully. |
Background
Please see #2710 (comment) for details.
Currently,
timeout
behavior is different between Browser (XHR) and Node.js environment.It was caused by PR #1752, Affected axios versions are v0.19.1 and v0.19.2 (latest).
What's changed
It reverts changes of #1752, and adds additional tests for various timeout scenarios.