whatwg / fetch

Fetch Standard
https://fetch.spec.whatwg.org/
Other
2.11k stars 329 forks source link

Add timeout option #20

Closed wheresrhys closed 2 years ago

wheresrhys commented 9 years ago

Most ajax libraries and xhr itself offer a convenient way to set a timeout for a request. Adding timeout to the properties accepted in the options object therefore seems like a sensible addition to fetch.

https://github.com/github/fetch/pull/68

CamiloTerevinto commented 7 years ago

@annevk thanks, I did not see that issue.

@jakearchibald is there any timeline for that to be production-ready or are you implying I can use that already? I also have the same doubt as matlubner.

jokeyrhyme commented 7 years ago

@CamiloTerevinto @mattlubner I believe const response = await fetch(url) gives you a response object that is not necessarily "done", but provides you with body methods to process content that will eventually be available

const response = await fetch(url)
// if you abort here, then some downloading may have happened, but you can avoid the rest
const text = await response.text()
// if you abort here, then you just downloaded the whole thing, wasteful!
annevk commented 7 years ago

It gives you a response with headers, but without a response body. A response with headers is the earliest signal we're exposing in the web platform. (Note that it doesn't necessarily mean that the request body has been fully transmitted, though typically that's the case.)

jakearchibald commented 7 years ago

@CamiloTerevinto Edge & Firefox are already implementing. Chrome will start shortly. I don't know about Safari. Timelines for production-ready depend on the release schedule of individual browsers.

CamiloTerevinto commented 7 years ago

@jakearchibald this might not be the right place, I'm aware, but I was looking into this for the react-native fetch implementation (which as far as I know, relies on this library).

HyperSimon commented 6 years ago

@domenic 因噎废食

Angelk90 commented 6 years ago

@alcat2008 : Hello, I tried the code you proposed but it does not seem to work. In a nutshell, the page to which I refer to a redirect, this is the problem. Can you give me some advice?

JSONRice commented 6 years ago

@Angelk90 et al, you might want to have a look at this AjaxService I wrote:

https://github.com/github/fetch/issues/617

IMHO node-fetch gives you everything the native Fetch API within ES6 does and it provides some additional features including a timeout

xgqfrms commented 6 years ago

credentials: "include"

https://developers.google.com/web/updates/2015/03/introduction-to-fetch

image

FranklinYu commented 6 years ago

@xgqfrms How on earth is that related to this issue?

ImanMh commented 5 years ago

2018! do we have timeout?

taralx commented 5 years ago

76% yes: https://caniuse.com/#feat=abortcontroller

FranklinYu commented 5 years ago

According to Can I Use link above, Safari doesn't support AbortController, but MDN says otherwise: https://developer.mozilla.org//en-US/docs/Web/API/AbortController#Browser_compatibility

How can I verify this? I tried this JavaScript code:

const controller = new AbortController()
const signal = controller.signal

document.getElementById('download').addEventListener('click', fetchVideo)
document.getElementById('abort').addEventListener('click', () => {
    controller.abort()
    console.log('Download aborted')
})

// some random large image I found online
const url = 'https://upload.wikimedia.org/wikipedia/commons/6/6e/Monasterio_Khor_Virap%2C_Armenia%2C_2016-10-01%2C_DD_25.jpg'

function fetchVideo() {
    fetch(url, {signal})
        .then( r => r.blob() )
        .then( () => console.log('success') )
        .catch( e => console.error('Download error: ' + e.message) )
}

but my Internet is too fast, and the image finished in a second so I cannot hit "Abort" before that. Any other way to test it?

I wish Safari has the throttling like Chrome...

JSONRice commented 5 years ago

@FranklinYu the easiest way to verify if it works is to run it in Safari. 💯

jameshartig commented 5 years ago

@FranklinYu you can use settimeout.io and just request a 5s timeout. For example: https://settimeout.io/5s

styfle commented 5 years ago

@FranklinYu It exists but it doesn't work.

Safari has window.AbortController defined in the DOM but it's just a stub, it does not abort requests at all. The same issue also affects Chrome on IOS and Firefox on IOS because they use the same WebKit rendering engine as Safari.

