diff --git a/content/browser/BUILD.gn b/content/browser/BUILD.gn index e517f517b4da6..b9067fa4704fa 100644 --- a/content/browser/BUILD.gn +++ b/content/browser/BUILD.gn @@ -1185,6 +1185,10 @@ source_set("browser") { "loader/download_utils_impl.h", "loader/file_url_loader_factory.cc", "loader/file_url_loader_factory.h", + "loader/keep_alive_url_loader.cc", + "loader/keep_alive_url_loader.h", + "loader/keep_alive_url_loader_service.cc", + "loader/keep_alive_url_loader_service.h", "loader/merkle_integrity_source_stream.cc", "loader/merkle_integrity_source_stream.h", "loader/navigation_early_hints_manager.cc", diff --git a/content/browser/loader/keep_alive_url_loader.cc b/content/browser/loader/keep_alive_url_loader.cc new file mode 100644 index 0000000000000..ca316e2f35962 --- /dev/null +++ b/content/browser/loader/keep_alive_url_loader.cc @@ -0,0 +1,251 @@ +// Copyright 2023 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/loader/keep_alive_url_loader.h" + +#include "base/functional/bind.h" +#include "base/trace_event/trace_event.h" +#include "content/public/browser/browser_thread.h" +#include "net/base/load_flags.h" +#include "net/http/http_request_headers.h" +#include "services/network/public/cpp/shared_url_loader_factory.h" +#include "services/network/public/mojom/early_hints.mojom.h" +#include "services/network/public/mojom/fetch_api.mojom-shared.h" +#include "third_party/blink/public/common/features.h" + +namespace content { + +KeepAliveURLLoader::KeepAliveURLLoader( + int32_t request_id, + uint32_t options, + const network::ResourceRequest& resource_request, + mojo::PendingRemote forwarding_client, + const net::MutableNetworkTrafficAnnotationTag& traffic_annotation, + scoped_refptr network_loader_factory, + base::PassKey) + : request_id_(request_id), + forwarding_client_(std::move(forwarding_client)) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DCHECK(network_loader_factory); + DCHECK(!resource_request.trusted_params); + TRACE_EVENT2("loading", "KeepAliveURLLoader::KeepAliveURLLoader", + "request_id", request_id_, "url", resource_request.url); + TRACE_EVENT_NESTABLE_ASYNC_BEGIN1("loading", "KeepAliveURLLoader", + request_id_, "url", resource_request.url); + + // Asks the network service to create a URL loader with passed in params. + network_loader_factory->CreateLoaderAndStart( + loader_.BindNewPipeAndPassReceiver(), request_id, options, + resource_request, loader_receiver_.BindNewPipeAndPassRemote(), + traffic_annotation); + loader_receiver_.set_disconnect_handler(base::BindOnce( + &KeepAliveURLLoader::OnNetworkConnectionError, base::Unretained(this))); + forwarding_client_.set_disconnect_handler(base::BindOnce( + &KeepAliveURLLoader::OnRendererConnectionError, base::Unretained(this))); +} + +KeepAliveURLLoader::~KeepAliveURLLoader() { + TRACE_EVENT1("loading", "KeepAliveURLLoader::~KeepAliveURLLoader", + "request_id", request_id_); + TRACE_EVENT_NESTABLE_ASYNC_END0("loading", "KeepAliveURLLoader", request_id_); +} + +void KeepAliveURLLoader::set_on_delete_callback( + OnDeleteCallback on_delete_callback) { + on_delete_callback_ = std::move(on_delete_callback); +} + +void KeepAliveURLLoader::FollowRedirect( + const std::vector& removed_headers, + const net::HttpRequestHeaders& modified_headers, + const net::HttpRequestHeaders& modified_cors_exempt_headers, + const absl::optional& new_url) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DCHECK(loader_); + TRACE_EVENT2("loading", "KeepAliveURLLoader::FollowRedirect", "request_id", + request_id_, "url", new_url); + + // Forwards the action to `loader_` in the network service. + loader_->FollowRedirect(removed_headers, modified_headers, + modified_cors_exempt_headers, new_url); +} + +void KeepAliveURLLoader::SetPriority(net::RequestPriority priority, + int intra_priority_value) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DCHECK(loader_); + TRACE_EVENT1("loading", "KeepAliveURLLoader::SetPriority", "request_id", + request_id_); + + // Forwards the action to `loader_` in the network service. + loader_->SetPriority(priority, intra_priority_value); +} + +void KeepAliveURLLoader::PauseReadingBodyFromNet() { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DCHECK(loader_); + TRACE_EVENT1("loading", "KeepAliveURLLoader::FollowRedirect", "request_id", + request_id_); + + // Forwards the action to `loader_` in the network service. + loader_->PauseReadingBodyFromNet(); +} + +void KeepAliveURLLoader::ResumeReadingBodyFromNet() { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DCHECK(loader_); + TRACE_EVENT1("loading", "KeepAliveURLLoader::ResumeReadingBodyFromNet", + "request_id", request_id_); + + // Forwards the action to `loader_` in the network service. + loader_->ResumeReadingBodyFromNet(); +} + +void KeepAliveURLLoader::OnReceiveEarlyHints( + network::mojom::EarlyHintsPtr early_hints) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + TRACE_EVENT1("loading", "KeepAliveURLLoader::OnReceiveEarlyHints", + "request_id", request_id_); + + if (forwarding_client_.is_bound() && forwarding_client_.is_connected()) { + // The renderer is alive, forwards the action. + forwarding_client_->OnReceiveEarlyHints(std::move(early_hints)); + return; + } + + // TODO(crbug.com/1356128): Handle in browser process. +} + +void KeepAliveURLLoader::OnReceiveResponse( + network::mojom::URLResponseHeadPtr response, + mojo::ScopedDataPipeConsumerHandle body, + absl::optional cached_metadata) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + TRACE_EVENT1("loading", "KeepAliveURLLoader::OnReceiveResponse", "request_id", + request_id_); + + has_received_response_ = true; + // TODO(crbug.com/1424731): The renderer might exit before `OnReceiveRedirect` + // or `OnReceiveResponse` is called, or during their execution. In such case, + // `forwarding_client_` can't finish response handling. Figure out a way to + // negotiate shutdown timing via RenderFrameHostImpl::OnUnloadAck() and + // invalidate `forwarding_client_`. + if (forwarding_client_.is_bound() && forwarding_client_.is_connected()) { + // The renderer is alive, forwards the action. + // The receiver may fail to finish reading `response`, so response caching + // is not guaranteed. + forwarding_client_->OnReceiveResponse(std::move(response), std::move(body), + std::move(cached_metadata)); + // TODO(crbug.com/1422645): Ensure that attributionsrc response handling is + // migrated to browser process. + return; + } + + // No need to wait for `OnComplete()`. + // This loader should be deleted immediately to avoid hanged requests taking + // up resources. + std::move(on_delete_callback_).Run(); +} + +void KeepAliveURLLoader::OnReceiveRedirect( + const net::RedirectInfo& redirect_info, + network::mojom::URLResponseHeadPtr head) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + TRACE_EVENT1("loading", "KeepAliveURLLoader::OnReceiveRedirect", "request_id", + request_id_); + + // TODO(crbug.com/1424731): The renderer might exit before `OnReceiveRedirect` + // or `OnReceiveResponse` is called, or during their execution. In such case, + // `forwarding_client_` can't finish response handling. Figure out a way to + // negotiate shutdown timing via RenderFrameHostImpl::OnUnloadAck() and + // invalidate `forwarding_client_`. + if (forwarding_client_.is_bound() && forwarding_client_.is_connected()) { + // The renderer is alive, forwards the action. + // Redirects must be handled by the renderer so that it know what URL the + // response come from when parsing responses. + forwarding_client_->OnReceiveRedirect(redirect_info, std::move(head)); + return; + } + + // TODO(crbug.com/1356128): Replicates all existing behaviors from all of + // `blink::URLLoaderThrottles`. + // TODO(crbug.com/1356128): Run security checks, including CSP, mixed-content, + // and SafeBrowsing. + // TODO(crbug.com/1356128): Ask the network service to follow the redirect. +} + +void KeepAliveURLLoader::OnUploadProgress(int64_t current_position, + int64_t total_size, + base::OnceCallback callback) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + TRACE_EVENT1("loading", "KeepAliveURLLoader::OnUploadProgress", "request_id", + request_id_); + + if (forwarding_client_.is_bound() && forwarding_client_.is_connected()) { + // The renderer is alive, forwards the action. + forwarding_client_->OnUploadProgress(current_position, total_size, + std::move(callback)); + return; + } + + // TODO(crbug.com/1356128): Handle in the browser process. +} + +void KeepAliveURLLoader::OnTransferSizeUpdated(int32_t transfer_size_diff) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + TRACE_EVENT1("loading", "KeepAliveURLLoader::OnTransferSizeUpdated", + "request_id", request_id_); + + if (forwarding_client_.is_bound() && forwarding_client_.is_connected()) { + // The renderer is alive, forwards the action. + forwarding_client_->OnTransferSizeUpdated(transfer_size_diff); + return; + } + + // TODO(crbug.com/1356128): Handle in the browser process. +} + +void KeepAliveURLLoader::OnComplete( + const network::URLLoaderCompletionStatus& completion_status) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + TRACE_EVENT1("loading", "KeepAliveURLLoader::OnComplete", "request_id", + request_id_); + + if (forwarding_client_.is_bound() && forwarding_client_.is_connected()) { + // The renderer is alive, forwards the action. + forwarding_client_->OnComplete(completion_status); + return; + } + + // TODO(crbug.com/1356128): Handle in the browser process. +} + +void KeepAliveURLLoader::OnNetworkConnectionError() { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + TRACE_EVENT1("loading", "KeepAliveURLLoader::OnNetworkConnectionError", + "request_id", request_id_); + + // The network loader has an error; we should let the client know it's + // closed by dropping this, which will in turn make this loader destroyed. + forwarding_client_.reset(); +} + +void KeepAliveURLLoader::OnRendererConnectionError() { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + TRACE_EVENT1("loading", "KeepAliveURLLoader::OnRendererConnectionError", + "request_id", request_id_); + + if (has_received_response_) { + // No need to wait for `OnComplete()`. + std::move(on_delete_callback_).Run(); + return; + } + // Otherwise, let this loader continue to handle responses. + forwarding_client_.reset(); + // TODO(crbug.com/1424731): When we reach here while the renderer is + // processing a redirect, we should take over the redirect handling in the + // browser process. See TODOs in `OnReceiveRedirect()`. +} + +} // namespace content diff --git a/content/browser/loader/keep_alive_url_loader.h b/content/browser/loader/keep_alive_url_loader.h new file mode 100644 index 0000000000000..03f39a7485d71 --- /dev/null +++ b/content/browser/loader/keep_alive_url_loader.h @@ -0,0 +1,149 @@ +// Copyright 2023 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CONTENT_BROWSER_LOADER_KEEP_ALIVE_URL_LOADER_H_ +#define CONTENT_BROWSER_LOADER_KEEP_ALIVE_URL_LOADER_H_ + +#include + +#include "base/functional/callback.h" +#include "base/types/pass_key.h" +#include "content/common/content_export.h" +#include "mojo/public/cpp/bindings/pending_remote.h" +#include "mojo/public/cpp/bindings/receiver.h" +#include "mojo/public/cpp/bindings/receiver_set.h" +#include "mojo/public/cpp/bindings/remote.h" +#include "net/traffic_annotation/network_traffic_annotation.h" +#include "services/network/public/cpp/resource_request.h" +#include "services/network/public/mojom/url_loader.mojom.h" +#include "services/network/public/mojom/url_response_head.mojom.h" +#include "url/gurl.h" + +namespace network { +class SharedURLLoaderFactory; +} + +namespace content { + +class KeepAliveURLLoaderService; + +// A URLLoader for loading a fetch keepalive request via the browser process, +// including both `fetch(..., {keepalive: true})` and `navigator.sendBeacon()` +// requests. +// +// To load a keepalive request initiated by a renderer, this loader performs the +// following logic: +// 1. Forwards all request loading actions received from a remote of +// `mojom::URLLoader` in a renderer to a receiver of `mojom::URLLoader` in +// the network service connected by `loader_`. +// 2. Receives request loading results from the network service, i.e. the remote +// of `loader_receiver_`. The URLLoaderClient overrides will be triggered to +// process results: +// A. For redirect, perform security checks and ask the network service to +// follow all subsequent redirects. +// B. For non-redirect, +// a. If the renderer is still alive, i.e. `forwarding_client_` is +// connected, ask it to process the results instead. +// b. If the renderer is dead, drop the results. +// +// Instances of this class must only be constructed and run within the browser +// process, such that the lifetime of the corresponding requests can be +// maintained by the browser instead of by a renderer. +// +// Design Doc: +// https://docs.google.com/document/d/1ZzxMMBvpqn8VZBZKnb7Go8TWjnrGcXuLS_USwVVRUvY/edit# +class CONTENT_EXPORT KeepAliveURLLoader + : public network::mojom::URLLoader, + public network::mojom::URLLoaderClient { + public: + // Deletes this loader immediately. + using OnDeleteCallback = base::OnceCallback; + + // Must only be constructed by a `KeepAliveURLLoaderService`. + // `resource_request` must be a keepalive request from a renderer. + // `forwarding_client` should handle request loading results from the network + // service if it is still connected. + // `delete_callback` is a callback to delete this object. + KeepAliveURLLoader( + int32_t request_id, + uint32_t options, + const network::ResourceRequest& resource_request, + mojo::PendingRemote forwarding_client, + const net::MutableNetworkTrafficAnnotationTag& traffic_annotation, + scoped_refptr network_loader_factory, + base::PassKey); + ~KeepAliveURLLoader() override; + + // Not copyable. + KeepAliveURLLoader(const KeepAliveURLLoader&) = delete; + KeepAliveURLLoader& operator=(const KeepAliveURLLoader&) = delete; + + // Sets the callback to be invoked on errors which require closing the pipe. + // Callback will also immediately delete `this`. + // Not an argument to constructor because the Mojo ReceiverId needs to be + // bound to the callback, but can only get that after creating the worklet. + // Must be called immediately after creating a KeepAliveLoader. + void set_on_delete_callback(OnDeleteCallback on_delete_callback); + + private: + // Receives actions from renderer. + // `network::mojom::URLLoader` overrides: + void FollowRedirect( + const std::vector& removed_headers, + const net::HttpRequestHeaders& modified_headers, + const net::HttpRequestHeaders& modified_cors_exempt_headers, + const absl::optional& new_url) override; + void SetPriority(net::RequestPriority priority, + int intra_priority_value) override; + void PauseReadingBodyFromNet() override; + void ResumeReadingBodyFromNet() override; + + // Receives actions from network service. + // `network::mojom::URLLoaderClient` overrides: + void OnReceiveEarlyHints(network::mojom::EarlyHintsPtr early_hints) override; + void OnReceiveResponse( + network::mojom::URLResponseHeadPtr head, + mojo::ScopedDataPipeConsumerHandle body, + absl::optional cached_metadata) override; + void OnReceiveRedirect(const net::RedirectInfo& redirect_info, + network::mojom::URLResponseHeadPtr head) override; + void OnUploadProgress(int64_t current_position, + int64_t total_size, + base::OnceCallback callback) override; + void OnTransferSizeUpdated(int32_t transfer_size_diff) override; + void OnComplete( + const network::URLLoaderCompletionStatus& completion_status) override; + + void OnNetworkConnectionError(); + void OnRendererConnectionError(); + + // The ID to identify the request being loaded by this loader. + int32_t request_id_; + + // Connection with the network service: + // Connects to the receiver network::URLLoader implemented in the network + // service that performs actual request loading. + mojo::Remote loader_; + // Connection with the network service: + // Receives the result of the request loaded by `loader_` from the network + // service. + mojo::Receiver loader_receiver_{this}; + + // Connection with a renderer: + // Connects to the receiver URLLoaderClient implemented in the renderer. + // It is the client to forward the URLLoader response from the network + // service to. + // It may be disconnected if the renderer is dead. + mojo::Remote forwarding_client_; + + // A callback to delete this loader object and clean up resource. + OnDeleteCallback on_delete_callback_; + + // Whether `OnReceiveResponse()` has been called. + bool has_received_response_ = false; +}; + +} // namespace content + +#endif // CONTENT_BROWSER_LOADER_KEEP_ALIVE_URL_LOADER_H_ diff --git a/content/browser/loader/keep_alive_url_loader_service.cc b/content/browser/loader/keep_alive_url_loader_service.cc new file mode 100644 index 0000000000000..ad722b67ceedc --- /dev/null +++ b/content/browser/loader/keep_alive_url_loader_service.cc @@ -0,0 +1,202 @@ +// Copyright 2023 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/loader/keep_alive_url_loader_service.h" + +#include "base/functional/bind.h" +#include "base/memory/raw_ptr.h" +#include "base/memory/ref_counted.h" +#include "base/trace_event/trace_event.h" +#include "content/browser/loader/keep_alive_url_loader.h" +#include "content/browser/url_loader_factory_getter.h" +#include "content/public/browser/browser_thread.h" +#include "mojo/public/cpp/bindings/message.h" +#include "mojo/public/cpp/bindings/remote.h" +#include "mojo/public/cpp/bindings/self_owned_receiver.h" +#include "services/network/public/cpp/shared_url_loader_factory.h" +#include "third_party/blink/public/common/features.h" + +namespace content { +namespace { + +// A Context for the receiver of a KeepAliveURLLoaderFactory connection between +// a renderer and the browser. +// +// See `mojo::ReceiverSetBase` for more details. +struct BindContext { + explicit BindContext(scoped_refptr factory) + : factory(factory) {} + + explicit BindContext(const std::unique_ptr& other) + : factory(other->factory) {} + + ~BindContext() = default; + + // A refptr to the factory to use for the requests initiated from this + // context. + scoped_refptr factory; + // This must be the last member. + base::WeakPtrFactory weak_ptr_factory{this}; +}; +} // namespace + +// A URLLoaderFactory to handle fetch keepalive requests. +// +// This factory can handle requests from multiple remotes of URLLoaderFactory. +// Users should call `BindFactory()` first to register a pending receiver with +// this factory. +// +// On requested by a remote, i.e. calling +// `network::mojom::URLLoaderFactory::CreateLoaderAndStart()`, this factory will +// create a KeepAliveURLLoader to load a keepalive request. The loader is held +// by the `KeepAliveURLLoaderService` owning this factory. +// +// This factory must be run in the browser process. +// +// See the "Implementation Details" section of the design doc +// https://docs.google.com/document/d/1ZzxMMBvpqn8VZBZKnb7Go8TWjnrGcXuLS_USwVVRUvY/edit# +class KeepAliveURLLoaderService::KeepAliveURLLoaderFactory final + : public network::mojom::URLLoaderFactory { + public: + explicit KeepAliveURLLoaderFactory(KeepAliveURLLoaderService* service) + : service_(service) { + DCHECK(service_); + } + ~KeepAliveURLLoaderFactory() override = default; + + // Not copyable. + KeepAliveURLLoaderFactory(const KeepAliveURLLoaderFactory&) = delete; + KeepAliveURLLoaderFactory& operator=(const KeepAliveURLLoaderFactory&) = + delete; + + // Creates a `BindContext` to hold a refptr to + // network::SharedURLLoaderFactory, which is constructed with + // `pending_factory`, and then bound with `receiver`. + void BindFactory( + mojo::PendingReceiver receiver, + std::unique_ptr pending_factory); + + // `network::mojom::URLLoaderFactory` overrides: + void CreateLoaderAndStart( + mojo::PendingReceiver receiver, + int32_t request_id, + uint32_t options, + const network::ResourceRequest& resource_request_in, + mojo::PendingRemote client, + const net::MutableNetworkTrafficAnnotationTag& traffic_annotation) + override; + void Clone(mojo::PendingReceiver receiver) + override; + + private: + // Guaranteed to exist, as `service_` owns this object. + raw_ptr service_; + + // Receives `network::mojom::URLLoaderFactory` requests from renderers. + mojo::ReceiverSet> + loader_factory_receivers_; +}; + +void KeepAliveURLLoaderService::KeepAliveURLLoaderFactory::BindFactory( + mojo::PendingReceiver receiver, + std::unique_ptr pending_factory) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + TRACE_EVENT0("loading", "KeepAliveURLLoaderFactory::BindFactory"); + + auto factory_bundle = + network::SharedURLLoaderFactory::Create(std::move(pending_factory)); + loader_factory_receivers_.Add(this, std::move(receiver), + std::make_unique(factory_bundle)); +} + +void KeepAliveURLLoaderService::KeepAliveURLLoaderFactory::CreateLoaderAndStart( + mojo::PendingReceiver receiver, + int32_t request_id, + uint32_t options, + const network::ResourceRequest& resource_request, + mojo::PendingRemote client, + const net::MutableNetworkTrafficAnnotationTag& traffic_annotation) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + TRACE_EVENT1("loading", "KeepAliveURLLoaderFactory::CreateLoaderAndStart", + "request_id", request_id); + + if (!resource_request.keepalive) { + loader_factory_receivers_.ReportBadMessage( + "Unexpected `resource_request` in " + "KeepAliveURLLoaderService::CreateLoaderAndStart(): " + "resource_request.keepalive must be true"); + return; + } + if (resource_request.trusted_params) { + // Must use untrusted URLLoaderFactory. If not, the requesting renderer + // should be aborted. + loader_factory_receivers_.ReportBadMessage( + "Unexpected `resource_request` in " + "KeepAliveURLLoaderService::CreateLoaderAndStart(): " + "resource_request.trusted_params must not be set"); + return; + } + + // Creates a new KeepAliveURLLoader from the current context. + const std::unique_ptr& current_context = + loader_factory_receivers_.current_context(); + // Passes in the pending remote of `client` from a renderer so that `loader` + // can forward response back to the renderer. + auto loader = std::make_unique( + request_id, options, resource_request, std::move(client), + traffic_annotation, current_context->factory, + base::PassKey()); + // Binds `loader` with the pending `receiver` from a renderer to handle URL + // requests. + auto* raw_loader = loader.get(); + auto receiver_id = service_->loader_receivers_.Add( + raw_loader, std::move(receiver), std::move(loader)); + raw_loader->set_on_delete_callback( + base::BindOnce(&KeepAliveURLLoaderService::RemoveLoader, + base::Unretained(service_), receiver_id)); +} + +void KeepAliveURLLoaderService::KeepAliveURLLoaderFactory::Clone( + mojo::PendingReceiver receiver) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + loader_factory_receivers_.Add( + this, std::move(receiver), + std::make_unique( + loader_factory_receivers_.current_context())); +} + +KeepAliveURLLoaderService::KeepAliveURLLoaderService() { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + factory_ = + std::make_unique( + this); +} + +KeepAliveURLLoaderService::~KeepAliveURLLoaderService() = default; + +void KeepAliveURLLoaderService::BindFactory( + mojo::PendingReceiver receiver, + std::unique_ptr pending_factory) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + factory_->BindFactory(std::move(receiver), std::move(pending_factory)); +} + +void KeepAliveURLLoaderService::RemoveLoader( + mojo::ReceiverId loader_receiver_id) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + TRACE_EVENT1("loading", "KeepAliveURLLoaderService::RemoveLoader", + "loader_id", loader_receiver_id); + + loader_receivers_.Remove(loader_receiver_id); +} + +size_t KeepAliveURLLoaderService::NumLoadersForTesting() const { + return loader_receivers_.size(); +} + +} // namespace content diff --git a/content/browser/loader/keep_alive_url_loader_service.h b/content/browser/loader/keep_alive_url_loader_service.h new file mode 100644 index 0000000000000..efb71cd4210e1 --- /dev/null +++ b/content/browser/loader/keep_alive_url_loader_service.h @@ -0,0 +1,72 @@ +// Copyright 2023 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CONTENT_BROWSER_LOADER_KEEP_ALIVE_URL_LOADER_SERVICE_H_ +#define CONTENT_BROWSER_LOADER_KEEP_ALIVE_URL_LOADER_SERVICE_H_ + +#include + +#include "content/common/content_export.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" +#include "mojo/public/cpp/bindings/pending_remote.h" +#include "mojo/public/cpp/bindings/receiver_set.h" +#include "services/network/public/mojom/url_loader_factory.mojom.h" +#include "third_party/blink/public/common/loader/url_loader_factory_bundle.h" + +namespace content { + +// A service that stores bound SharedURLLoaderFactory mojo pipes. Every remote +// of the pipes can be used to create a URLLoader that loads fetch keepalive +// requests. The service is responsible for keeping the loaders in +// `loader_receivers_`. +// +// A renderer can ask this service to handle `fetch(..., {keepalive: true})` or +// `navigator.sendBeacon()` requests by using a remote of URLLoaderFactory bound +// to this service by `BindFactory()`, +// +// Handling keepalive requests in this service allows a request to continue even +// if a renderer unloads before completion, i.e. the request is "keepalive". +// +// Design Doc: +// https://docs.google.com/document/d/1ZzxMMBvpqn8VZBZKnb7Go8TWjnrGcXuLS_USwVVRUvY/edit# +class CONTENT_EXPORT KeepAliveURLLoaderService { + public: + explicit KeepAliveURLLoaderService(); + ~KeepAliveURLLoaderService(); + + // Not Copyable. + KeepAliveURLLoaderService(const KeepAliveURLLoaderService&) = delete; + KeepAliveURLLoaderService& operator=(const KeepAliveURLLoaderService&) = + delete; + + // Binds the pending `receiver` with this service, using `pending_factory`. + // + // The remote of `receiver` can be passed to another process, i.e. renderer, + // to handle fetch keepalive requests. + void BindFactory( + mojo::PendingReceiver receiver, + std::unique_ptr pending_factory); + + // For testing only: + size_t NumLoadersForTesting() const; + + private: + class KeepAliveURLLoaderFactory; + + // Removes the loader receiver held by `loader_receivers_`. + void RemoveLoader(mojo::ReceiverId loader_receiver_id); + + // Many-to-one mojo receiver of URLLoaderFactory. + std::unique_ptr factory_; + + // Holds all the KeepAliveURLLoader connected with remotes in renderers. + // Each of them corresponds to the handling of one pending keepalive request. + mojo::ReceiverSet> + loader_receivers_; +}; + +} // namespace content + +#endif // CONTENT_BROWSER_LOADER_KEEP_ALIVE_URL_LOADER_SERVICE_H_ diff --git a/content/browser/loader/keep_alive_url_loader_service_unittest.cc b/content/browser/loader/keep_alive_url_loader_service_unittest.cc new file mode 100644 index 0000000000000..7eff45483761e --- /dev/null +++ b/content/browser/loader/keep_alive_url_loader_service_unittest.cc @@ -0,0 +1,577 @@ +// Copyright 2023 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/loader/keep_alive_url_loader_service.h" + +#include +#include + +#include "base/strings/stringprintf.h" +#include "base/test/bind.h" +#include "content/public/browser/navigation_entry.h" +#include "content/public/test/test_utils.h" +#include "content/test/test_render_view_host.h" +#include "mojo/public/cpp/system/functions.h" +#include "net/traffic_annotation/network_traffic_annotation_test_helper.h" +#include "services/network/public/cpp/parsed_headers.h" +#include "services/network/public/cpp/resource_request.h" +#include "services/network/public/cpp/wrapper_shared_url_loader_factory.h" +#include "services/network/public/mojom/early_hints.mojom.h" +#include "services/network/public/mojom/ip_address_space.mojom.h" +#include "services/network/public/mojom/referrer_policy.mojom.h" +#include "services/network/public/mojom/url_response_head.mojom.h" +#include "services/network/test/test_url_loader_factory.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace content { +namespace { + +using testing::_; +using testing::Eq; +using testing::WithArg; + +constexpr char kTestRequestUrl[] = "https://example.test"; +constexpr char kTestResponseHeaderName[] = "My-Test-Header"; +constexpr char kTestResponseHeaderValue[] = "my-test-value"; + +// Mock a receiver URLLoaderClient that may exist in renderer. +class MockReceiverURLLoaderClient : public network::mojom::URLLoaderClient { + public: + MockReceiverURLLoaderClient() = default; + MockReceiverURLLoaderClient(const MockReceiverURLLoaderClient&) = delete; + MockReceiverURLLoaderClient& operator=(const MockReceiverURLLoaderClient&) = + delete; + ~MockReceiverURLLoaderClient() override { + if (receiver_.is_bound()) { + // Flush the pipe to make sure there aren't any lingering events. + receiver_.FlushForTesting(); + } + } + + mojo::PendingRemote + BindNewPipeAndPassRemote() { + return receiver_.BindNewPipeAndPassRemote(); + } + + // Note that this also unbinds the receiver. + void ResetReceiver() { receiver_.reset(); } + + // `network::mojom::URLLoaderClient` overrides: + MOCK_METHOD1(OnReceiveEarlyHints, void(network::mojom::EarlyHintsPtr)); + MOCK_METHOD3(OnReceiveResponse, + void(network::mojom::URLResponseHeadPtr, + mojo::ScopedDataPipeConsumerHandle, + absl::optional)); + MOCK_METHOD2(OnReceiveRedirect, + void(const net::RedirectInfo&, + network::mojom::URLResponseHeadPtr)); + MOCK_METHOD3(OnUploadProgress, + void(int64_t, int64_t, base::OnceCallback)); + MOCK_METHOD1(OnTransferSizeUpdated, void(int32_t)); + MOCK_METHOD1(OnComplete, void(const network::URLLoaderCompletionStatus&)); + + private: + mojo::Receiver receiver_{this}; +}; + +// Fakes a URLLoaderFactory that may exist in renderer, which only delegates to +// `remote_url_loader_factory`. +class FakeRemoteURLLoaderFactory { + public: + FakeRemoteURLLoaderFactory() = default; + FakeRemoteURLLoaderFactory(const FakeRemoteURLLoaderFactory&) = delete; + FakeRemoteURLLoaderFactory& operator=(const FakeRemoteURLLoaderFactory&) = + delete; + ~FakeRemoteURLLoaderFactory() = default; + + mojo::PendingReceiver + BindNewPipeAndPassReceiver() { + return remote_url_loader_factory.BindNewPipeAndPassReceiver(); + } + + // Binds `remote_url_loader` to a new URLLoader. + void CreateLoaderAndStart( + const network::ResourceRequest& request, + mojo::PendingRemote client) { + remote_url_loader_factory->CreateLoaderAndStart( + remote_url_loader.BindNewPipeAndPassReceiver(), + /*request_id=*/1, /*options=*/0, request, std::move(client), + net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS)); + remote_url_loader_factory.FlushForTesting(); + ASSERT_TRUE(remote_url_loader.is_connected()); + } + + bool is_remote_url_loader_connected() { + return remote_url_loader.is_connected(); + } + + private: + mojo::Remote remote_url_loader_factory; + mojo::Remote remote_url_loader; +}; + +// Returns true if `arg` has a header of the given `name` and `value`. +// `arg` is an `network::mojom::URLResponseHeadPtr`. +MATCHER_P2(ResponseHasHeader, + name, + value, + base::StringPrintf("Response has %sheader[%s=%s]", + negation ? "no " : "", + name, + value)) { + return arg->headers->HasHeaderValue(name, value); +} + +} // namespace + +class KeepAliveURLLoaderServiceTest : public RenderViewHostTestHarness { + protected: + void SetUp() override { + network_url_loader_factory_ = + std::make_unique(); + // Intercepts Mojo bad-message error. + mojo::SetDefaultProcessErrorHandler( + base::BindLambdaForTesting([&](const std::string& error) { + ASSERT_FALSE(mojo_bad_message_.has_value()); + mojo_bad_message_ = error; + })); + RenderViewHostTestHarness::SetUp(); + } + + void TearDown() override { + network_url_loader_factory_ = nullptr; + loader_service_ = nullptr; + mojo::SetDefaultProcessErrorHandler(base::NullCallback()); + mojo_bad_message_ = absl::nullopt; + RenderViewHostTestHarness::TearDown(); + } + + void ExpectMojoBadMessage(const std::string& message) { + EXPECT_EQ(mojo_bad_message_, message); + } + void ExpectNumPendingRequests(int num_requests) { + EXPECT_EQ(network_url_loader_factory_->NumPending(), num_requests); + } + void ExpectNumKeepAliveURLLoaders(size_t num_loaders) { + EXPECT_EQ(loader_service_->NumLoadersForTesting(), num_loaders); + } + + // Asks KeepAliveURLLoaderService to bind a KeepAliveURLLoaderFactory to the + // given `remote_url_loader_factory`. + // More than one factory can be bound to the same service. + void BindKeepAliveURLLoaderFactory( + FakeRemoteURLLoaderFactory& remote_url_loader_factory) { + if (!loader_service_) { + loader_service_ = std::make_unique(); + } + + mojo::Remote factory; + network_url_loader_factory_->Clone(factory.BindNewPipeAndPassReceiver()); + auto pending_factory = + std::make_unique( + factory.Unbind()); + + // Remote: `remote_url_loader_factory` + // Receiver: Held in `loader_service_`. + loader_service_->BindFactory( + remote_url_loader_factory.BindNewPipeAndPassReceiver(), + std::move(pending_factory)); + } + + network::ResourceRequest CreateResourceRequest(const GURL& url, + bool keepalive = true, + bool is_trusted = false) { + network::ResourceRequest request; + request.url = url; + request.keepalive = keepalive; + if (is_trusted) { + request.trusted_params = network::ResourceRequest::TrustedParams(); + } + return request; + } + + network::mojom::URLResponseHeadPtr CreateResponseHead( + const std::vector>& extra_headers = + {}) { + auto response = network::mojom::URLResponseHead::New(); + response->headers = + base::MakeRefCounted("HTTP/1.1 200 OK\n"); + for (const auto& header : extra_headers) { + response->headers->SetHeader(header.first, header.second); + } + return response; + } + + net::RedirectInfo CreateRedirectInfo() { + net::RedirectInfo redirect_info; + redirect_info.new_method = "GET"; + redirect_info.new_url = GURL("https://redirect.test/"); + redirect_info.status_code = 301; + return redirect_info; + } + + network::mojom::EarlyHintsPtr CreateEarlyHints( + const GURL& url, + const std::vector>& extra_headers = + {}) { + auto response_headers = + base::MakeRefCounted("HTTP/1.1 200 OK\n"); + for (const auto& header : extra_headers) { + response_headers->SetHeader(header.first, header.second); + } + return network::mojom::EarlyHints::New( + network::PopulateParsedHeaders(response_headers.get(), url), + network::mojom::ReferrerPolicy::kDefault, + network::mojom::IPAddressSpace::kPublic); + } + + network::TestURLLoaderFactory::PendingRequest* GetLastPendingRequest() { + return &network_url_loader_factory_->pending_requests()->back(); + } + + private: + // Intercepts network facotry requests instead of using production factory. + std::unique_ptr network_url_loader_factory_ = + nullptr; + // The test target. + std::unique_ptr loader_service_ = nullptr; + absl::optional mojo_bad_message_; +}; + +TEST_F(KeepAliveURLLoaderServiceTest, LoadNonKeepaliveRequestAndTerminate) { + FakeRemoteURLLoaderFactory renderer_loader_factory; + MockReceiverURLLoaderClient renderer_loader_client; + BindKeepAliveURLLoaderFactory(renderer_loader_factory); + + // Loads non-keepalive request: + renderer_loader_factory.CreateLoaderAndStart( + CreateResourceRequest(GURL(kTestRequestUrl), /*keepalive=*/false), + renderer_loader_client.BindNewPipeAndPassRemote()); + + ExpectNumPendingRequests(0); + ExpectNumKeepAliveURLLoaders(0); + EXPECT_FALSE(renderer_loader_factory.is_remote_url_loader_connected()); + ExpectMojoBadMessage( + "Unexpected `resource_request` in " + "KeepAliveURLLoaderService::CreateLoaderAndStart(): " + "resource_request.keepalive must be true"); +} + +TEST_F(KeepAliveURLLoaderServiceTest, LoadTrustedRequestAndTerminate) { + FakeRemoteURLLoaderFactory renderer_loader_factory; + MockReceiverURLLoaderClient renderer_loader_client; + BindKeepAliveURLLoaderFactory(renderer_loader_factory); + + // Loads trusted keepalive request: + renderer_loader_factory.CreateLoaderAndStart( + CreateResourceRequest(GURL(kTestRequestUrl), /*keepalive=*/true, + /*is_trusted=*/true), + renderer_loader_client.BindNewPipeAndPassRemote()); + + ExpectNumPendingRequests(0); + ExpectNumKeepAliveURLLoaders(0); + EXPECT_FALSE(renderer_loader_factory.is_remote_url_loader_connected()); + ExpectMojoBadMessage( + "Unexpected `resource_request` in " + "KeepAliveURLLoaderService::CreateLoaderAndStart(): " + "resource_request.trusted_params must not be set"); +} + +TEST_F(KeepAliveURLLoaderServiceTest, ForwardOnReceiveResponse) { + FakeRemoteURLLoaderFactory renderer_loader_factory; + MockReceiverURLLoaderClient renderer_loader_client; + BindKeepAliveURLLoaderFactory(renderer_loader_factory); + + // Loads keepalive request: + renderer_loader_factory.CreateLoaderAndStart( + CreateResourceRequest(GURL(kTestRequestUrl)), + renderer_loader_client.BindNewPipeAndPassRemote()); + ExpectNumPendingRequests(1); + ExpectNumKeepAliveURLLoaders(1); + + // OnReceiveResponse: + // Expects underlying KeepAliveURLLoader forwards to `renderer_loader_client`. + EXPECT_CALL(renderer_loader_client, + OnReceiveResponse(ResponseHasHeader(kTestResponseHeaderName, + kTestResponseHeaderValue), + _, Eq(absl::nullopt))) + .Times(1); + // Simluates receiving response in the network service. + GetLastPendingRequest()->client->OnReceiveResponse( + CreateResponseHead({{kTestResponseHeaderName, kTestResponseHeaderValue}}), + /*body=*/{}, absl::nullopt); + base::RunLoop().RunUntilIdle(); + ExpectNumKeepAliveURLLoaders(1); +} + +TEST_F(KeepAliveURLLoaderServiceTest, + OnReceiveResponseWhenRendererIsDisconnected) { + FakeRemoteURLLoaderFactory renderer_loader_factory; + MockReceiverURLLoaderClient renderer_loader_client; + BindKeepAliveURLLoaderFactory(renderer_loader_factory); + + // Loads keepalive request: + renderer_loader_factory.CreateLoaderAndStart( + CreateResourceRequest(GURL(kTestRequestUrl)), + renderer_loader_client.BindNewPipeAndPassRemote()); + ExpectNumPendingRequests(1); + ExpectNumKeepAliveURLLoaders(1); + + // OnReceiveResponse: + // Disconnects and unbinds the receiver client from KeepAliveURLLoader. + renderer_loader_client.ResetReceiver(); + // Expects no forwarding. + EXPECT_CALL(renderer_loader_client, OnReceiveResponse(_, _, _)).Times(0); + // Simluates receiving response in the network service. + GetLastPendingRequest()->client->OnReceiveResponse( + CreateResponseHead({{kTestResponseHeaderName, kTestResponseHeaderValue}}), + /*body=*/{}, absl::nullopt); + base::RunLoop().RunUntilIdle(); + // The loader should have been deleted by the service. + ExpectNumKeepAliveURLLoaders(0); +} + +TEST_F(KeepAliveURLLoaderServiceTest, ForwardOnReceiveRedirect) { + FakeRemoteURLLoaderFactory renderer_loader_factory; + MockReceiverURLLoaderClient renderer_loader_client; + BindKeepAliveURLLoaderFactory(renderer_loader_factory); + + // Loads keepalive request: + renderer_loader_factory.CreateLoaderAndStart( + CreateResourceRequest(GURL(kTestRequestUrl)), + renderer_loader_client.BindNewPipeAndPassRemote()); + ExpectNumPendingRequests(1); + ExpectNumKeepAliveURLLoaders(1); + + // OnReceiveRedirect: + // Expects underlying KeepAliveURLLoader forwards to `renderer_loader_client`. + EXPECT_CALL(renderer_loader_client, + OnReceiveRedirect(_, ResponseHasHeader(kTestResponseHeaderName, + kTestResponseHeaderValue))) + .Times(1); + // Simluates receiving redirect in the network service. + GetLastPendingRequest()->client->OnReceiveRedirect( + CreateRedirectInfo(), CreateResponseHead({{kTestResponseHeaderName, + kTestResponseHeaderValue}})); + base::RunLoop().RunUntilIdle(); +} + +TEST_F(KeepAliveURLLoaderServiceTest, + OnReceiveRedirectWhenRendererIsDisconnected) { + FakeRemoteURLLoaderFactory renderer_loader_factory; + MockReceiverURLLoaderClient renderer_loader_client; + BindKeepAliveURLLoaderFactory(renderer_loader_factory); + + // Loads keepalive request: + renderer_loader_factory.CreateLoaderAndStart( + CreateResourceRequest(GURL(kTestRequestUrl)), + renderer_loader_client.BindNewPipeAndPassRemote()); + ExpectNumPendingRequests(1); + ExpectNumKeepAliveURLLoaders(1); + + // OnReceiveRedirect: + // Disconnects the receiver client from KeepAliveURLLoader. + renderer_loader_client.ResetReceiver(); + // Expects no forwarding. + EXPECT_CALL(renderer_loader_client, OnReceiveRedirect(_, _)).Times(0); + // Simluates receiving redirect in the network service. + GetLastPendingRequest()->client->OnReceiveRedirect( + CreateRedirectInfo(), CreateResponseHead({{kTestResponseHeaderName, + kTestResponseHeaderValue}})); + base::RunLoop().RunUntilIdle(); +} + +TEST_F(KeepAliveURLLoaderServiceTest, ForwardOnReceiveEarlyHints) { + FakeRemoteURLLoaderFactory renderer_loader_factory; + MockReceiverURLLoaderClient renderer_loader_client; + BindKeepAliveURLLoaderFactory(renderer_loader_factory); + + // Loads keepalive request: + renderer_loader_factory.CreateLoaderAndStart( + CreateResourceRequest(GURL(kTestRequestUrl)), + renderer_loader_client.BindNewPipeAndPassRemote()); + ExpectNumPendingRequests(1); + ExpectNumKeepAliveURLLoaders(1); + + // OnReceiveEarlyHints: + // Expects underlying KeepAliveURLLoader forwards to `renderer_loader_client`. + EXPECT_CALL(renderer_loader_client, OnReceiveEarlyHints(_)).Times(1); + // Simluates receiving early hints in the network service. + GetLastPendingRequest()->client->OnReceiveEarlyHints( + CreateEarlyHints(GURL(kTestRequestUrl))); + base::RunLoop().RunUntilIdle(); +} + +TEST_F(KeepAliveURLLoaderServiceTest, + OnReceiveEarlyHintsWhenRendererIsDisconnected) { + FakeRemoteURLLoaderFactory renderer_loader_factory; + MockReceiverURLLoaderClient renderer_loader_client; + BindKeepAliveURLLoaderFactory(renderer_loader_factory); + + // Loads keepalive request: + renderer_loader_factory.CreateLoaderAndStart( + CreateResourceRequest(GURL(kTestRequestUrl)), + renderer_loader_client.BindNewPipeAndPassRemote()); + ExpectNumPendingRequests(1); + ExpectNumKeepAliveURLLoaders(1); + + // OnReceiveEarlyHints: + // Disconnects the receiver client from KeepAliveURLLoader. + renderer_loader_client.ResetReceiver(); + // Expects no forwarding. + EXPECT_CALL(renderer_loader_client, OnReceiveEarlyHints(_)).Times(0); + // Simluates receiving early hints in the network service. + GetLastPendingRequest()->client->OnReceiveEarlyHints( + CreateEarlyHints(GURL(kTestRequestUrl))); + base::RunLoop().RunUntilIdle(); +} + +TEST_F(KeepAliveURLLoaderServiceTest, ForwardOnUploadProgress) { + FakeRemoteURLLoaderFactory renderer_loader_factory; + MockReceiverURLLoaderClient renderer_loader_client; + BindKeepAliveURLLoaderFactory(renderer_loader_factory); + + // Loads keepalive request: + renderer_loader_factory.CreateLoaderAndStart( + CreateResourceRequest(GURL(kTestRequestUrl)), + renderer_loader_client.BindNewPipeAndPassRemote()); + ExpectNumPendingRequests(1); + ExpectNumKeepAliveURLLoaders(1); + + // OnUploadProgress: + const int64_t current_position = 5; + const int64_t total_size = 100; + base::OnceCallback callback; + // Expects underlying KeepAliveURLLoader forwards to `renderer_loader_client`. + EXPECT_CALL(renderer_loader_client, + OnUploadProgress(Eq(current_position), Eq(total_size), _)) + .Times(1) + .WillOnce(WithArg<2>([](base::OnceCallback callback) { + // must be consumed. + std::move(callback).Run(); + })); + // Simluates receiving upload progress in the network service. + GetLastPendingRequest()->client->OnUploadProgress( + current_position, total_size, std::move(callback)); + base::RunLoop().RunUntilIdle(); +} + +TEST_F(KeepAliveURLLoaderServiceTest, ForwardOnTransferSizeUpdated) { + FakeRemoteURLLoaderFactory renderer_loader_factory; + MockReceiverURLLoaderClient renderer_loader_client; + BindKeepAliveURLLoaderFactory(renderer_loader_factory); + + // Loads keepalive request: + renderer_loader_factory.CreateLoaderAndStart( + CreateResourceRequest(GURL(kTestRequestUrl)), + renderer_loader_client.BindNewPipeAndPassRemote()); + ExpectNumPendingRequests(1); + ExpectNumKeepAliveURLLoaders(1); + + // OnTransferSizeUpdated: + const int32_t size_diff = 5; + // Expects underlying KeepAliveURLLoader forwards to `renderer_loader_client`. + EXPECT_CALL(renderer_loader_client, OnTransferSizeUpdated(Eq(size_diff))) + .Times(1); + // Simluates receiving transfer size update in the network service. + GetLastPendingRequest()->client->OnTransferSizeUpdated(size_diff); + base::RunLoop().RunUntilIdle(); +} + +TEST_F(KeepAliveURLLoaderServiceTest, + OnTransferSizeUpdatedWhenRendererIsDisconnected) { + FakeRemoteURLLoaderFactory renderer_loader_factory; + MockReceiverURLLoaderClient renderer_loader_client; + BindKeepAliveURLLoaderFactory(renderer_loader_factory); + + // Loads keepalive request: + renderer_loader_factory.CreateLoaderAndStart( + CreateResourceRequest(GURL(kTestRequestUrl)), + renderer_loader_client.BindNewPipeAndPassRemote()); + ExpectNumPendingRequests(1); + ExpectNumKeepAliveURLLoaders(1); + + // OnTransferSizeUpdated: + // Disconnects the receiver client from KeepAliveURLLoader. + renderer_loader_client.ResetReceiver(); + const int32_t size_diff = 5; + // Expects no forwarding. + EXPECT_CALL(renderer_loader_client, OnTransferSizeUpdated(_)).Times(0); + // Simluates receiving transfer size update in the network service. + GetLastPendingRequest()->client->OnTransferSizeUpdated(size_diff); + base::RunLoop().RunUntilIdle(); +} + +TEST_F(KeepAliveURLLoaderServiceTest, ForwardOnComplete) { + FakeRemoteURLLoaderFactory renderer_loader_factory; + MockReceiverURLLoaderClient renderer_loader_client; + BindKeepAliveURLLoaderFactory(renderer_loader_factory); + + // Loads keepalive request: + renderer_loader_factory.CreateLoaderAndStart( + CreateResourceRequest(GURL(kTestRequestUrl)), + renderer_loader_client.BindNewPipeAndPassRemote()); + ExpectNumPendingRequests(1); + ExpectNumKeepAliveURLLoaders(1); + + // OnComplete: + const network::URLLoaderCompletionStatus status{net::OK}; + // Expects underlying KeepAliveURLLoader forwards to `renderer_loader_client`. + EXPECT_CALL(renderer_loader_client, OnComplete(Eq(status))).Times(1); + // Simluates receiving completion status in the network service. + GetLastPendingRequest()->client->OnComplete(status); + base::RunLoop().RunUntilIdle(); +} + +TEST_F(KeepAliveURLLoaderServiceTest, OnCompleteWhenRendererIsDisconnected) { + FakeRemoteURLLoaderFactory renderer_loader_factory; + MockReceiverURLLoaderClient renderer_loader_client; + BindKeepAliveURLLoaderFactory(renderer_loader_factory); + + // Loads keepalive request: + renderer_loader_factory.CreateLoaderAndStart( + CreateResourceRequest(GURL(kTestRequestUrl)), + renderer_loader_client.BindNewPipeAndPassRemote()); + ExpectNumPendingRequests(1); + ExpectNumKeepAliveURLLoaders(1); + + // OnComplete: + // Disconnects the receiver client from KeepAliveURLLoader. + renderer_loader_client.ResetReceiver(); + const network::URLLoaderCompletionStatus status{net::OK}; + // Expects no forwarding. + EXPECT_CALL(renderer_loader_client, OnComplete(_)).Times(0); + // Simluates receiving completion status in the network service. + GetLastPendingRequest()->client->OnComplete(status); + base::RunLoop().RunUntilIdle(); +} + +TEST_F(KeepAliveURLLoaderServiceTest, RendererDisconnectedBeforeOnComplete) { + FakeRemoteURLLoaderFactory renderer_loader_factory; + MockReceiverURLLoaderClient renderer_loader_client; + BindKeepAliveURLLoaderFactory(renderer_loader_factory); + + // Loads keepalive request: + renderer_loader_factory.CreateLoaderAndStart( + CreateResourceRequest(GURL(kTestRequestUrl)), + renderer_loader_client.BindNewPipeAndPassRemote()); + ExpectNumPendingRequests(1); + ExpectNumKeepAliveURLLoaders(1); + + // OnReceiveResponse + // Simluates receiving response in the network service. + GetLastPendingRequest()->client->OnReceiveResponse( + CreateResponseHead({{kTestResponseHeaderName, kTestResponseHeaderValue}}), + /*body=*/{}, absl::nullopt); + + // Disconnects the receiver client from KeepAliveURLLoader. + renderer_loader_client.ResetReceiver(); + base::RunLoop().RunUntilIdle(); + + // The loader should have been deleted. + ExpectNumKeepAliveURLLoaders(0); +} + +} // namespace content diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn index 31f131ad7c15b..56eb128a0c8b6 100644 --- a/content/test/BUILD.gn +++ b/content/test/BUILD.gn @@ -2336,6 +2336,7 @@ test("content_unittests") { "../browser/interest_group/test_interest_group_private_aggregation_manager.h", "../browser/loader/cors_origin_pattern_setter_unittest.cc", "../browser/loader/file_url_loader_factory_unittest.cc", + "../browser/loader/keep_alive_url_loader_service_unittest.cc", "../browser/loader/merkle_integrity_source_stream_unittest.cc", "../browser/loader/navigation_early_hints_manager_unittest.cc", "../browser/loader/navigation_url_loader_impl_unittest.cc",