This is an archived snapshot of W3C's public bugzilla bug tracker, decommissioned in April 2019. Please see the home page for more details.

Bug 27033 - XHR request termination doesn't terminate queued tasks
Summary: XHR request termination doesn't terminate queued tasks
Status: RESOLVED WORKSFORME
Alias: None
Product: WebAppsWG
Classification: Unclassified
Component: XHR (show other bugs)
Version: unspecified
Hardware: All All
: P2 normal
Target Milestone: ---
Assignee: Anne
QA Contact: public-webapps-bugzilla
URL:
Whiteboard:
Keywords:
Depends on:
Blocks:
 
Reported: 2014-10-13 16:57 UTC by Manish Goregaokar
Modified: 2017-03-08 13:26 UTC (History)
5 users (show)

See Also:


Attachments

Description Manish Goregaokar 2014-10-13 16:57:25 UTC
https://xhr.spec.whatwg.org/

When a request is terminated via `abort()` or the timeout, the fetch algorithm is the only thing that is terminated[1]


However, there's no indication that we need to terminate existing queued up tasks for the XHR object.

For example, the following can happen:

 - Initialiaze an XHR object, call send()
 - On readystatechange to 2 or 3, in the event handler, perform a long computation
    - In the meantime, the fetch algorithm continues to fetch, and pushes fetch-related events (eg "process response body"/"process response end of file") to the DOM task queue.
 - abort() is called once the computation finishes. This aborts the fetch algorithm, but does *not* remove already-queued events
 - After calling abort(), try to initialize and send a new request with the same XHR object
 - The queued up tasks from the previous request will now be received by the XHR object assuming they are for the new request, and we'll have some sort of frankenresponse.

The w3 spec[2] explicitly mentions that tasks in the queue for the XHR object should be aborted. I'm not certain if this includes the currently running task -- if, for example, the user calls abort() followed by a new open() and send() in the onprogress handler, should the onload/onloadend handlers be called for the previous request? Probably not -- but this involves checking for an abort on each step of the send(), which seems icky.

I guess the WHATWG spec needs to be updated to terminate more than just the fetch algorithm.

 [1]: https://xhr.spec.whatwg.org/#terminate-the-request
 [2]: http://www.w3.org/TR/XMLHttpRequest2/#the-abort-method
Comment 1 Manish Goregaokar 2014-10-13 17:00:04 UTC
Credit to Mukilan Thiagarajan for finding this out. He has a test page here[1] which demonstrates some of the issues. It behaves differently in Firefox, Servo, and Chrome (Servo follows the WHATWG spec, and we suspect that Firefox has a broken implementation)

 [1]: http://mukilan.github.io/servo_xhr_abort.html
Comment 2 Anne 2014-10-15 12:40:18 UTC
So once fetch is terminated no more tasks for that fetch will be queued. But the question here is that the tasks have been queued but not yet processed and it is terminated then.

So I guess https://fetch.spec.whatwg.org/#concept-fetch-terminate should be updated to remove any queued tasks as well just to be safe.
Comment 3 Manish Goregaokar 2014-10-15 13:24:33 UTC
Sounds good. 

What about currently running tasks? Do we break at the next step as defined in the spec?