See caniuse

JSONRice commented 5 years ago

@FranklinYu I believe that there are tools to slow down your connection with Safari else just go to a local coffee shop. For Safari a quick Google search reveals:

https://spin.atomicobject.com/2016/01/05/simulating-poor-network-connectivity-mac-osx/

Though that is from 2016. You'll have to try it out.

FranklinYu commented 5 years ago

@styfle I saw that note, but I would like to verify this myself before I submit pull request to https://github.com/mdn/browser-compat-data.

@fastest963's suggestion is right on the spot. I'm going to fix the compatibility table.

update

Damn, I'm an idiot. W3C officially provide powerful test page.

https://w3c-test.org/fetch/api/abort/general.any.html

ianstormtaylor commented 5 years ago

Has there been any talk of improving the timeout ergonomics? Now that the whole AbortController thing has been hashed out?

It seems like… given the nature of HTTP/TCP, no request is ever guaranteed to respond to you, and the agreed upon solution for this is to use timeouts. Such that pretty much every request you make should have some form of timeout. (Just like every request you make should account for network errors.)

Given that, it seems problematic that it takes…

const controller = new AbortController();
const signal = controller.signal;
const fetchPromise = fetch(url, {signal});
const timeoutId = setTimeout(() => controller.abort(), 5000);
const response = await fetchPromise;
clearTimeout(timeoutId);

…to properly send a request with a timeout.

Has anyone discussed adding a timeout: number shorthand option? (It was mentioned in the very first comment back in 2015, but it seems like the UX might have gotten lost in the quest for true cancellation?)

fetch(url, {
  timeout: 5000
})
taralx commented 5 years ago

If you're not making a huge number of requests, you can probably skip the clearTimeout step. Then you can make a timeoutSignal(ms) function pretty easily.

Pauan commented 5 years ago

@ianstormtaylor I think there's some complicated questions about the timeout.

For example, if you specify timeout: 5000, should the timeout also include things like response.json()? I imagine most of the time the answer is "yes", but then what if you don't call response.json()?

In other words, how does fetch know that you're "done", so it can cancel the timer?

So I think this is a better design:

async function with_timeout(ms, f) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), ms);

    try {
        return await f(controller.signal);

    } finally {
        clearTimeout(timeoutId);
    }
}

Now you can use it like this:

with_timeout(5000, (signal) => fetch(url, { signal }))

Or if you want it to also cancel the response:

with_timeout(5000, async (signal) => {
    const response = await fetch(url, { signal });
    return await response.json();
})

This is a general solution which isn't tied to fetch, so you can also use with_timeout in other areas, like Streams.

ianstormtaylor commented 5 years ago

@Pauan why would you want it to include response.json()? I don't think including it gains you anything.

The way "timeout" is used in most libraries is pretty clear: if you haven't received a response in that amount of time then it's considered failed. And it's defined that way because that's how it's useful to guard against the uncertainty of the network. But if you're about to call response.json that means you've already received a response. There's no need to have the timeout extend to that part of the code because there's no longer any uncertainty there?

@Pauan @taralx As for the withTimeout helpers, I agree that those can be created. Thanks for the examples! I just think timeouts are such a common case (so much so that arguably you're not writing "complete" code if you aren't including a timeout). And that for common cases like that the API should help you do the right thing, which in this case would be:

fetch(url, {
  timeout: 5000
})

This seems like at least one thing XHR got right? xhr.timeout = 500.

ImanMh commented 5 years ago

@ianstormtaylor you got this very correctly. This thread has become the place where some people are trying to convince the others that 'timeout' must be included. I know that I can implement it my self but the entire library can also be implemented. The point is that this library looks incomplete and unfinished without timeout functionality. So let's write timeout here instead of expecting everyone to write their own version of it, which is also the whole point of making a library.

Bnaya commented 5 years ago

but the entire library can also be implemented

This is incorrect. also fetch is not a library. fetch is the most low level http client api that the browser provides. (Even more than XHR) It first goal is to be powerful primitives without un-handled cases (eg, what do we do with response.body.json()) before it tries to give more high level features, that can be implemented in user land.

ImanMh commented 5 years ago

