Skip to content
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

Clarify how to cancel a navigation #3447

Closed
zcorpan opened this issue Feb 5, 2018 · 6 comments · Fixed by #6315
Closed

Clarify how to cancel a navigation #3447

zcorpan opened this issue Feb 5, 2018 · 6 comments · Fixed by #6315

Comments

@zcorpan
Copy link
Member

zcorpan commented Feb 5, 2018

@rwaldron and I reasoned a bit around these tests web-platform-tests/wpt#8558 and found a possible problem with how HTML cancels navigation attempts. It won't remove any "navigate" tasks that still exist on the task queue. Is that intentional?

Consider this test

https://github.com/w3c/web-platform-tests/pull/8558/files#diff-cc01cb4065c5670fb1bf4b19c88fd562

click() will do

https://html.spec.whatwg.org/#the-a-element:following-hyperlinks-2-2
and step 14 in
https://html.spec.whatwg.org/#following-hyperlinks-2

Queue a task to navigate the target browsing context to resource. If replace is true, the navigation must be performed with replacement enabled. The source browsing context must be source.

Note "queue a task".

Then the test also has

onclick="document.querySelector('iframe').src='javascript:`<script>parent.verifyNavigation(false);</script>`';"

https://html.spec.whatwg.org/#the-iframe-element:attr-iframe-src-4
https://html.spec.whatwg.org/#otherwise-steps-for-iframe-or-frame-elements
which step 4 says to navigate, without queueing a task:

Navigate the element's nested browsing context to resource.

So the iframe src navigation starts first (and it will queue another task for https://html.spec.whatwg.org/#javascript-protocol ). Then the hyperlink navigation starts, which cancels the src navigation.

If the above analysis is correct and the behavior is intentional, we should probably clarify in the spec that canceling navigations does not mean to remove tasks from the task queue. OTOH, if removing tasks is a thing browsers do, we should specify that. Maybe each browsing context can have an internal slot to track the most recent navigation to cancel, and have an abstract operation that handles queued task case as well as started case.


Places where the spec cancels navigations:

https://html.spec.whatwg.org/multipage/browsing-the-web.html#navigate

step 6:

Cancel any preexisting but not yet mature attempt to navigate browsingContext, including canceling any instances of the fetch algorithm started by those attempts. If one of those attempts has already created and initialized a new Document object, abort that Document also. (Navigation attempts that have matured already have session history entries, and are therefore handled during the update the session history with the new page algorithm, later.)

https://html.spec.whatwg.org/#traverse-the-history-by-a-delta

step 5.1:

If there is an ongoing attempt to navigate specified browsing context that has not yet matured (i.e. it has not passed the point of making its Document the active document), then cancel that attempt to navigate the browsing context.

@TimothyGu
Copy link
Member

Another place where navigation is canceled:

https://html.spec.whatwg.org/multipage/window-object.html#dom-window-stop

The stop() method on Window objects should, if there is an existing attempt to navigate the browsing context and that attempt is not currently running the unload a document algorithm, cancel that navigation; ...

(And soon that algorithm will be called in document.open() through #3999 also.)

aarongable pushed a commit to chromium/chromium that referenced this issue Sep 10, 2018
Currently, we have different behaviors for the "having a provisional
document loader" state versus the "having a queued navigation" state. In
the first case, we call FrameLoader::StopAllLoaders(), which cancels the
ongoing navigation as well as fetches on the current page (e.g.
XMLHttpRequest). In the second, we merely cancel the task to navigate,
but do NOT cancel fetches.

Indeed, it is recognized that the spec is currently unclear about
canceling queued navigation vs. direct navigation (see [1]). However, it
is worth noting that Chrome does not always follow the spec with this
distinction in the first place (through location.href, for example,
which queues a navigation task in Chrome but navigates directly in
spec).

Additionally, since even the current code cancels navigation in both
circumstances (the only disagreement being if peripheral fetches are
also canceled), we see no reason to have an inconsistency in this regard
(see [2]).

This CL now always calls FrameLoader::StopAllLoaders(), for both when we
have a provisional loader and when we have a queued navigation, thus
ridding ourselves of the inconsistency.

By doing so, we implement the "ideal 2" plan laid out in [2], which
recently became part of the HTML Standard in [3]. Tests for this new
behavior (which this CL fully passes) are in [4], which was imported
into our tree by the WPT Importer bot, whose expectations this CL now
change.

[1]: whatwg/html#3447
[2]: whatwg/html#3975
[3]: whatwg/html#3999
[4]: web-platform-tests/wpt#10789

Bug: 866274
Change-Id: I4e3ffac6b7c07bc8da812f6f210ab5d6933bdfd1
Reviewed-on: https://chromium-review.googlesource.com/1195837
Commit-Queue: Timothy Gu <[email protected]>
Reviewed-by: Nate Chapin <[email protected]>
Reviewed-by: Kent Tamura <[email protected]>
Cr-Commit-Position: refs/heads/master@{#590011}
@bzbarsky
Copy link
Contributor

See also #3730

I was recently looking at this area of code in Firefox, and it looks like Firefox queues a task to navigate from "follow a hyperlink" but not from form submission or location href sets. #3730 suggests that some browsers do it for location href sets too. In any case, there's an interop problem here. This testcase:

<a href="http://example.com"></a>
<script>
  document.querySelector("a").click();
  location.href = "http://software.hixie.ch/utilities/cgi/test-tools/delayed-file?pause=2&mime=text%2Fplain&text=Testing.";
</script>

ends up loading example.com in Firefox but the software.hixie.ch page in Chrome and Safari. Per spec as written it should load the example.com page, but that seems pretty weird to me.

My preference for this stuff in a vacuum would be that neither following a hyperlink nor location sets queue a task. A task would still be queued for javascript: execution under navigation. But I'd really like to know what exactly Chrome and Safari do here.

@domenic do you know what Chrome does or who would know?

@rniwa same thing for Safari: do you know what the actual behavior is or who would know?

@annevk

@domenic
Copy link
Member

domenic commented Jan 14, 2019

Based on #3730 I'm going to guess that @zetafunction is the right person on Chrome to describe our navigation/task-queuing behavior. Indeed, this is quite an interop mess.

@fergald
Copy link

fergald commented Sep 25, 2020

Does anyone have a non-racy example of a use case for stopping navigations on document.open? By racy I mean, e.g. if the navigation is triggered in a JS task and the open is called in a subsequent task, then this is racy as it depends on the navigation taking some amount of time - if it's quick enough we don't make it to the open. It seems that to reliably depend on this feature you would have to request the navigation and then later in the same task call open and I'm curious if this is something that is actually done in reality. Would it be OK to spec this as requesting cancellation of ongoing navigation without guaranteeing it so that all uses of this are equally racy? Pages that really need to end their tasks with if (we_didnt_call_open) {do_that_navigation()} Maybe not a very attractive proposition...

The reason I ask is that this is looking like the only place where the page gets synchronous control over cross-document navigation (if you know of others, please tell me) and we might end up implementing that if under the hood.

@domenic
Copy link
Member

domenic commented Sep 25, 2020

I suspect a common pattern that isn't racy in current browsers is

location.href = "foo";
// ...later in the same task...
document.open();

@domenic
Copy link
Member

domenic commented May 25, 2021

The reason I ask is that this is looking like the only place where the page gets synchronous control over cross-document navigation (if you know of others, please tell me)

I think window.stop() is also specced to synchronously cancel cross-document navigations, currently.

aarongable pushed a commit to chromium/chromium that referenced this issue Jul 19, 2022
…ncellationThrottle

Renderer-initiated navigations can be cancelled from the JS task it was
initiated from, e.g. if the script runs window.stop() after initiating
the navigation. See also whatwg/html#3447 and
https://crbug.com/763106 for more background.

The renderer cancels navigation by triggering the disconnection of the
NavigationClient interface used to start the navigation, eventually
calling `NavigationRequest::OnRendererAbortedNavigation()`.
Same-SiteInstanceGroup navigations used to use the same NavigationClient
for starting and committing navigation. This means even if a
CommitNavigation IPC is in-flight at the time of navigation
cancellation, the navigation can still get cancelled. Since the same
RenderFrame is reused, the CommitNavigation IPC also implicitly waits
for the JS task that triggers the navigation to finish, as the commit
can't be processed before then. However, with RenderDocument, the
RenderFrame and NavigationClient won't be reused, which means
navigation cancellations might only affect navigations that haven't
entered READY_TO_COMMIT stage.

This CL introduces RendererCancellationThrottle, which helps preserve
the previous behavior by waiting for the JS task to finish, through
deferring the navigations before it gets into the READY_TO_COMMIT
stage until the renderer that started the navigation calls the
`RendererCancellationWindowEnded` method on the per-navigation
NavigationRendererCancellationListener interface (also added by
this CL), signifying that the JS task that initiated the
navigation had ended and no more renderer-initiated navigation
cancellations can happen.

See also: https://docs.google.com/document/d/1VNmvEVuaiNH3ypt6YfrYPsJJp8okCTYjooekarOiWN8/edit#heading=h.71sdg5clbek8

Bug: 936696
Change-Id: I07393142c3fa03c1b3937147f730cc4e6dca4eff
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3561214
Reviewed-by: Alexander Timin <[email protected]>
Reviewed-by: Daniel Cheng <[email protected]>
Reviewed-by: David Bokan <[email protected]>
Commit-Queue: Rakina Zata Amni <[email protected]>
Reviewed-by: John Delaney <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1025993}
ForterLi pushed a commit to ForterLi/chromium_src_base that referenced this issue Aug 8, 2022
…ncellationThrottle

Renderer-initiated navigations can be cancelled from the JS task it was
initiated from, e.g. if the script runs window.stop() after initiating
the navigation. See also whatwg/html#3447 and
https://crbug.com/763106 for more background.

The renderer cancels navigation by triggering the disconnection of the
NavigationClient interface used to start the navigation, eventually
calling `NavigationRequest::OnRendererAbortedNavigation()`.
Same-SiteInstanceGroup navigations used to use the same NavigationClient
for starting and committing navigation. This means even if a
CommitNavigation IPC is in-flight at the time of navigation
cancellation, the navigation can still get cancelled. Since the same
RenderFrame is reused, the CommitNavigation IPC also implicitly waits
for the JS task that triggers the navigation to finish, as the commit
can't be processed before then. However, with RenderDocument, the
RenderFrame and NavigationClient won't be reused, which means
navigation cancellations might only affect navigations that haven't
entered READY_TO_COMMIT stage.

This CL introduces RendererCancellationThrottle, which helps preserve
the previous behavior by waiting for the JS task to finish, through
deferring the navigations before it gets into the READY_TO_COMMIT
stage until the renderer that started the navigation calls the
`RendererCancellationWindowEnded` method on the per-navigation
NavigationRendererCancellationListener interface (also added by
this CL), signifying that the JS task that initiated the
navigation had ended and no more renderer-initiated navigation
cancellations can happen.

See also: https://docs.google.com/document/d/1VNmvEVuaiNH3ypt6YfrYPsJJp8okCTYjooekarOiWN8/edit#heading=h.71sdg5clbek8

Bug: 936696
Change-Id: I07393142c3fa03c1b3937147f730cc4e6dca4eff
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3561214
Reviewed-by: Alexander Timin <[email protected]>
Reviewed-by: Daniel Cheng <[email protected]>
Reviewed-by: David Bokan <[email protected]>
Commit-Queue: Rakina Zata Amni <[email protected]>
Reviewed-by: John Delaney <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1025993}
NOKEYCHECK=True
GitOrigin-RevId: af55b5b6ebe03da36d40e71bd733617291c6c9c7
mjfroman pushed a commit to mjfroman/moz-libwebrtc-third-party that referenced this issue Oct 14, 2022
…ncellationThrottle

Renderer-initiated navigations can be cancelled from the JS task it was
initiated from, e.g. if the script runs window.stop() after initiating
the navigation. See also whatwg/html#3447 and
https://crbug.com/763106 for more background.

The renderer cancels navigation by triggering the disconnection of the
NavigationClient interface used to start the navigation, eventually
calling `NavigationRequest::OnRendererAbortedNavigation()`.
Same-SiteInstanceGroup navigations used to use the same NavigationClient
for starting and committing navigation. This means even if a
CommitNavigation IPC is in-flight at the time of navigation
cancellation, the navigation can still get cancelled. Since the same
RenderFrame is reused, the CommitNavigation IPC also implicitly waits
for the JS task that triggers the navigation to finish, as the commit
can't be processed before then. However, with RenderDocument, the
RenderFrame and NavigationClient won't be reused, which means
navigation cancellations might only affect navigations that haven't
entered READY_TO_COMMIT stage.

This CL introduces RendererCancellationThrottle, which helps preserve
the previous behavior by waiting for the JS task to finish, through
deferring the navigations before it gets into the READY_TO_COMMIT
stage until the renderer that started the navigation calls the
`RendererCancellationWindowEnded` method on the per-navigation
NavigationRendererCancellationListener interface (also added by
this CL), signifying that the JS task that initiated the
navigation had ended and no more renderer-initiated navigation
cancellations can happen.

See also: https://docs.google.com/document/d/1VNmvEVuaiNH3ypt6YfrYPsJJp8okCTYjooekarOiWN8/edit#heading=h.71sdg5clbek8

Bug: 936696
Change-Id: I07393142c3fa03c1b3937147f730cc4e6dca4eff
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3561214
Reviewed-by: Alexander Timin <[email protected]>
Reviewed-by: Daniel Cheng <[email protected]>
Reviewed-by: David Bokan <[email protected]>
Commit-Queue: Rakina Zata Amni <[email protected]>
Reviewed-by: John Delaney <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1025993}
NOKEYCHECK=True
GitOrigin-RevId: af55b5b6ebe03da36d40e71bd733617291c6c9c7
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

Successfully merging a pull request may close this issue.

5 participants