For example, abort() can be called within an event handler (let's say readystatechange at LOADING) -- should the progress event be fired as well?
Comment 4 Anne 2014-10-15 13:33:26 UTC
(In reply to Manish Goregaokar from comment #3)
> For example, abort() can be called within an event handler (let's say
> readystatechange at LOADING) -- should the progress event be fired as well?

Yeah, the idea was that would still dispatch. We could add more state to prevent this, but it does not seem worth it.
Comment 5 Manish Goregaokar 2014-10-15 13:35:41 UTC
Hm, alright. Seems a bit strange, but better than having checks all over the place.

Also, the original w3 spec lets the user terminate abort() [as in, not go further in the steps] by calling open() during an onging abort (i.e. in the onabort event handler). Should we do the same here? "If open() is called in any of the event handlers, do not go to the next step n the abort() algorithm"
Comment 6 Anne 2014-10-15 13:39:45 UTC
The only change I can think of would be to only change the state to unsent if the current state is done, if that is what implementations are actually doing.
Comment 7 Anne 2014-10-15 15:03:05 UTC
Hallvord, is there a test covering comment 5? Invoke open() from onabort?
Comment 8 Hallvord R. M. Steen 2014-10-16 10:11:29 UTC
(In reply to Anne from comment #7)
> Hallvord, is there a test covering comment 5? Invoke open() from onabort?

No, we don't have such a test but I'll add one right now (a small tweak on http://w3c-test.org/XMLHttpRequest/open-after-abort.htm should do fine)
Comment 9 Hallvord R. M. Steen 2014-10-16 12:21:27 UTC
Heh, it was a bit tricky and browsers are a bit inconsistent..
https://github.com/w3c/web-platform-tests/pull/1294
Comment 10 Hallvord R. M. Steen 2014-10-19 22:21:26 UTC
(In reply to Anne from comment #6)
> The only change I can think of would be to only change the state to unsent
> if the current state is done, if that is what implementations are actually
> doing.

As far as I can tell, all implementations change to 4 due to abort() and 1 due to open() (even when open() is called during abort processing). This is pretty consistent. Everything else is .. all over the place. I haven't found two implementations that fire the same events in the same order with the same states.

The last iteration of my two tests in https://github.com/w3c/web-platform-tests/pull/1294 log a number of events and does a console.log() to better inspect what browsers are actually up to.

* Webkit (as seen in Opera and Chrome, Safari not tested) doesn't fire any onabort events at all if you call abort() in the OPENED state. Hence these tests aren't really valid in Webkit-based browsers. If we want to require these abort events to fire, we can probably just ignore the Webkit family for now.

* Trident (1st below) and Gecko (2nd below) differ in this part of the sequence of events in open-during-abort-processing (where abort() is called from onloadstart and open() is called from the readystatechange event that fires due to abort() being called):

readyState after abort() 1
onloadstart readyState 1

onloadstart readyState 1
client.onabort 1
readyState after abort() 1

so in IE, abort() returns before onloadstart fires again, but in Gecko it's the other way around and there's an extra abort event firing even though open() was called. I don't know why it fails to fire upload.onabort here though.. client.onabort fires in spite of open() but upload.onabort doesn't? Weird.

The other test, where open() is called from upload.onabort, has these logs:
IE:
"readyState before abort() 1
upload.onabort - before open() 4
readyState after open() 1
readyState after abort() 1
client.onload 4
client.onloadend 4"

Gecko:
"readyState before abort() 1
client.onabort 4
client.onloadend 4
upload.onabort - before open() 4
readyState after open() 1
readyState after abort() 1
client.onload 4
client.onloadend 4"

Gecko has a known spec violation in failing to fire events on upload before client. IE consistently terminates abort processing and fires no more abort events when open() is called.

It may be noteworthy that implementations consistently let open() prevent the abort processing's onloadend event from firing. AFAIK the spec is out of tune with implementations here.
Comment 11 Mukilan Thiyagarajan 2014-10-20 13:19:02 UTC
> It may be noteworthy that implementations consistently let open() prevent
> the abort processing's onloadend event from firing. AFAIK the spec is out of
> tune with implementations here.

I'm seeing the opposite behaviour on both Firefox (32.0) and Chrome (38.0.2125.104). Here is a test page that demonstrates the issue:

http://mukilan.github.io/servo_xhr_abort_and_open.html

In this test case, open is called from the onabort event handler and yet the loadend event is fired. Chrome does not even terminate the processing of the state change event from 2 to 3 (LOADING) from where we invoke abort(), and proceeds to fire the 'progress' event associated with the state change to 'LOADING', after the request has been aborted.
Comment 12 Mukilan Thiyagarajan 2014-10-20 14:11:34 UTC
(In reply to Anne from comment #4)
> (In reply to Manish Goregaokar from comment #3)
> > For example, abort() can be called within an event handler (let's say
> > readystatechange at LOADING) -- should the progress event be fired as well?
> 
> Yeah, the idea was that would still dispatch. We could add more state to
> prevent this, but it does not seem worth it.

Chrome does prevent the triggering of the subsequent load and loadend events when abort() is called from the readystatehandler (state = 4). However Firefox does not exhibit this behaviour. 

Test page:
http://mukilan.github.io/servo_xhr_abort_prevents_progress_events.html


Also, should Timeout and Network error events behave the same way as abort? i.e if a timeout or network error happens while inside the readystate event handler, should the subsequent "progress", "load", "loadend" events not be triggered?
Comment 13 Anne 2014-10-20 15:18:09 UTC
(In reply to Hallvord R. M. Steen from comment #10)
> As far as I can tell, all implementations change to 4 due to abort() and 1
> due to open() (even when open() is called during abort processing). This is
> pretty consistent.

Are you saying that they do not eventually change it to UNSENT in case of abort() (when open() is not called)? Because I believe that part of the specification is actually tested.


> * Webkit (as seen in Opera and Chrome, Safari not tested) doesn't fire any
> onabort events at all if you call abort() in the OPENED state. Hence these
> tests aren't really valid in Webkit-based browsers. If we want to require
> these abort events to fire, we can probably just ignore the Webkit family
> for now.

This is OPENED with the send() flag set, correct?


> It may be noteworthy that implementations consistently let open() prevent
> the abort processing's onloadend event from firing. AFAIK the spec is out of
> tune with implementations here.

So if that's the case we'd need to add a new flag "open()/abort() flag" or some such, that is checked before an event is fired.
Comment 14 Anne 2014-10-20 15:20:03 UTC
(In reply to Mukilan Thiyagarajan from comment #12)
> Also, should Timeout and Network error events behave the same way as abort?
> i.e if a timeout or network error happens while inside the readystate event
> handler, should the subsequent "progress", "load", "loadend" events not be
> triggered?

Timeout can only happen until the server has send back a response. The same is true for a network error.
Comment 15 Mukilan Thiyagarajan 2014-10-20 17:03:14 UTC
(In reply to Anne from comment #14)
> (In reply to Mukilan Thiyagarajan from comment #12)
> > Also, should Timeout and Network error events behave the same way as abort?
> > i.e if a timeout or network error happens while inside the readystate event
> > handler, should the subsequent "progress", "load", "loadend" events not be
> > triggered?
> 
> Timeout can only happen until the server has send back a response. The same
> is true for a network error.

I apologize, I should not have listed the "load" and "loadend" events. The scenario I mentioned is possible when a "readystatechange" event is fired while transitioning from "HEADERS_RECIEVED" to the "LOADING" state (as a part of the 'process response body'). While in the handler, the request is not complete yet and could still timeout or result in an error. What happens in such cases? Should the subsequent 'progress' event still be called?
Comment 16 Anne 2014-10-20 17:18:07 UTC
No, so per the specification timeout can no longer happen at that point, as you reached the point where you have received headers. The same is true for network error. If the network drops at that point, only Content-Length can tell you did not really get the full response, but we don't turn that into an error for the purposes of XMLHttpRequest.
Comment 17 Mukilan Thiyagarajan 2014-10-20 18:30:41 UTC
(In reply to Anne from comment #16)
> No, so per the specification timeout can no longer happen at that point, as
> you reached the point where you have received headers. 

I tested this in both Firefox and Chrome and both fire a timeout event after the headers are received.

Here is  a test page where you can experiment with different timeout values.

http://mukilan.github.io/xhr_timeout_and_headers_received.html

Depending on your network latency, for some values you can observe that timeouts do get triggered after the headers are received.

In my case a value of 500 ms resulted in the following events:
Event Type: readystatechange; Ready State: 1
Event Type: readystatechange; Ready State: 2 
Event Type: readystatechange; Ready State: 3
Event Type: readystatechange; Ready State: 3
Event Type: readystatechange; Ready State: 3
Event Type: readystatechange; Ready State: 3
Event Type: readystatechange; Ready State: 3
Event Type: readystatechange; Ready State: 3
Event Type: readystatechange; Ready State: 3
Event Type: readystatechange; Ready State: 3
Event Type: readystatechange; Ready State: 4 
Event Type: timeout; Ready State: 4

The spec says that a non-zero timeout will:

> cause fetching to terminate after the given time has passed. When the time 
> has passed, the request has not yet completed [...],

But neither the XHR spec nor the HTTP Fetch spec defines *when* a request is considered to be complete.
Comment 18 Hallvord R. M. Steen 2014-10-20 21:53:15 UTC
(In reply to Mukilan Thiyagarajan from comment #11)
> > It may be noteworthy that implementations consistently let open() prevent
> > the abort processing's onloadend event from firing. AFAIK the spec is out of
> > tune with implementations here.
> 
> I'm seeing the opposite behaviour on both Firefox (32.0) and Chrome
> (38.0.2125.104). Here is a test page that demonstrates the issue:
> 
> http://mukilan.github.io/servo_xhr_abort_and_open.html

Interesting - here you call abort() at a much later stage, with readyState being 3 already and the content basically delivered.

BTW - your test with a suitable pass condition would be a welcome addition to https://github.com/w3c/web-platform-tests/tree/master/XMLHttpRequest !

(In reply to Anne from comment #13)

> Are you saying that they do not eventually change it to UNSENT in case of
> abort() (when open() is not called)? Because I believe that part of the
> specification is actually tested.

Indeed it is - for example here:
http://w3c-test.org/XMLHttpRequest/abort-after-receive.htm

> > * Webkit (as seen in Opera and Chrome, Safari not tested) doesn't fire any
> > onabort events at all if you call abort() in the OPENED state.
> This is OPENED with the send() flag set, correct?

Correct, I presume - in for example the open-during-abort-processing test, abort() is called from an onloadstart event handler, should happen right after the send() flag is set.

> > It may be noteworthy that implementations consistently let open() prevent
> > the abort processing's onloadend event from firing. AFAIK the spec is out of
> > tune with implementations here.
> 
> So if that's the case we'd need to add a new flag "open()/abort() flag" or
> some such, that is checked before an event is fired.

An "actually reset" flag?
What if open() simply un-sets the send() flag, and those events are only fired if the send() flag is set? 

(BTW - is it a spec bug that the open() method steps do not say "unset the send() flag"? Seems like it should, i.e. 

xhr.send()
xhr.open()
xhr.setRequestHeader('foo', 'bar')

should presumably not throw.)
Comment 19 Anne 2014-10-21 15:16:19 UTC
Okay, so let's approach this from a different angle. These are the ways XMLHttpRequest can be terminated:

* open()
* abort()
* timeout (thanks Mukilan)
* navigating the "containing" browsing context
* user pressing "ESC"

Each of these can trigger a sequence of events. The question is whether all those events need to be dispatched if you invoke one of these from a handler.

open() changes the state from anything to OPENED.

abort() changes the state to DONE first, then dispatches events, then changes to UNSENT.

timeout changes the state to DONE first, then dispatches events.

The last two behave roughly similar to timeout.


Here is my proposal:

* open() keeps doing what it does.
* abort() and timeout check the state of the object after each dispatched event. If it is no longer DONE, they terminate those steps. (This way when open() is invoked the events stop. If abort() is invoked (perhaps again) they stop too.)
* abort() only changes to UNSENT in the end if the state is DONE.


(And yes, it does seem like open() should unset the send() flag. Will fix that now.)
Comment 20 Anne 2017-03-08 13:26:43 UTC
Based on https://github.com/w3c/web-platform-tests/pull/1294 it seems that Chrome almost never evaluates state after going down the "request error steps" path and Firefox only after the readystatechange event. I think the simplest is to keep the standard as-is and just emit some extra events in this rather contrived scenarios.

I'll file bugs against Firefox and Safari once those tests land.