@bnaya you are right, There are a lot of fetch libraries out there that are very much similar to the standards in this repo. My point is that adding timeout to these standards will just improve it and prevents others to write unnecessary extra code.

Pauan commented 5 years ago

@ianstormtaylor But if you're about to call response.json that means you've already received a response. There's no need to have the timeout extend to that part of the code because there's no longer any uncertainty there?

That's not true: when fetch resolves that just means that the headers have been received, the body has not been received.

So when you fetch the body (using response.json() or response.text() or similar), it can fail. That's why they return Promises. So you need the timeout to cover that case as well.

And that for common cases like that the API should help you do the right thing

That's my point: the "right thing" in most cases is that the timeout should extend to retrieving the body. That's very easy to do if you're using with_timeout, but tricky to define if timeout is native to fetch.

This seems like at least one thing XHR got right?

XHR completes when the body is received. fetch completes when the headers are received. They're not the same thing.

I agree that XHR's behavior is correct, which is why the timeout for fetch must include fetching the body. That's what with_timeout does.

Mouvedia commented 5 years ago

So when you fetch the body (using response.json() or response.text() or similar), it can fail.

Can you be more precise about this please. In general it can throw if it's locked, has already been read or canceled. For example:

Where does it say that it can fail because the body wasn't received? It's a stream, hence it's handled.

Pauan commented 5 years ago

@Mouvedia The server might hang (not send a response). That's the whole point of a timeout, to handle the situation where the server doesn't send data (or sends the data way too slowly).

Mouvedia commented 5 years ago

@Pauan Could you point out the type of error—from the spec—that this would produce?

Pauan commented 5 years ago

@Mouvedia You are misunderstanding. Failure is not the same as an error. There is no error if the server fails to send data, there is simply nothing.

That is why you are supposed to add in a timeout. The timeout will throw an AbortError if the server takes too long to send the data:

https://dom.spec.whatwg.org/#abortcontroller-api-integration

Mouvedia commented 5 years ago

I think you are misunderstanding me.

there is simply nothing.

There is never "nothing". If the "server hangs" and it happens during the streaming, you won't receive another chunk.

Therefore the timeout should be taken into account until the read's reader returns done as true. This cannot be achieved if you are only consuming the body without using getReader.

Pauan commented 5 years ago

@Mouvedia "Not receiving another chunk" is "nothing". That's the point: you have not received the data, and the server has not sent the data, so you have to sit there with nothing while you wait for the server to send the data.

I'm not sure what you're arguing for. I have always argued that the timeout should include the body (in most situations), which is exactly what my with_timeout does.

The problem with making timeout native to fetch is that you might not retrieve the body, you might call fetch solely to check the headers. So in that case the timeout shouldn't take into account the body. So how does fetch know whether the timeout should take into account the body or not?

The with_timeout function does not have that problem, it makes it easy to choose whether the timeout should include the body or not.

Mouvedia commented 5 years ago

As you can read here, XHR's timeout is only taken into account during "fetching". We should match that behaviour to achieve consistency.

ref https://fetch.spec.whatwg.org/#concept-fetch

karlhorky commented 5 years ago

XHR completes when the body is received. fetch completes when the headers are received. They're not the same thing.

@Pauan Then maybe the timeout for fetch should take this into account? Adding one more property to @ianstormtaylor's proposal could be a possible solution, no?

fetch(url, {
  timeout: 5000,
  timeoutIncludesBody: true, // default: false
})

Defaulting this to false is so that the default behavior of fetch (to complete when the headers are received) is respected. This is to provide the least surprising API when one knows how fetch works.

I don't think that the solution to "keep this in userland" is great here, because this is a very common use case for non-trivial apps. It should be built into the platform.

Mouvedia commented 5 years ago

It's inaccurate or incomplete.

XHR completes when the body is received. fetch completes when the headers are received.

XHR has readystate 3 which corresponds to HEADERS_RECEIVED. We gotta strive for parity with XHR.

ref https://stackoverflow.com/a/40877968/248058

jokeyrhyme commented 5 years ago

