blob: f3b3b1053c0574fde466a860f30c18d1fa58dfce [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/preloading/prefetch/prefetch_url_loader_interceptor.h"
#include "base/debug/dump_without_crashing.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_macros.h"
#include "content/browser/loader/navigation_loader_interceptor.h"
#include "content/browser/loader/url_loader_factory_utils.h"
#include "content/browser/preloading/prefetch/prefetch_features.h"
#include "content/browser/preloading/prefetch/prefetch_match_resolver.h"
#include "content/browser/preloading/prefetch/prefetch_params.h"
#include "content/browser/preloading/prefetch/prefetch_service.h"
#include "content/browser/preloading/prefetch/prefetch_url_loader_helper.h"
#include "content/browser/renderer_host/frame_tree.h"
#include "content/browser/renderer_host/frame_tree_node.h"
#include "content/browser/renderer_host/navigation_request.h"
#include "content/browser/service_worker/service_worker_main_resource_handle.h"
#include "content/public/browser/web_contents.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/single_request_url_loader_factory.h"
namespace content {
namespace {
BrowserContext* BrowserContextFromFrameTreeNodeId(
FrameTreeNodeId frame_tree_node_id) {
WebContents* web_content =
WebContents::FromFrameTreeNodeId(frame_tree_node_id);
if (!web_content)
return nullptr;
return web_content->GetBrowserContext();
}
void RecordWasFullRedirectChainServedHistogram(
bool was_full_redirect_chain_served) {
UMA_HISTOGRAM_BOOLEAN("PrefetchProxy.AfterClick.WasFullRedirectChainServed",
was_full_redirect_chain_served);
}
PrefetchCompleteCallbackForTesting& GetPrefetchCompleteCallbackForTesting() {
static base::NoDestructor<PrefetchCompleteCallbackForTesting>
get_prefetch_complete_callback_for_testing;
return *get_prefetch_complete_callback_for_testing;
}
} // namespace
// static
void PrefetchURLLoaderInterceptor::SetPrefetchCompleteCallbackForTesting(
PrefetchCompleteCallbackForTesting callback) {
GetPrefetchCompleteCallbackForTesting() = std::move(callback); // IN-TEST
}
PrefetchURLLoaderInterceptor::PrefetchURLLoaderInterceptor(
PrefetchServiceWorkerState expected_service_worker_state,
base::WeakPtr<ServiceWorkerMainResourceHandle>
service_worker_handle_for_navigation,
FrameTreeNodeId frame_tree_node_id,
std::optional<blink::DocumentToken> initiator_document_token,
base::WeakPtr<PrefetchServingPageMetricsContainer>
serving_page_metrics_container)
: expected_service_worker_state_(expected_service_worker_state),
service_worker_handle_for_navigation_(
std::move(service_worker_handle_for_navigation)),
frame_tree_node_id_(frame_tree_node_id),
initiator_document_token_(std::move(initiator_document_token)),
serving_page_metrics_container_(
std::move(serving_page_metrics_container)) {
if (!features::IsPrefetchServiceWorkerEnabled(
BrowserContextFromFrameTreeNodeId(frame_tree_node_id_))) {
CHECK_EQ(expected_service_worker_state_,
PrefetchServiceWorkerState::kDisallowed);
}
}
PrefetchURLLoaderInterceptor::~PrefetchURLLoaderInterceptor() = default;
void PrefetchURLLoaderInterceptor::MaybeCreateLoader(
const network::ResourceRequest& tentative_resource_request,
BrowserContext* browser_context,
NavigationLoaderInterceptor::LoaderCallback callback,
NavigationLoaderInterceptor::FallbackCallback fallback_callback) {
TRACE_EVENT0("loading", "PrefetchURLLoaderInterceptor::MaybeCreateLoader");
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK(!loader_callback_);
loader_callback_ = std::move(callback);
// Prefetches are only ever used to fulfill `GET` requests. spec: if
// documentResource is null,
// https://wicg.github.io/nav-speculation/prefetch.html#create-navigation-params-from-a-prefetch-record
// etc. are not called.
if (tentative_resource_request.method !=
net::HttpRequestHeaders::kGetMethod) {
redirect_serving_handle_ = PrefetchServingHandle();
std::move(loader_callback_).Run(std::nullopt);
return;
}
// SW-controlled prefetches shouldn't serve navigation with
// `skip_service_worker` == `true`.
// TODO(https://crbug.com/438478667): The current serving-time
// `skip_service_worker` check here assumes prefetching-time
// `skip_service_worker` is always false (see the
// `CHECK(!skip_service_worker)` in
// `PrefetchContainer::MakeResourceRequest()`). We should revisit the check
// when we support prefetch-time `skip_service_worker`. Probably a prefetch
// whose request's `skip_service_worker` == `true` shouldn't serve navigation
// whose request's `skip_service_worker` == `false`.
if (tentative_resource_request.skip_service_worker &&
expected_service_worker_state_ ==
PrefetchServiceWorkerState::kControlled) {
redirect_serving_handle_ = PrefetchServingHandle();
std::move(loader_callback_).Run(std::nullopt);
return;
}
if (redirect_serving_handle_ &&
redirect_serving_handle_.DoesCurrentURLToServeMatch(
tentative_resource_request.url)) {
if (redirect_serving_handle_.HaveDefaultContextCookiesChanged()) {
// Cookies have changed for the next redirect hop's URL since the fetch,
// so we cannot use this prefetch anymore.
PrefetchContainer* prefetch_container =
redirect_serving_handle_.GetPrefetchContainer();
CHECK(prefetch_container);
// Use `std::nullopt` as we need to record the crash key to identify
// which case in `PrefetchMatchResolver` is the cause.
prefetch_container->OnDetectedCookiesChange(
/*is_unblock_for_cookies_changed_triggered_by_this_prefetch_container*/
std::nullopt);
} else {
OnGotPrefetchToServe(
frame_tree_node_id_, tentative_resource_request.url,
base::BindOnce(&PrefetchURLLoaderInterceptor::OnGetPrefetchComplete,
weak_factory_.GetWeakPtr(),
tentative_resource_request),
std::move(redirect_serving_handle_));
return;
}
}
if (redirect_serving_handle_) {
RecordWasFullRedirectChainServedHistogram(false);
redirect_serving_handle_ = PrefetchServingHandle();
}
FrameTreeNode* frame_tree_node =
FrameTreeNode::GloballyFindByID(frame_tree_node_id_);
if (!frame_tree_node->IsOutermostMainFrame()) {
// The prefetch code does not currently deal with prefetching within a frame
// (i.e., where the partition which should be assigned to the request is not
// the same as the partition belonging to its site at the top level).
//
// This could be made smarter in the future (to do those prefetches within
// the right partition, or at minimum to use it from that partition if they
// happen to be the same, i.e., the URL remains within the same site as the
// top-level document).
std::move(loader_callback_).Run(std::nullopt);
return;
}
GetPrefetch(
tentative_resource_request,
base::BindOnce(&PrefetchURLLoaderInterceptor::OnGetPrefetchComplete,
weak_factory_.GetWeakPtr(), tentative_resource_request));
}
void PrefetchURLLoaderInterceptor::GetPrefetch(
const network::ResourceRequest& tentative_resource_request,
base::OnceCallback<void(PrefetchServingHandle)> get_prefetch_callback)
const {
TRACE_EVENT0("loading", "PrefetchURLLoaderInterceptor::GetPrefetch");
PrefetchService* prefetch_service =
PrefetchService::GetFromFrameTreeNodeId(frame_tree_node_id_);
if (!prefetch_service) {
std::move(get_prefetch_callback).Run({});
return;
}
if (!initiator_document_token_.has_value()) {
// TODO(crbug.com/40288091): Currently PrefetchServingPageMetricsContainer
// is created only when the navigation is renderer-initiated and its
// initiator document has PrefetchDocumentManager.
CHECK(!serving_page_metrics_container_);
}
const GURL tentative_resource_request_url = tentative_resource_request.url;
auto callback = base::BindOnce(&OnGotPrefetchToServe, frame_tree_node_id_,
tentative_resource_request_url,
std::move(get_prefetch_callback));
auto key =
PrefetchKey(initiator_document_token_, tentative_resource_request_url);
const bool is_nav_prerender = [&]() -> bool {
auto* frame_tree_node =
FrameTreeNode::GloballyFindByID(frame_tree_node_id_);
if (!frame_tree_node) {
return false;
}
return frame_tree_node->frame_tree().is_prerendering();
}();
PrefetchMatchResolver::FindPrefetch(
std::move(key), expected_service_worker_state_, is_nav_prerender,
*prefetch_service, serving_page_metrics_container_, std::move(callback));
}
void PrefetchURLLoaderInterceptor::OnGetPrefetchComplete(
const network::ResourceRequest& tentative_resource_request,
PrefetchServingHandle serving_handle) {
TRACE_EVENT0("loading",
"PrefetchURLLoaderInterceptor::OnGetPrefetchComplete");
PrefetchRequestHandler request_handler;
base::WeakPtr<ServiceWorkerClient> client_for_prefetch;
if (serving_handle) {
std::tie(request_handler, client_for_prefetch) =
serving_handle.CreateRequestHandler();
}
if (expected_service_worker_state_ ==
PrefetchServiceWorkerState::kControlled &&
request_handler) {
// ServiceWorker-controlled prefetch should be always non-redirecting.
CHECK(serving_handle.IsEnd());
if (!service_worker_handle_for_navigation_ || !client_for_prefetch) {
// Do not intercept the request.
request_handler = PrefetchRequestHandler();
} else if (!service_worker_handle_for_navigation_->InitializeForRequest(
tentative_resource_request, client_for_prefetch.get())) {
// Make tests fail and report in production builds when
// `InitializeForRequest()` should fail, i.e. when top frame origin or
// storage key used for `client_for_prefetch` is wrong/mismatching.
// TODO(https://crbug.com/413207408): Monitor the reports and fix
// `ServiceWorkerClient::CalculateStorageKeyForUpdateUrls()` if there
// are actual mismatches.
DCHECK(false);
base::debug::DumpWithoutCrashing();
// We anyway gracefully fallback to non-prefetch path.
// Do not intercept the request.
request_handler = PrefetchRequestHandler();
}
}
if (!request_handler) {
// Do not intercept the request.
redirect_serving_handle_ = PrefetchServingHandle();
if (GetPrefetchCompleteCallbackForTesting()) {
GetPrefetchCompleteCallbackForTesting().Run(nullptr); // IN-TEST
}
std::move(loader_callback_).Run(std::nullopt);
return;
}
scoped_refptr<network::SingleRequestURLLoaderFactory>
single_request_url_loader_factory =
base::MakeRefCounted<network::SingleRequestURLLoaderFactory>(
std::move(request_handler));
PrefetchContainer* prefetch_container = serving_handle.GetPrefetchContainer();
// If |prefetch_container| is done serving the prefetch, clear out
// |redirect_serving_handle_|, but otherwise cache it in
// |redirect_serving_handle_|.
if (serving_handle.IsEnd()) {
if (redirect_serving_handle_) {
RecordWasFullRedirectChainServedHistogram(true);
}
redirect_serving_handle_ = PrefetchServingHandle();
} else {
CHECK_EQ(expected_service_worker_state_,
PrefetchServiceWorkerState::kDisallowed);
redirect_serving_handle_ = std::move(serving_handle);
}
FrameTreeNode* frame_tree_node =
FrameTreeNode::GloballyFindByID(frame_tree_node_id_);
RenderFrameHost* render_frame_host = frame_tree_node->current_frame_host();
NavigationRequest* navigation_request = frame_tree_node->navigation_request();
bool bypass_redirect_checks = false;
if (GetPrefetchCompleteCallbackForTesting()) {
GetPrefetchCompleteCallbackForTesting().Run(prefetch_container); // IN-TEST
}
// TODO (https://crbug.com/1369766): Investigate if
// `HeaderClientOption::kAllowed` should be used for `TerminalParams`, and
// then how to utilize it.
std::move(loader_callback_)
.Run(NavigationLoaderInterceptor::Result(
url_loader_factory::Create(
ContentBrowserClient::URLLoaderFactoryType::kNavigation,
url_loader_factory::TerminalParams::ForNonNetwork(
std::move(single_request_url_loader_factory),
network::mojom::kBrowserProcessId),
url_loader_factory::ContentClientParams(
BrowserContextFromFrameTreeNodeId(frame_tree_node_id_),
render_frame_host,
render_frame_host->GetProcess()->GetDeprecatedID(),
url::Origin(), net::IsolationInfo(),
ukm::SourceIdObj::FromInt64(
navigation_request->GetNextPageUkmSourceId()),
&bypass_redirect_checks,
navigation_request->GetNavigationId())),
/*subresource_loader_params=*/{}));
}
} // namespace content