This documentation covers a set of important topics to understand related to navigation. For a timeline of how a given navigation proceeds, see Life of a Navigation.
Chromium defines two types of navigations based on whether the navigation results in a new document or not. A cross-document navigation is one that results in creating a new document to replace an existing document. This is the type of navigation that most users are familiar with. A same-document navigation does not create a new document, but rather keeps the same document and changes state associated with it. A same-document navigation does create a new session history entry, even though the same document remains active. This can be the result of one of the following cases:
https://foo.com/1.html#fragment
).history.pushState()
or history.replaceState()
APIs.Chromium also defines two types of navigations based on which process started the navigation: browser-initiated and renderer-initiated. This distinction is useful when making decisions about navigations, for example whether an ongoing navigation needs to be cancelled or not when a new navigation is starting. It is also used for some security decisions, such as whether to display the target URL of the navigation in the address bar or not. Browser-initiated navigations are more trustworthy, as they are usually in response to a user interaction with the UI of the browser. Renderer-initiated navigations originate in the renderer process, which may be under the control of an attacker. Note that some renderer-initiated navigations may be considered user-initiated, if they were performed with a user activation (e.g., links), while others are not user-initiated (e.g., script navigations).
Many features care about the URL or Origin of a given document, or about a pending navigation, or about what is showing in the address bar. These are all different concepts with different security implications, so be sure to use the correct value for your use case.
See Origin vs URL when deciding whether to check the Origin or URL. In many cases that care about the security context, Origin should be preferred.
The last committed URL or Origin represents the document that is currently in the frame, regardless of what is showing in the address bar. This is almost always what should be used for feature-related state, unless a feature is explicitly tied to the address bar (e.g., padlock icon). See RenderFrameHost::GetLastCommittedOrigin
(or URL) and NavigationController::GetLastCommittedEntry
.
The pending URL exists when a main frame navigation has started but has not yet committed. This URL is only sometimes shown to the user in the address bar; see the description of visible URLs below. Features should rarely need to care about the pending URL, unless they are probing for a navigation they expect to have started. See NavigationController::GetPendingEntry
.
The visible URL is what the address bar displays. This is carefully managed to show the main frame's last committed URL in most cases, and the pending URL in cases where it is safe and unlikely to be abused for a URL spoof attack (where an attacker is able to display content as if it came from a victim URL). In general, the visible URL is:
about:blank
URL is not displayed), but only until another document tries to access the initial empty document of the new tab. For example, an attacker window might open a new tab to a slow victim URL, then inject content into the initial about:blank
document as if the slow URL had committed. If that occurs, the visible URL reverts to about:blank
to avoid a URL spoof scenario. Once the initial navigation commits in the new tab, pending renderer-initiated navigation URLs are no longer displayed.Virtual URLs are a way for features to change how certain URLs are displayed to the user (whether visible or committed). They are generally implemented using BrowserURLHandlers. Examples include:
view-source:
prefix is not present in the actual committed URL.Navigations can redirect to other URLs in two different ways.
A server redirect happens when the browser receives a 300-level HTTP response code before the document commits, telling it to request a different URL, possibly cross-origin. The new request will usually be an HTTP GET request, unless the redirect is triggered by a 307 or 308 response code, which preserves the original request method and body. Server redirects are managed by a single NavigationRequest. No document is committed to session history, but the original URL remains in the redirect chain.
In contrast, a client redirect happens after a document has committed, when the HTML in the document instructs the browser to request a new document (e.g., via meta tags or JavaScript). Blink classifies the navigation as a client redirect based partly on how much time has passed. In this case, a session history item is created for the redirecting document, but it gets replaced when the actual destination document commits. A separate NavigationRequest is used for the second navigation.
Many navigations can be in progress simultaneously. In general, every frame is considered independent and may have its own navigations(s), with each tracked by a NavigationRequest. Within a frame, it is possible to have multiple concurrent navigations:
about:blank
, about:srcdoc
, MHTML).pushState
, fragment link click). In this case, the browser process creates and destroys a NavigationRequest in the same task.Note that the navigation code is not re-entrant. Callers must not start a new navigation while a call to NavigateWithoutEntry
or NavigateToExistingPendingEntry
is on the stack, to avoid a CHECK that guards against use-after-free for pending_entry_
.
We generally do not want an abusive page to prevent the user from navigating away, such as by endlessly starting new navigations that interrupt or cancel the user's attempts. Generally, a new navigation will cancel an existing one in a frame, but we make the following exception: a renderer-initiated navigation is ignored iff there is an ongoing browser-initiated navigation and the new navigation lacks a user activation. (This is implemented in Navigator::ShouldIgnoreIncomingRendererRequest
.)
NavigationThrottles also have an ability to cancel navigations when desired by a feature. Keep in mind that it is problematic to simulate a redirect by canceling a navigation and starting a new one, since this may lose relevant context from the original navigation (e.g., ReloadType, CSP state, Sec-Fetch-Metadata state, redirect chain, etc), and it will lead to unexpected observer events and metrics (e.g., extra navigation starts, inflated numbers of canceled navigations, etc). Feature authors that want to simulate redirects may want to consider using a URLLoaderRequestInterceptor instead.
There are several types of error pages that can be displayed when a navigation is not successful.
The server can return a custom error page, such as a 400 or 500 level HTTP response code page. These pages are rendered much like a successful navigation to the site (and go into an appropriate process for that site), but the error code is available and NavigationHandle::IsErrorPage()
is true.
If the navigation fails to get a response from the server (e.g., the DNS lookup fails), then Chromium will display an error page. For main frames, this error page will be in a special error page process, not affiliated with any site or containing any untrustworthy content from the web. In these failed cases, NetErrorHelperCore may try to reload the URL at a later time (e.g., if a network connection comes back online), to load the document in an appropriate process.
If instead the navigation is blocked (e.g., by an extension API or a NavigationThrottle), then Chromium will similarly display an error page in a special error page process. However, in blocked cases, Chromium will not attempt to reload the URL at a later time.
Interstitial pages are implemented as committed error pages. (Prior to issue 448486, they were implemented as overlays.) The original in-progress navigation is canceled when the interstitial is displayed, and Chromium repeats the navigation if the user chooses to proceed.
Note that some interstitials can be shown after a page has committed (e.g., when a subresource load triggers a Safe Browsing error). In this case, Chromium navigates away from the original page to the interstitial page, with the intent of replacing the original NavigationEntry. However, the original NavigationEntry is preserved in NavigationControllerImpl::entry_replaced_by_post_commit_error_
in case the user chooses to dismiss the interstitial and return to the original page.