There's a potentially different kind of timeout here: https://aws.amazon.com/blogs/aws/elb-idle-timeout-control/ An "idle timeout", for when there is no activity on a connection for X seconds

e.g. with an idle timeout of 15 seconds:

I don't think it is always the intention for a timeout to only consider the time until the first byte

I don't think it is always the intention for a timeout to consider full receipt of the headers and body

But each of these ways of defining timeouts is useful in different situations

Pauan commented 5 years ago

@Mouvedia As the spec clearly shows (in step 14), XHR will wait for both the headers and the body to complete. The timeout for XHR includes the body. Please read the spec more carefully.

XHR has readystate 3 which corresponds to HEADERS_RECEIVED. We gotta strive for parity with XHR.

The load event will only fire when the body has been received. The timeout cares about the load event, not readystate.

XHR is not the same as fetch. XHR always retrieves the body, whereas fetch lets you choose whether to retrieve the body or not. That's why fetch needs to have a choice whether the timeout includes the body or not.


@jokeyrhyme That's a good point, that complicates the timeout even more. Which is another argument for having this in user-land, so that way the user has full control, rather than adding in several options to fetch.

FranklinYu commented 5 years ago

If the "server hangs" and it happens during the streaming, you won't receive another chunk.

@Mouvedia Yes, you won't receive another chunk, but the promise of .text() won't resolve either. It simply wait for the server to recover (possibly up until the network/system timeout which could be 5 minutes or similar).

FranklinYu commented 5 years ago
  • a slow connection that trickles out the headers at 10 seconds and a body chunk every 10 seconds will NOT trigger the idle timeout

@jokeyrhyme I doubt whether this can be detected in JavaScript. Now that the body is a stream, is browser allowed to buffer the response? Is browser supposed to trigger any JavaScript event as soon as it receive a single byte from server?

ImanMh commented 4 years ago

This thread is open since 2015 and the issue is still present and I don't think this is considered an issue at all, I'm switching to Axios it has built it timeout.

annevk commented 3 years ago

I noticed some misunderstanding about fetch() above. It will retrieve the body, just like XMLHttpRequest. So timeout would work similarly.

We're considering adding timeout support to AbortSignal: https://github.com/whatwg/dom/issues/951. What I'm wondering is whether people need to distinguish between the different reasons a fetch might have aborted or whether that's immaterial.

Mouvedia commented 3 years ago

What's annoying is non-parity with XHR: fetch will call the signal's abort handler even if the request has been completed—XHR, as expected, doesn't. After the completion the callback should become a no-op.

annevk commented 3 years ago

That doesn't make sense. You pass a signal to fetch() and that will react to the abort signal. If the fetch has completed it'll no-op.

Mouvedia commented 3 years ago

<off topic>

If the fetch has completed it'll no-op.

Are you talking about the specification or the implementations? Do you have tests for that? </off topic>


Anyway what I meant is, please try to strive for parity with XHR. We don't want more gotchas: the specification needs to be explicit. Ill give you another example, in XHR abortion behaviour on page quit wasn't clear at first. So first and foremost parity, then you can move on and add new functionalities that are missing. i.e. don't reactively change how XHR behaves in light of what you are trying to add to fetch (because XHR is now explained in terms of fetch)

annevk commented 3 years ago

I'm not sure I follow. There are open issues around document unloading, but they affect XMLHttpRequest and fetch() equally. And I wasn't even talking about adding anything to fetch here...

Mouvedia commented 3 years ago

And I wasn't even talking about adding anything to fetch here...

From the point of view of developers, adding timeout to AbortSignal is directly related to XHR obviously.

We're considering adding timeout support to AbortSignal: whatwg/dom#951.

Are you adding timeout or deadline?

annevk commented 3 years ago

It's on top of signal, so deadline, which is also what XMLHttpRequest has. However, we might add signals to reading from a stream, which would allow for the former.

Mouvedia commented 3 years ago

Dunno what the others think but it would be better if we could stay consistent regarding terminology. Personally I don't care what it is called as long as I have at least XHR parity.

ririko5834 commented 3 years ago

How can I add timeout to request now?

saschanaz commented 2 years ago

Should this be considered fixed as we now have AbortSignal.timeout?