-
Notifications
You must be signed in to change notification settings - Fork 337
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
Add a timeout
option, to prevent hanging
#951
Comments
I don't think the code example comparisons are fair. Here is something more realistic: const controller = new AbortController();
setTimeout(() => controller.abort(), 5000);
fetch('https://example.com', { signal: controller.signal }); or, if you have a tiny utility library fetch('https://example.com', { signal: timeoutSignal(5000) }); compared to fetch('https://example.com', { timeout: 5000 }); If anything, it might make sense to add something like But if you are making a purely ergonomic argument, it's best not to exaggerate your point with unnecessary |
@ianstormtaylor The problem with your solution is that it only covers the common case but falls apart in even slightly uncommon cases. For example: const res = await fetch('https://example.com', { timeout: 10000 });
const shouldFetch = await checkSomething(res);
if (shouldFetch) {
return await res.json();
} In this case the timeout will cover only the headers, not the body. This is very surprising and unexpected, it will lead to subtle bugs that are hard to find and debug. That's why I said this issue is tricky, and it's why I still advocate for explicitly indicating the scope of the timeout. This makes it absolutely 100% clear what is covered by the timeout, so it prevents bugs. And the code is short, so it's not a big burden on the developer. So if you're going to have a |
@domenic Sorry! My goal was not to exaggerate things at all. My example was actually copied and modified from #20 (comment) and I didn't try to optimize it. You're totally right that it can be simplified a little bit though—I've just edited the initial example to simplify it! Thanks. I feel like the gravity of the situation didn't come across though… This is something that needs to happen for pretty much every For that level of severity, I do not think it's possible that the current And I'm not talking about utility libraries on purpose. Again… I'm talking about the average use of @Pauan I think you've missed the core of what I'm saying. (Not to mention the example you gave seems kind of incomplete, and doesn't seem grounded in any real-world use cases.) I'm talking about how the That's the whole point. It seems like you're not thinking in terms of the user's expectation. The Again… the core of what I'm talking about is that the current This problem is widespread. This blog post comes up as one of the first results for "fetch javascript timeout" in Google, but it actually still can hang indefinitely when you This accepted stackoverflow answer has the same issue, it can infinitely hang. This is widespread. Having a simple |
I believe timeout logic belongs to the userland. Any browser async callback potentially can be postponed to forever. I can't imagine handling everything feature by feature. Maybe I am overlooking something and maybe it can be useful to cancel request but as far I understand it, Promise like API cancelation is breaking the promise itself. https://gcanti.github.io/fp-ts-contrib/modules/Task/withTimeout.ts.html Related https://medium.com/@benlesh/promise-cancellation-is-dead-long-live-promise-cancellation-c6601f1f5082 |
Can you please help me to understand why I am wrong? Thank you. |
timeout
optiontimeout
option, to prevent hanging
That's not the point, of course it's a contrived example. My point is that (with your solution) people will eventually write code similar to that, and their code will now be broken, but they won't realize that it's broken. That's a big problem.
I am well aware of the user's expectations. My point is that your proposal does not work, from a technical perspective.
You have to look at it from the implementation's perspective. With your proposal, if the user does not access the body, then the timeout is cancelled. If the user does access the body, then the timeout is extended to cover the body. But in my example, it uses Remember, at this point |
@Pauan the proposal absolutely works from a technical perspective. And I know it does—if you read my opening issue carefully—because it's the technique that Go already uses for the This proposal gives people a very clear contract:
That's actually exactly the contract people want in all the most common cases of using It's beautiful because no matter what you do with the response, you're guaranteed that the request will never hang indefinitely while you're working with it. Eliminating that inherent uncertainty from TCP is the goal when using a timeout in the first place.
That's not true. The timeout is not cancelled, it continues to tick until it finishes, or the user has fully read the body. The way the timeout works is actually pretty simple:
If you look at the different cases that people are using
Covering everything that's being read—and explicitly not choosing just the headers, or just the body—is actually the goal of the You're describing an extreme edge case in the 1% of cases above. It's very easy to document (as Go has done) that |
userland |
Agree that it should be in the userland, it's like you said
AbortController can... fetch is a lower level API
If you want it to apply to every request then you should look into using service worker and modify each request (acting a bit as a MITM). Or make this implementation yourself in a mini-lib like @domenic mention. most ppl basically wrap fetch in there own http-client since they need to inject some authentication in each request, then you could as well create a abort controller if they pass in a timeout option |
@sibelius More constructive please? Saying "do it in userland" misses the entire point of this issue. It's already possible to do in userland—I know that. But people aren't doing it correctly because the API doesn't have good UX. Most of the popular code samples that show adding timeouts to fetch do it incorrectly, and leave open the infinite hanging failure mode. And that's not even counting the code that just doesn't add timeouts because It's extremely hard to debug these infinite hangs when they happen, because they can happen at any layer of the callstack above where It's critical that people use timeouts. And to make that happen it's critical that it be easy. Asking people to master entirely new @jimmywarting Saying " And because timeouts are not handled nicely in the spec, those polyfill libraries are incapable of adding The options are:
|
I certainly appreciate there's a demand for timeout support, but opening duplicates is not the way to go about it. #179 (comment) still applies, though we should maybe explore some alternatives as @domenic proposes. I would also recommend that you keep your posts brief and to the point. Take the time to edit and elide unnecessary information so the 140 folks following the conversation don't have to. |
@annevk sorry! I read a lot of the different offshoot issues from #20, but somehow missed #179, or I absolutely would have just commented there instead. Thanks.
Fair enough. I did do a lot of editing actually. Based on #20 (not having seen #179) I was of the impression that the general opinion was "userland only, timeouts are solved". And when you're arguing to revisit a contentious topic, if you aren't thorough people will close it saying "we've already covered this". It's a bit of a catch-22. From #179 it seems like you were after more information, so maybe some of the initial writeup can be helpful. Looking forward to |
It is inevitable, the numbers of Web API's and people is too big. |
Not happened yet at 2021? |
abort signal is the way |
// Fetch the URL, cancelling after 8 seconds
fetch(url, { signal: AbortSignal.timeout(8000) }); Apparently available in Firefox 100 and Chrome 103 beta!! |
I'd like to propose adding a
timeout
option to the Fetch API.Prior issue: I know this was in #20, but that discussion was long and winding. One of the blockers was aborting requests, which was a big rabbit hole, but is now solved with
AbortController
! So I'd like to open this fresh issue to discuss the importance and ergonomics of timeouts.Please bare with me! I just want a blank slate to argue the case…
Timeouts are critical!
Before talking implementation… it's important to reiterate just how critical timeouts are. Since TCP/HTTP is inherently unpredictable, you can't actually guarantee that any call to
fetch
will ever resolve or reject. It can very easily just hang forever waiting for a response. You have no guarantee of a response. To reiterate:Any HTTP request without a timeout might hang your code forever.
That should scare you if you're trying to write stable code. Because this applies at all levels of the callstack. If you call into an
async
function that callsfetch
without a timeout, you can still hang forever.The tried-and-true solution for this uncertainty is timeouts. Pretty much every HTTP-requesting library has a way to use them. As long as you've set a timeout for a response, you've returned to a state of certainty. Your request might fail, sure, but at least you're now guaranteed not to be left in an infinitely hanging state.
That is the critical piece: timeouts eliminate uncertainty.
I think Python's
requests
documentation sums up the severity nicely (emphasis mine):It underscores just how widespread the use case is. If your code is striving to be stable and bug-free, then every call to
fetch
should have an associated timeout.But to make that goal possible, specifying a timeout needs to be easy. Right now it's not.
Prior concern: In #20, people brought up other types of timeouts, like "idle timeouts", which ensure that at least some data is received for a request every interval of time. These can be useful, but they are definitely not the 90% case. And importantly, they don't actually eliminate the infinite hanging uncertainty. Other types of timeouts can either be implemented in userland or taken up in a separate issue if there's a need.
AbortController
is great, but not enough.The
AbortController
andAbortSignal
features are great! And they seem really well designed for use across different APIs and for low-level use cases. I have no qualms with them at all, and I think they are perfect to use to make atimeout
property available.But I don't think that just adding them has solved "timeouts" for Fetch. Because they aren't ergonomic enough to offer a good solution for the common case.
Right now you'd need to do...
This is a lot of code.
It's too much code for a use case that is so common. (Remember, this is something that should be done for almost every call to
fetch
.) Not only is it a lot, it requires learning and mastering a newAbortController
concept, just to get a guarantee of certainty forfetch
. For most users this is unnecessary.And it's easy to get wrong too!
Most people want to wait for the entire body to be received (notice how the timeout is cleared after
res.json()
), but most examples of usingAbortController
in the wild right now do not properly handle this case, leaving them in an uncertain state.Not only that, but prior to
AbortController
(and still now) people would use libraries likep-timeout
and almost certainly added unexpected bugs because it is common to see people recommend things like:That example also has the potential to hang forever!
What's the ideal UX?
Most people are currently using either no timeouts, or incorrectly written timeouts that still leave their code in a potentially infinitely hanging state. And these subtle bugs are only getting more common as more and more people switch to using the
isomorphic-fetch
ornode-fetch
packages. (And remember, this bubbles up the callstack!)I think this is something that
fetch
should solve. And to do that, we really need something as simple as:It needs to be simple, because it needs to be something you can add to any call to
fetch
and be guaranteed that it will no longer hang forever. Simple enough that is "just works" as expected. Which means that if people are reading the body (and they often are), the timeout should cover you when reading the body too.Prior concern: In #20, people brought up that because
fetch
breaks responses down into "headers" and "body" across two promises, it's unclear what atimeout
property should apply to. I think this is actually not a problem, and there's a good solution. (Keep reading! It's a solution that is used in Go for theirTimeout
parameter.)A potential solution…
For the
timeout
option to match user expectations, it needs to incorporate reading the full body when they do.This is just how people think about HTTP responses—it's the 90% use case. They will absolutely expect that
timeout
encompasses time to read the entire response they're working with, not just the headers. (This mental model is also why people are incorrectly libraries likep-timeout
to add timeouts right now.)However! Keep reading before you assume things…
The decision does not have to be the black-and-white "either the timeout only applies to the headers, or it only applies to the body" that was dismissed in #20. It can just as easily apply to either just the headers, or both the headers and the body, in a way that ensures it always meets user expectations and gives them the guarantee of certainty.
This is similar to how Go handles their default
Timeout
parameter:This is smart, because it allows
timeout
to adapt to how the user's code handles the response. If you read the body, the timeout covers it. If you don't you don't need to worry either.Here's what that looks like in terms of where errors are thrown…
To summarize what's going on there in English...
The
timeout
property can be passed intofetch
calls, which specifies a total duration for the read operations on a response. If only the headers are read, the timeout only applies to the headers. If both the headers and body are read the timeout applies to the headers and body.A real example.
So you can do:
Which guarantees you received that full JSON body in 10 seconds.
Or, if you don't care about the body...
Which guarantees you receive the headers in 1 second.
This aligns well with user expectations and use cases. As far as a user is concerned, they can set a
timeout
property, and be covered from network uncertainty regardless of how much of the response they read. By settingtimeout
, they are saved from the infinitely hanging bug.What about
$my_edge_case
?To be clear, I'm not saying that this
timeout
option handles every single need for timeouts under the sun. That's impossible.There are concepts like "idle timeouts" that are unrelated to the uncertainty of TCP. And there are always going to be advanced use cases where you want a timeout for only the initial connection, or only for reading the headers, etc.
This proposal does not try to address those advanced cases.
It's about improving the ergonomics for the 90% of cases that should have a timeout already today, but likely don't.
The
timeout
option is… optional. It's opt-in. And when you do opt-in to it, it gives you the guarantee of certainty—that your calls tofetch()
andres.json()
will no longer hang indefinitely, no matter how much of the response you choose to read or not. That's it job. And it does it in a way that matches user expectations for the 90% use cases.Anyone who needs more explicit/complex timeout logic can always use the existing
AbortController
pattern, which is designed well enough to handle any use case you can throw at it.Why not do it in userland?
It's already possible to do in userland. But people aren't doing it correctly because right now the API doesn't have good ergonomics. Most of the popular code samples that show adding timeouts to fetch do it incorrectly, and leave open the infinite hanging failure mode. And that's not even counting the code that just doesn't add timeouts because
fetch
's current ergonomics don't make it easy.It's extremely hard to debug these infinite hangs when they happen, because they can happen at any layer of the callstack above where
fetch
is called. Anyasync
function that has a dependency call a buggyfetch
can hang indefinitely.It's critical that people use timeouts. And to make that happen it's critical that it be easy.
Asking people to master entirely new
AbortController
andAbortSignal
concepts for the 99% use case of timing out a request is not a smart thing to do if you're looking to help people write stable code. (Nothing wrong with those two concepts, they just shouldn't be involved in the common case because they are super low-level.)And the argument that "
fetch
is a low-level API" also misses the point. People are increasingly usingfetch
,isomorphic-fetch
,node-fetch
, etc. in non-low-level places. They are using it as their only HTTP dependency for their apps, because bundle size is important in JavaScript.And because timeouts are not handled nicely in the spec, those polyfill libraries are incapable of adding
timeout
options themselves.In summary…
fetch
without a timeout is not guaranteed to resolve or reject.fetch
polyfills.fetch
aren't ergonomic enough.timeout
option that meets user expectations.Thanks for reading!
The text was updated successfully, but these errors were encountered: