Allow the navigate event to cancel same-document main-frame traversals
Currently, the navigate event is allowed to cancel push/replace/reload
navigations, but not traversals. There are two reasons for this:
chromium's architecture made it difficult to support cancelation
without getting out of sync with the authoritative version of the
joint session history in the browser process, and we were
concerned about the possibility of trapping the user if canceling
a traversal was too easy.
The case where we might get out of sync is when multiple frames
navigate as part of a traversal. We want to avoid the case where
some frames cancel the navigation in their frame, but others allow
it to proceed (since giving every frame what it requested would
cause some frames to be out of sync with the browser process). Therefore, only the main frame is allowed to cancel the navigation
via the navigate event. In order to ensure the main frame is able to
cancel the entire traversal, we send the main frame navigation (if any)
to the renderer first and wait for its commit to complete before
proceeding with any subframe navigations.
The main frame is only allowed to cancel a traversal when it is
traversing same-document (regardless of whether its subframes
traverse same-document or cross-document). We had originally
planned on allowing cross-document traversals to be cancelled, too
(https://chromium-review.googlesource.com/c/chromium/src/+/3868615),
but this proved to have unacceptable performance characteristics,
requiring roundtrips to the renderer whenever a navigate event
handler was present, even if the navigate event handler had no
intention of ever cancelling a traversal.
Therefore the sequence during a traversal is now:
1. Calculate which frames to navigate, and invoke
Navigator::Navigate() for each.
2. The main frame's NavigationRequest will proceed as normal.
3. If the main frame needs to do a same-document navigation, then:
3a. Any subframe navigations will be deferred until the main
frame NavigationRequest either commits or is canceled.
3b. If it cancels, abort the entire traversal.
3c. Resume all deferred subframes. These navigations will all fire
a navigate event just before committing, but none of those of
those events will be cancelable.
As for preventing trapping the user, we only allow canceling the
navigation in the main frame if the navigating is programmatic, or
if there is a consumable user activation. This ensures that, e.g.,
pressing the back button once might be canceled by the navigate
event, but the second back button press is guaranteed to go through.
Traversals via the navigation API or the legacy history API will
always be cancelable because they are programmatic. Canceling a
traversal consumes HistoryUserActivationState rather than
UserActivationState, in order to minimize the potential for
collisions with other UserActivationState consumers that are not
in the history/navigation space.
Bug: 1371580
Change-Id: I0c8c39bec8e21f3ca86389a4343881ebe2bde43e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4092862
Reviewed-by: Domenic Denicola <[email protected]>
Commit-Queue: Nate Chapin <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1106877}
diff --git a/content/browser/renderer_host/navigation_controller_impl.cc b/content/browser/renderer_host/navigation_controller_impl.cc
index 1ee80fd..8c0177e 100644
--- a/content/browser/renderer_host/navigation_controller_impl.cc
+++ b/content/browser/renderer_host/navigation_controller_impl.cc
@@ -3253,6 +3253,32 @@
// function.
std::unique_ptr<PendingEntryRef> pending_entry_ref = ReferencePendingEntry();
+ // If there is a main-frame same-document history navigation, we may defer
+ // the subframe history navigations in order to give JS in the main frame the
+ // opportunity to cancel the entire traverse via the navigate event. In that
+ // case, we need to stash the main frame request's navigation token on the
+ // subframes, so they can look up the main frame request and defer themselves
+ // until it completes.
+ if (!same_document_loads.empty() &&
+ same_document_loads.at(0)->frame_tree_node()->IsMainFrame()) {
+ NavigationRequest* main_frame_request = same_document_loads.at(0).get();
+ // The token will only be returned in cases where deferring the navigation
+ // is necessary.
+ if (auto main_frame_same_document_token =
+ main_frame_request->GetNavigationTokenForDeferringSubframes()) {
+ for (auto& item : same_document_loads) {
+ if (item.get() != main_frame_request) {
+ item->set_main_frame_same_document_history_token(
+ main_frame_same_document_token);
+ }
+ }
+ for (auto& item : different_document_loads) {
+ item->set_main_frame_same_document_history_token(
+ main_frame_same_document_token);
+ }
+ }
+ }
+
// Send all the same document frame loads before the different document loads.
for (auto& item : same_document_loads) {
FrameTreeNode* frame = item->frame_tree_node();