-
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
Aborting a fetch: The Next Generation #447
Comments
Having thought about it while writing the above, my gut-preference is for method(s) on the request object. It leaves the door open for cancellation tokens, which provide a better solution for service workers and response methods like |
Re: service workers watching for changes to a How do you feel about the |
When talking about the SW case, do you mean aborting the request sent to the network by the |
This is for the service worker case right? Yeah, I had the same thought. I dunno if that's possible. But if we need to go down that route it could be some kind of observable thing instead. |
Nope. Not going to explore that route closely. I recommend we don't base this API on CancelToken. |
Yeah. For example, I may react to a fetch for |
@benjamingr thanks, I'll update the OP. |
Personally, I'm in favor of that solution and it's also how it's typically implemented in other languages. I don't mind having programmers type a little more if they need abort functionality. I'm also fine with the solution @jar-ivan suggested: request(uri, {
cancel: somePromise
}) Where |
Okay, so, one thing I think is kind of muddying this design a bit, in terms of solutions that work for SW Service workers shouldn't have control over incoming requests. They can do whatever they want with the response the SW produces for the request, of course. If the SW wishes to "abort" an incoming request, it can respond with an appropriate 5XX(?) code denoting "Request Terminated" (and it could also pass some out-of-band signal back to whatever originated the request that would make that entity abort the request). And of course, a SW can control requests initiated the service worker, using But, as far as the incoming request goes, while there's certainly sense in making it possible to monitor the request's status, I think that listening is as far as the abstraction should sensibly go. A request coming into a SW should be effectively read-only, beyond control - the solution should be not just something that can differ from Although Service Workers have a privileged position in the browser's plumbing, in terms of design I still mentally model them as, effectively, a local proxy server. You can write a proxy server that stops listening to a request, but you can't make a server that controls the UA making a request. |
This one for reference. |
I still dislike mutating the Request with state like cancelation, etc, but I'm willing to accept it at this time. I just wish we had an object that represents the on-going fetch activity. Request is more "a fetch that can be performed" and a Response is "a fetch that has been performed". If we go with Also, should we allow script to tell if a Request is completed and
I'm not sure I understand this assertion. The entire ServiceWorker interception system is to allow it to gain some control over how that incoming request is processed. |
I'm +1 on both. |
To TL;DR my last comment a bit, we need to recognize that there are four concerns in play here, which I believe will be best served if they're recognized orthogonally:
Two of these concerns are about monitoring something out of the current code's control, two of these things are about controlling something the current code initiated, two of them are about requests, and two are about responses - but these are four different cases, and an appropriate design for any of these concerns needs to recognize which elements of the case it shares with which of its siblings. |
I'm personally against modifying the Request object, both because it fails to cancel the response (which will surprise people) and because it interacts badly with the various ways in which Requests are copied/cloned. The other solutions are all trade-offs, but so far I like either the controller pattern or the cancelling promise (@jan-ivar) pattern. The latter has the advantage of working with service workers: addEventListener('fetch', event => {
event.respondWith(
fetch('cat.jpg', {cancel: event.cancelled})
);
}); |
I agree that modifying the Request object feels very wrong (why not go all the way back to the XHR api...). I kind of like the cancelling promise pattern. It's conceptually simple and easy to understand (well, not sure what I would intuitively expect when the cancel promise is rejected, but other than that...), while providing all the necessary functionality. |
I think there's a strong argument for having something that represents the ongoing fetch:
Putting that on Request would get quite ugly. (Especially considering the Cache API and short lifetime of Request objects ( |
@benjamingr @jan-ivar I've updated the "token"s proposal in the OP to use the promise-based pattern you suggested. Cheers! |
@mkruisselbrink when you say "I kind of like the cancelling promise pattern", which pattern are you referring to? |
I agree with @annevk that having a representation of an ongoing fetch is the least hacky way to do this. However, I don't like the way you end up passing the controller out of the function that creates it: // eww
let controller;
fetch(url, {
control(c) {
controller = c;
}
}); This brings me back to "Add a method to the returned promise". We could make const controller = fetch(url);
const p = controller.then(response => …);
controller.abort();
p.abort === undefined; Something like const controller = fetch(url);
// Hand waving through the API a bit…
controller.observer.complete; // a promise that resolves, or rejects with a TypeError (network error) or AbortError (cancelled)
controller.observer.uploadProgress.subscribe(…);
controller.observer.priorityChange.subscribe(…); This could also appear on the addEventListener('fetch', event => {
const catFetch = fetch('cat.jpg');
event.fetchObserver.complete.catch(err => {
if (err.name == 'AbortError') catFetch.abort();
});
event.respondWith(catFetch);
}); You could even have a helper method to couple a fetch observer to another fetch. So changes to one fetch happen in other(s). We could ship the controller first, and leave the observable until later, especially if we want to use TC39 observables. Cons: This is a pretty big change. One of the points of contention in the previous thread was allowing someone you gave the promise to, to affect the outcome of the promise (eg, aborting it). Whoever you pass the promise to can already lock & drain the response stream, so it isn't really immutable. If you're passing a fetch promise to multiple people and expecting them to all be able to independently handle the response… you're going to have a bad time. If this becomes a blocker, the return type could switch on |
Passing the controller out of the function that creates it doesn't seem hacky at all to me - certainly no hackier than the way the |
If revealing constructors bother you so much, we could do something where you can also pre-construct the controller via Of course, this makes it so you run into a whole thorny woods of potential mis-uses like attempting to use the same Of course, you could want to design for a world where one controller can control multiple Fetches (it's a use case you described for tokens above), but then you'd have to grapple with all the things that breaking that symmetrical assurance would ruin, like |
@stuartpb I rarely need to pass resolve/reject out of the promise constructor, since the promise-related work is done within the constructor function. That doesn't seem anywhere near as likely here.
What are the pros of that vs returning a promise subclass from |
I'm not certain that |
Sharing my thoughts. I think cancellable promises are not the right way. I would keep Promises as values-in-time abstractions and I would not mess with fetch-specific details. I always thought fetch API is missing an intermediate type to represent the ongoing operation so I'm more inclined to create this control object once and for all. To be honest, I really like the In the future, it could offer the possibility of controlling multiple requests (to cancel all the fetches involved in the start/middle/end example) and keeps regular Following @wanderview suggestion, I would make For the big-stop-button case. I'm not sure if we should allow the requests to be continued. I'm happy with providing a way to notificate the SW about this action (or page closing) but perhaps we should stop all ongoing fetches since this is user's will. In this case, maybe If I haven't convinced you, I'm more inclined for @stuartpb Hope it helps. |
IMO aborting a fetch should signal a cancel stopping any upload of data from the request but also preventing the download of data from the response. For me that's the most important aspect of this, that it immediately stops both upload and download so we are not wasting bandwidth and freeing up the connection. This is super important for devices on slow networks and where bandwidth is metered by usage. If we only wanted to ignore or error the use of the response then that can be easily done in user land by wrapping the result in an observable. Thus we can do this today without any changes. Of course it would be nice to have a built-in standardized way to do this in the API, but nevertheless it can be done today as is. So I would hope that it remains a goal of this feature to stop the data transfers of both uploads and downloads, that is the more important goal in my opinion. |
@delapuente "cancelable promises" are not being suggested here. They've been abandoned. Or are you calling the In the subclass proposal, the returned value from const controller = fetch(url);
const p = controller.then(response => …);
controller.abort();
p.abort === undefined; |
@jakearchibald A controller that is also a promise, is magic. I think that's begging for trouble. I can see SO filling up with questions like: const controller = fetch(url).then(response => …);
controller.abort(); // Why is abort undefined??? Also, we should be forward-looking and use async functions in examples IMHO. Today I write: let response = await fetch(...args); Accessing the magic controller would force me to do this: let controller = fetch(...args);
let response = await controller; Which again is easy to get wrong: let controller = await fetch(...args);
let response = await controller;
controller.abort(); // Why is abort undefined??? To |
🎉 |
I know I'm late for the party and missed something, but why the attribute name was chosen to be just |
@sompylasar The |
Yep, if we introduce |
Guys from Google say it already work, but it still doesn't as of Chrome 63. |
@wzup the article says it works in Edge and Firefox, and points to https://bugs.chromium.org/p/chromium/issues/detail?id=750599 for Chrome, which isn't marked fixed yet... |
It looks like a change for this was landed on Feb 5, so based on the Chromium release schedule, I am guessing that it would show up in Chrome 66. Just a guess. |
it's on chrome canary (66) |
@joeyparrish That change added the
If you want, you can already write your own functions that can be aborted through an |
Ah, okay. Now I get it. Thanks for clarifying. |
@MattiasBuelens Which means we can't use the existence of the |
I couldn't find a reference to abortions automatically triggered by the browser in the specification. What happens to ongoing requests when you navigate away or close a tab? |
That's #153 and there's various issues against whatwg/html too. Someone needs to do a lot of research across browsers to get that nailed down more exactly. |
We were blocked on the resolution of cancelable promises, but consensus has broken down, so we need to find a new way.
How cancellation feels
(Hopefully
finally
will make it through TC39).If we want reusable code with Node.js we might want to consider actually minting a non-DOMException for this.
The fetch controller
You can get a controller object for the fetch:
The function name above is undecided.
This controller object will have methods to influence the in-progress fetch, such as
abort
. These methods will influence both the request and the response stream.Aborting an already aborted fetch will be a no-op, but there will be ways to query the final state of the fetch.
Other than the above, the design of the API is undecided.
The fetch observer
There will be an component that allows the developer to observe the progress of the fetch. This includes changes to priority, progress, final state (complete, cancelled, error etc). This component will include async getters for current state.
The observable component should use
addEventListener
rather than observables. We don't want a TC39 dependency, and true observables are likely to be compatible with addEventListener. We should be careful with the naming of this observer as a result.It's undecided how you'll get this object when calling fetch. The controller may extend it, the controller may have it as a property, or the two objects may be offered independently.
Other than the above, the design of the API is undecided.
Controlling multiple fetches at once
It should be relatively easy to cancel three fetches at once, or change their priority at the same time. If this is complicated to do in a low-level way, we may consider ways of creating a single controller that can be provided to multiple fetches, or a way to allow a controller to mimic another fetch through its observer.
How this happens is undecided.
Within a service worker
The observer component will be available to a fetch event via a property.
The property name above is undecided.
This should allow the service worker to hear about the page's intentions with the fetch in regards to priority and cancellation, and perform the same on the fetches it creates.
It seems unlikely that
event.respondWith(fetch(event.request))
would give you any of this for free.The text was updated successfully, but these errors were encountered: