#include "content/browser/loader/keep_alive_url_loader.h"
#include <memory>
#include <utility>
#include <vector>
#include "base/check_is_test.h"
#include "base/feature_list.h"
#include "base/features.h"
#include "base/functional/bind.h"
#include "base/memory/scoped_refptr.h"
#include "base/metrics/histogram.h"
#include "base/metrics/histogram_base.h"
#include "base/metrics/histogram_functions.h"
#include "base/no_destructor.h"
#include "base/strings/strcat.h"
#include "base/trace_event/trace_event.h"
#include "base/trace_event/typed_macros.h"
#include "base/unguessable_token.h"
#include "content/browser/devtools/devtools_instrumentation.h"
#include "content/browser/loader/keep_alive_attribution_request_helper.h"
#include "content/browser/renderer_host/mixed_content_checker.h"
#include "content/browser/renderer_host/policy_container_host.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/keep_alive_request_tracker.h"
#include "content/public/common/content_client.h"
#include "content/public/common/url_utils.h"
#include "net/base/load_flags.h"
#include "net/http/http_request_headers.h"
#include "net/http/http_response_headers.h"
#include "net/url_request/redirect_util.h"
#include "services/network/public/cpp/content_security_policy/content_security_policy.h"
#include "services/network/public/cpp/content_security_policy/csp_context.h"
#include "services/network/public/cpp/devtools_observer_util.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/url_loader_completion_status.h"
#include "services/network/public/mojom/content_security_policy.mojom.h"
#include "services/network/public/mojom/early_hints.mojom.h"
#include "services/network/public/mojom/fetch_api.mojom-shared.h"
#include "services/network/public/mojom/url_request.mojom.h"
#include "third_party/abseil-cpp/absl/container/flat_hash_map.h"
#include "third_party/blink/public/common/features.h"
namespace features {
const base::FeatureParam<size_t> kMaxRetryCount{&blink::features::kFetchRetry,
"max_retry_count", 10};
const base::FeatureParam<base::TimeDelta> kMinRetryDelta{
&blink::features::kFetchRetry, "min_retry_delta", base::Milliseconds(500)};
const base::FeatureParam<double> kMinRetryBackoffFactor{
&blink::features::kFetchRetry, "min_retry_backoff", 1.0};
const base::FeatureParam<base::TimeDelta> kMaxRetryAge{
&blink::features::kFetchRetry, "max_retry_age", base::Days(1)};
const base::FeatureParam<bool> kAddRetryHeader{&blink::features::kFetchRetry,
"add_retry_header", true};
}
namespace content {
namespace {
class WrappedThreadChecker {
public:
void Check() { DCHECK_CALLED_ON_VALID_THREAD(thread_checker); }
private:
THREAD_CHECKER(thread_checker);
};
constexpr net::NetworkTrafficAnnotationTag kKeepAliveRetryAnnotationTag =
net::DefineNetworkTrafficAnnotation("keepalive_fetch_retry", R"(
semantics {
sender: "KeepAlive Fetch Retry Mechanism"
description:
"This request is an automated retry of a fetch request that was "
"originally initiated by a website with the `keepalive: true` flag "
"and `retryOptions` specifying that retries should occur. This "
"retry is triggered by the browser process if the initial attempt or "
"a subsequent retry fails, and can occur even if the originating "
"renderer process has been closed. The purpose is to increase the "
"likelihood of successful data delivery for critical beacons, logs, "
"or other data the website intended to send reliably."
trigger:
"An initial keepalive fetch request (or a previous retry of it) "
"failed due to a network error or a specific HTTP 5xx server error "
"(if configured in RetryOptions). The RetryOptions associated with "
"the original request dictated that a retry should be attempted. "
"This specific request is triggered by an automated timer within the "
"browser process's KeepAliveURLLoader after a calculated backoff period."
data:
"The same HTTP method, URL, headers, and body as the original "
"keepalive request that is being retried. This data is determined "
"by the website that initiated the original fetch request."
destination: WEBSITE
internal {
contacts {
email: "chrome-loading@google.com"
}
}
user_data {
type: ARBITRARY_DATA
type: SENSITIVE_URL
}
last_reviewed: "2025-05-27"
}
policy {
cookies_allowed: YES
cookies_store: "Same as the original request. Governed by the "
"credentials mode ('omit', 'same-origin', 'include') "
"of the original fetch request."
setting:
"This retry mechanism is a sub-feature of the Fetch API's "
"`keepalive` and the proposed `retryOptions`. Websites enable this "
"by constructing fetch requests with these options. Users can "
"disable JavaScript or clear/block cookies for sites, which would "
"affect the original request and consequently prevent retries. "
"There isn't a separate browser setting to disable only the retry "
"aspect of keepalive fetches."
policy_exception_justification:
"This is an automated follow-up to a user-initiated (via website "
"JavaScript) keepalive request."
}
)");
constexpr base::TimeDelta kDefaultDisconnectedKeepAliveURLLoaderTimeout =
base::Seconds(30);
base::TimeDelta GetDisconnectedKeepAliveURLLoaderTimeout() {
return base::Seconds(GetFieldTrialParamByFeatureAsInt(
blink::features::kKeepAliveInBrowserMigration,
"disconnected_loader_timeout_seconds",
base::checked_cast<int32_t>(
kDefaultDisconnectedKeepAliveURLLoaderTimeout.InSeconds())));
}
enum class FetchLaterBrowserMetricType {
kAbortedByInitiator = 0,
kStartedAfterInitiatorDisconnected = 1,
kStartedByInitiator = 2,
kCancelledAfterTimeLimit = 3,
kStartedWhenShutdown = 4,
kMaxValue = kStartedWhenShutdown,
};
void LogFetchLaterMetric(const FetchLaterBrowserMetricType& type) {
base::UmaHistogramEnumeration("FetchLater.Browser.Metrics", type);
}
class KeepAliveURLLoaderCSPContext final : public network::CSPContext {
public:
void ReportContentSecurityPolicyViolation(
network::mojom::CSPViolationPtr violation_params) final {
}
void SanitizeDataForUseInCspViolation(
network::mojom::CSPDirectiveName directive,
GURL* blocked_url,
network::mojom::SourceLocation* source_location) const final {
}
};
bool IsRedirectAllowedByCSP(
const std::vector<network::mojom::ContentSecurityPolicyPtr>& policies,
const GURL& url,
const GURL& url_before_redirects,
bool has_followed_redirect) {
auto directive = network::mojom::CSPDirectiveName::ConnectSrc;
auto empty_source_location = network::mojom::SourceLocation::New();
auto disposition = network::CSPContext::CheckCSPDisposition::CHECK_ALL_CSP;
return KeepAliveURLLoaderCSPContext()
.IsAllowedByCsp(policies, directive, url, url_before_redirects,
has_followed_redirect, empty_source_location, disposition,
false)
.IsAllowed();
}
bool IsNetErrorEligibleForRetry(int net_error) {
return
net_error == net::ERR_TIMED_OUT ||
net_error == net::ERR_CONNECTION_TIMED_OUT ||
net_error == net::ERR_CONNECTION_CLOSED ||
net_error == net::ERR_CONNECTION_REFUSED ||
net_error == net::ERR_CONNECTION_RESET ||
net_error == net::ERR_CONNECTION_FAILED ||
net_error == net::ERR_ADDRESS_UNREACHABLE ||
net_error == net::ERR_NETWORK_CHANGED ||
net_error == net::ERR_TUNNEL_CONNECTION_FAILED ||
net_error == net::ERR_PROXY_CONNECTION_FAILED ||
net_error == net::ERR_SOCKS_CONNECTION_FAILED ||
net_error == net::ERR_HTTP2_PING_FAILED ||
net_error == net::ERR_HTTP2_PROTOCOL_ERROR ||
net_error == net::ERR_QUIC_PROTOCOL_ERROR ||
net_error == net::ERR_NAME_NOT_RESOLVED ||
net_error == net::ERR_INTERNET_DISCONNECTED ||
net_error == net::ERR_NAME_RESOLUTION_FAILED;
}
bool IsServerGuaranteedToBeNotReachedYet(int net_error) {
return net_error == net::ERR_CONNECTION_REFUSED ||
net_error == net::ERR_ADDRESS_UNREACHABLE ||
net_error == net::ERR_TUNNEL_CONNECTION_FAILED ||
net_error == net::ERR_PROXY_CONNECTION_FAILED ||
net_error == net::ERR_SOCKS_CONNECTION_FAILED ||
net_error == net::ERR_NAME_NOT_RESOLVED ||
net_error == net::ERR_NAME_RESOLUTION_FAILED;
}
}
class KeepAliveURLLoader::ForwardingClient final
: public network::mojom::URLLoaderClient {
public:
ForwardingClient(
KeepAliveURLLoader* loader,
mojo::PendingRemote<network::mojom::URLLoaderClient> forwarding_client)
: keep_alive_url_loader_(std::move(loader)),
target_(std::move(forwarding_client)) {
CHECK(keep_alive_url_loader_);
if (target_) {
target_.set_disconnect_handler(base::BindOnce(
&ForwardingClient::OnDisconnected, base::Unretained(this)));
}
}
ForwardingClient(const ForwardingClient&) = delete;
ForwardingClient& operator=(const ForwardingClient&) = delete;
int32_t request_id() const { return keep_alive_url_loader_->request_id_; }
bool IsConnected() const { return !!target_; }
void OnDisconnected();
void OnReceiveEarlyHints(network::mojom::EarlyHintsPtr early_hints) override {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
TRACE_EVENT("loading",
"KeepAliveURLLoader::ForwardingClient::OnReceiveEarlyHints",
"request_id", request_id());
if (IsConnected()) {
target_->OnReceiveEarlyHints(std::move(early_hints));
return;
}
}
void OnUploadProgress(int64_t current_position,
int64_t total_size,
base::OnceCallback<void()> callback) override {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
TRACE_EVENT("loading",
"KeepAliveURLLoader::ForwardingClient::OnUploadProgress",
"request_id", request_id());
if (IsConnected()) {
target_->OnUploadProgress(current_position, total_size,
std::move(callback));
return;
}
}
void OnTransferSizeUpdated(int32_t transfer_size_diff) override {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
TRACE_EVENT("loading",
"KeepAliveURLLoader::ForwardingClient::OnTransferSizeUpdated",
"request_id", request_id());
if (IsConnected()) {
target_->OnTransferSizeUpdated(transfer_size_diff);
return;
}
}
#if BUILDFLAG(ARKWEB_RESOURCE_INTERCEPTION)
void OnTransferDataWithSharedMemory(base::ReadOnlySharedMemoryRegion region, uint64_t buffer_size) override {}
#endif
void OnReceiveRedirect(const net::RedirectInfo& redirect_info,
network::mojom::URLResponseHeadPtr head) override {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
CHECK(IsConnected());
target_->OnReceiveRedirect(redirect_info, std::move(head));
}
void OnReceiveResponse(
network::mojom::URLResponseHeadPtr head,
mojo::ScopedDataPipeConsumerHandle body,
std::optional<mojo_base::BigBuffer> cached_metadata) override {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
CHECK(IsConnected());
target_->OnReceiveResponse(std::move(head), std::move(body),
std::move(cached_metadata));
}
void OnComplete(
const network::URLLoaderCompletionStatus& completion_status) override {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
CHECK(IsConnected());
target_->OnComplete(completion_status);
}
private:
const raw_ptr<KeepAliveURLLoader> keep_alive_url_loader_;
mojo::Remote<network::mojom::URLLoaderClient> target_;
};
struct KeepAliveURLLoader::StoredURLLoad {
StoredURLLoad() = default;
StoredURLLoad(const StoredURLLoad&) = delete;
StoredURLLoad& operator=(const StoredURLLoad&) = delete;
struct RedirectData {
RedirectData(const net::RedirectInfo& redirect_info,
network::mojom::URLResponseHeadPtr response_head)
: info(redirect_info), head(std::move(response_head)) {}
RedirectData(const RedirectData&) = delete;
RedirectData& operator=(const RedirectData&) = delete;
net::RedirectInfo info;
network::mojom::URLResponseHeadPtr head;
};
struct ResponseData {
ResponseData(network::mojom::URLResponseHeadPtr response_head,
mojo::ScopedDataPipeConsumerHandle body_handle,
std::optional<mojo_base::BigBuffer> cached_metadata)
: head(std::move(response_head)),
body(std::move(body_handle)),
metadata(std::move(cached_metadata)) {}
ResponseData(const ResponseData&) = delete;
ResponseData& operator=(const ResponseData&) = delete;
network::mojom::URLResponseHeadPtr head;
mojo::ScopedDataPipeConsumerHandle body;
std::optional<mojo_base::BigBuffer> metadata;
};
std::queue<std::unique_ptr<RedirectData>> redirects;
std::unique_ptr<ResponseData> response = nullptr;
std::optional<network::URLLoaderCompletionStatus> completion_status =
std::nullopt;
bool forwarding = false;
};
KeepAliveURLLoader::KeepAliveURLLoader(
int32_t request_id,
uint32_t options,
const network::ResourceRequest& resource_request,
mojo::PendingRemote<network::mojom::URLLoaderClient> forwarding_client,
const net::MutableNetworkTrafficAnnotationTag& traffic_annotation,
scoped_refptr<network::SharedURLLoaderFactory> network_loader_factory,
scoped_refptr<PolicyContainerHost> policy_container_host,
WeakDocumentPtr weak_document_ptr,
net::NetworkIsolationKey network_isolation_key,
std::optional<ukm::SourceId> ukm_source_id,
StoragePartitionImpl* storage_partition,
URLLoaderThrottlesGetter throttles_getter,
base::PassKey<KeepAliveURLLoaderService>,
std::unique_ptr<KeepAliveAttributionRequestHelper>
attribution_request_helper)
: request_id_(request_id),
devtools_request_id_(base::UnguessableToken::Create().ToString()),
options_(options),
resource_request_(resource_request),
forwarding_client_(
std::make_unique<ForwardingClient>(this,
std::move(forwarding_client))),
traffic_annotation_(net::NetworkTrafficAnnotationTag(traffic_annotation)),
network_loader_factory_(std::move(network_loader_factory)),
stored_url_load_(std::make_unique<StoredURLLoad>()),
policy_container_host_(std::move(policy_container_host)),
weak_document_ptr_(std::move(weak_document_ptr)),
network_isolation_key_(network_isolation_key),
ukm_source_id_(ukm_source_id),
request_tracker_(
GetContentClient()->browser()->MaybeCreateKeepAliveRequestTracker(
resource_request,
ukm_source_id,
base::BindRepeating(&KeepAliveURLLoader::IsContextDetached,
base::Unretained(this)))),
storage_partition_(storage_partition),
initial_url_(resource_request.url),
last_url_(resource_request.url),
throttles_getter_(throttles_getter),
attribution_request_helper_(std::move(attribution_request_helper)) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
CHECK(network_loader_factory_);
CHECK(policy_container_host_);
CHECK(!resource_request.trusted_params);
CHECK(storage_partition_);
TRACE_EVENT("loading", "KeepAliveURLLoader::KeepAliveURLLoader", "request_id",
request_id_, "url", last_url_);
TRACE_EVENT_BEGIN("loading", "KeepAliveURLLoader",
perfetto::Track(request_id_), "url", last_url_);
original_resource_request_ = resource_request_;
LogFetchKeepAliveRequestMetric("Total");
if (IsFetchLater()) {
base::UmaHistogramBoolean("FetchLater.Browser.Total", true);
}
}
void KeepAliveURLLoader::Start() {
StartInternal(false);
}
void KeepAliveURLLoader::StartInternal(bool is_retry) {
CHECK(!is_started_ || is_retry);
TRACE_EVENT("loading", "KeepAliveURLLoader::Start", "request_id",
request_id_);
is_started_ = true;
if (!is_retry) {
first_request_start_time_ = base::TimeTicks::Now();
}
LogFetchKeepAliveRequestMetric(is_retry ? "Retried" : "Started");
if (request_tracker_) {
request_tracker_->AdvanceToNextStage(
KeepAliveRequestTracker::RequestStageType::kRequestStarted);
}
if (IsFetchLater()) {
if (is_retry) {
base::UmaHistogramBoolean("FetchLater.Browser.Total.Retried", true);
} else {
base::UmaHistogramBoolean("FetchLater.Browser.Total.Started", true);
}
if (RenderFrameHostImpl* rfh = GetInitiator(); rfh) {
devtools_instrumentation::OnFetchKeepAliveRequestWillBeSent(
rfh->frame_tree_node(), devtools_request_id_, resource_request_);
}
}
GetContentClient()->browser()->OnKeepaliveRequestStarted(
storage_partition_->browser_context());
url_loader_ = blink::ThrottlingURLLoader::CreateLoaderAndStart(
network_loader_factory_, throttles_getter_.Run(), request_id_, options_,
&resource_request_, forwarding_client_.get(),
is_retry ? kKeepAliveRetryAnnotationTag : traffic_annotation_,
base::SingleThreadTaskRunner::GetCurrentDefault(),
std::nullopt,
this);
}
KeepAliveURLLoader::~KeepAliveURLLoader() {
TRACE_EVENT("loading", "KeepAliveURLLoader::~KeepAliveURLLoader",
"request_id", request_id_);
TRACE_EVENT_END("loading", perfetto::Track(request_id_));
request_tracker_.reset();
disconnected_loader_timer_.Stop();
if (IsStarted()) {
GetContentClient()->browser()->OnKeepaliveRequestFinished();
}
}
void KeepAliveURLLoader::set_on_delete_callback(
OnDeleteCallback on_delete_callback) {
on_delete_callback_ = std::move(on_delete_callback);
}
void KeepAliveURLLoader::set_check_retry_eligibility_callback(
CheckRetryEligibilityCallback check_retry_eligibility_callback) {
check_retry_eligibility_callback_ =
std::move(check_retry_eligibility_callback);
}
void KeepAliveURLLoader::set_on_retry_scheduled_callback(
OnRetryScheduledCallback on_retry_scheduled_callback) {
on_retry_scheduled_callback_ = std::move(on_retry_scheduled_callback);
}
base::WeakPtr<KeepAliveURLLoader> KeepAliveURLLoader::GetWeakPtr() {
return weak_ptr_factory_.GetWeakPtr();
}
bool KeepAliveURLLoader::IsStarted() const {
return is_started_;
}
bool KeepAliveURLLoader::IsAttemptingRetry(bool include_failed_retry) const {
return retry_state_ != RetryState::kNotAttemptingRetry &&
(include_failed_retry || retry_state_ != RetryState::kRetryFailed);
}
RenderFrameHostImpl* KeepAliveURLLoader::GetInitiator() const {
return static_cast<RenderFrameHostImpl*>(
weak_document_ptr_.AsRenderFrameHostIfValid());
}
bool KeepAliveURLLoader::IsContextDetached() const {
return !GetInitiator();
}
void KeepAliveURLLoader::FollowRedirect(
const std::vector<std::string>& removed_headers,
const net::HttpRequestHeaders& modified_headers,
const net::HttpRequestHeaders& modified_cors_exempt_headers,
const std::optional<GURL>& new_url) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
TRACE_EVENT("loading", "KeepAliveURLLoader::FollowRedirect", "request_id",
request_id_, "url", new_url);
if (new_url != std::nullopt) {
mojo::ReportBadMessage(
"Unexpected `new_url` in KeepAliveURLLoader::FollowRedirect(): "
"must be null");
return;
}
if (IsRendererConnected()) {
ForwardURLLoad();
return;
}
DeleteSelf();
}
void KeepAliveURLLoader::SetPriority(net::RequestPriority priority,
int intra_priority_value) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
TRACE_EVENT("loading", "KeepAliveURLLoader::SetPriority", "request_id",
request_id_);
url_loader_->SetPriority(priority, intra_priority_value);
}
void KeepAliveURLLoader::EndReceiveRedirect(
const net::RedirectInfo& redirect_info,
network::mojom::URLResponseHeadPtr head) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
did_encounter_redirect_ = true;
CHECK_GT(redirect_limit_, 0u);
if (--redirect_limit_ == 0) {
OnComplete(network::URLLoaderCompletionStatus(net::ERR_TOO_MANY_REDIRECTS));
return;
}
TRACE_EVENT("loading", "KeepAliveURLLoader::EndReceiveRedirect", "request_id",
request_id_);
if (request_tracker_) {
request_tracker_->AdvanceToNextStage(
request_tracker_->GetNextRedirectStageType());
}
if (IsFetchLater()) {
if (auto* rfh = GetInitiator(); rfh) {
auto redirect_head_info = network::ExtractDevToolsInfo(*head);
std::pair<const GURL&, const network::mojom::URLResponseHeadDevToolsInfo&>
redirect_info_for_devtools{last_url_, *redirect_head_info};
devtools_instrumentation::OnFetchKeepAliveRequestWillBeSent(
rfh->frame_tree_node(), devtools_request_id_, resource_request_,
redirect_info_for_devtools);
}
}
scoped_refptr<net::HttpResponseHeaders> headers = head->headers;
stored_url_load_->redirects.emplace(
std::make_unique<StoredURLLoad::RedirectData>(redirect_info,
std::move(head)));
if (net::Error err = WillFollowRedirect(redirect_info); err != net::OK) {
OnComplete(network::URLLoaderCompletionStatus(err));
return;
}
if (attribution_request_helper_) {
attribution_request_helper_->OnReceiveRedirect(headers,
redirect_info.new_url);
}
resource_request_.url = redirect_info.new_url;
resource_request_.site_for_cookies = redirect_info.new_site_for_cookies;
resource_request_.referrer = GURL(redirect_info.new_referrer);
resource_request_.referrer_policy = redirect_info.new_referrer_policy;
last_url_ = GURL(redirect_info.new_url);
if (observer_for_testing_) {
CHECK_IS_TEST();
observer_for_testing_->OnReceiveRedirectProcessed(this);
}
url_loader_->FollowRedirect(
{}, {},
{});
}
void KeepAliveURLLoader::OnReceiveResponse(
network::mojom::URLResponseHeadPtr response,
mojo::ScopedDataPipeConsumerHandle body,
std::optional<mojo_base::BigBuffer> cached_metadata) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
TRACE_EVENT("loading", "KeepAliveURLLoader::OnReceiveResponse", "request_id",
request_id_, "url", last_url_);
LogFetchKeepAliveRequestMetric("Succeeded");
if (request_tracker_) {
request_tracker_->AdvanceToNextStage(
KeepAliveRequestTracker::RequestStageType::kResponseReceived);
}
if (observer_for_testing_) {
CHECK_IS_TEST();
observer_for_testing_->OnReceiveResponse(this);
}
if (IsFetchLater()) {
if (auto* rfh = GetInitiator(); rfh) {
devtools_instrumentation::OnFetchKeepAliveResponseReceived(
rfh->frame_tree_node(), devtools_request_id_, last_url_, *response);
}
}
if (attribution_request_helper_) {
attribution_request_helper_->OnReceiveResponse(response->headers.get());
attribution_request_helper_.reset();
}
stored_url_load_->response = std::make_unique<StoredURLLoad::ResponseData>(
std::move(response), std::move(body), std::move(cached_metadata));
if (IsRendererConnected()) {
ForwardURLLoad();
return;
}
if (observer_for_testing_) {
CHECK_IS_TEST();
observer_for_testing_->OnReceiveResponseProcessed(this);
}
if (IsFetchLater() && !disconnected_loader_timer_.IsRunning()) {
disconnected_loader_timer_.Start(
FROM_HERE, GetDisconnectedKeepAliveURLLoaderTimeout(),
base::BindOnce(&KeepAliveURLLoader::OnDisconnectedLoaderTimerFired,
base::Unretained(this)));
return;
}
DeleteSelf();
}
void KeepAliveURLLoader::NotifyOnCompleteForTestAndDevTools(
const network::URLLoaderCompletionStatus& completion_status) {
if (observer_for_testing_) {
CHECK_IS_TEST();
observer_for_testing_->OnComplete(this, completion_status);
}
if (IsFetchLater()) {
if (RenderFrameHostImpl* rfh = GetInitiator(); rfh) {
devtools_instrumentation::OnFetchKeepAliveRequestComplete(
rfh->frame_tree_node(), devtools_request_id_, completion_status);
}
}
}
void KeepAliveURLLoader::OnComplete(
const network::URLLoaderCompletionStatus& completion_status) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
TRACE_EVENT("loading", "KeepAliveURLLoader::OnComplete", "request_id",
request_id_);
if (IsAttemptingRetry(false)) {
retry_state_ = RetryState::kRetryFailed;
}
if (request_tracker_) {
request_tracker_->AdvanceToNextStage(
KeepAliveRequestTracker::RequestStageType::kLoaderCompleted,
completion_status);
}
if (completion_status.error_code != net::OK) {
LogFetchKeepAliveRequestMetric("Failed");
if (RetryOrDelayErrorIfNeeded(
completion_status,
base::BindOnce(&KeepAliveURLLoader::OnCompleteInternal,
base::Unretained(this), completion_status))) {
NotifyOnCompleteForTestAndDevTools(completion_status);
return;
}
}
NotifyOnCompleteForTestAndDevTools(completion_status);
OnCompleteInternal(completion_status);
}
void KeepAliveURLLoader::OnCompleteInternal(
const network::URLLoaderCompletionStatus& completion_status) {
if (completion_status.error_code != net::OK) {
if (attribution_request_helper_) {
attribution_request_helper_->OnError();
attribution_request_helper_.reset();
}
}
stored_url_load_->completion_status = completion_status;
if (IsRendererConnected()) {
if (HasReceivedResponse()) {
return;
}
ForwardURLLoad();
return;
}
if (observer_for_testing_) {
CHECK_IS_TEST();
observer_for_testing_->OnCompleteProcessed(this, completion_status);
}
DeleteSelf();
}
size_t KeepAliveURLLoader::GetMaxAttemptsForRetry() const {
CHECK(resource_request_.fetch_retry_options.has_value());
return std::min(
features::kMaxRetryCount.Get(),
static_cast<size_t>(resource_request_.fetch_retry_options->max_attempts));
}
base::TimeDelta KeepAliveURLLoader::GetMaxAgeForRetry() const {
CHECK(resource_request_.fetch_retry_options.has_value());
if (!resource_request_.fetch_retry_options->max_age.has_value()) {
return features::kMaxRetryAge.Get();
}
return std::min(features::kMaxRetryAge.Get(),
resource_request_.fetch_retry_options->max_age.value());
}
base::TimeDelta KeepAliveURLLoader::GetInitialTimeDeltaForRetry() const {
CHECK(resource_request_.fetch_retry_options.has_value());
if (!resource_request_.fetch_retry_options->initial_delay.has_value()) {
return features::kMinRetryDelta.Get();
}
return std::max(features::kMinRetryDelta.Get(),
resource_request_.fetch_retry_options->initial_delay.value());
}
double KeepAliveURLLoader::GetBackoffFactorForRetry() const {
CHECK(resource_request_.fetch_retry_options.has_value());
if (!resource_request_.fetch_retry_options->backoff_factor.has_value()) {
return features::kMinRetryBackoffFactor.Get();
}
return std::max(
features::kMinRetryBackoffFactor.Get(),
resource_request_.fetch_retry_options->backoff_factor.value());
}
base::TimeDelta KeepAliveURLLoader::UpdateNextRetryDelay() {
if (last_retry_delay_ == base::TimeDelta()) {
last_retry_delay_ = GetInitialTimeDeltaForRetry();
} else {
last_retry_delay_ *= GetBackoffFactorForRetry();
}
return last_retry_delay_;
}
bool KeepAliveURLLoader::IsEligibleForRetry(
std::optional<network::URLLoaderCompletionStatus> completion_status) const {
auto retry_options = resource_request_.fetch_retry_options;
if (!retry_options.has_value()) {
return false;
}
if (!resource_request_.url.SchemeIs(url::kHttpsScheme) ||
retry_state_ == RetryState::kRetryScheduled ||
retry_state_ == RetryState::kWaitingForSameNetworkIsolationKeyDocument ||
retry_count_ >= GetMaxAttemptsForRetry() ||
(first_request_start_time_ != base::TimeTicks() &&
base::TimeTicks::Now() - first_request_start_time_ >
GetMaxAgeForRetry())) {
return false;
}
if (!retry_options->retry_non_idempotent &&
!net::HttpUtil::IsMethodIdempotent(resource_request_.method)) {
return false;
}
if (!retry_options->retry_after_unload && IsContextDetached()) {
return false;
}
CHECK(!check_retry_eligibility_callback_.is_null());
if (!check_retry_eligibility_callback_.Run()) {
return false;
}
if (HasReceivedResponse()) {
return false;
}
if (!completion_status.has_value()) {
return !retry_options->retry_only_if_server_unreached;
}
if (completion_status->resolve_error_info.is_secure_network_error) {
return false;
}
if (retry_options->retry_only_if_server_unreached) {
return !did_encounter_redirect_ &&
IsServerGuaranteedToBeNotReachedYet(completion_status->error_code);
}
return IsNetErrorEligibleForRetry(completion_status->error_code);
}
bool KeepAliveURLLoader::MaybeScheduleRetry(
std::optional<network::URLLoaderCompletionStatus> completion_status) {
if (!IsEligibleForRetry(completion_status)) {
return false;
}
url_loader_.reset();
if (!max_age_handler_timer_.IsRunning()) {
base::TimeDelta current_age =
(base::TimeTicks::Now() - first_request_start_time_);
max_age_handler_timer_.Start(
FROM_HERE, GetMaxAgeForRetry() - current_age,
base::BindOnce(&KeepAliveURLLoader::DeleteSelf,
base::Unretained(this)));
}
retry_count_++;
CHECK_LE(retry_count_, GetMaxAttemptsForRetry());
retry_state_ = RetryState::kRetryScheduled;
retry_timer_.Start(FROM_HERE, UpdateNextRetryDelay(),
base::BindOnce(&KeepAliveURLLoader::AttemptRetryIfAllowed,
base::Unretained(this)));
CHECK(!on_retry_scheduled_callback_.is_null());
on_retry_scheduled_callback_.Run();
return true;
}
void KeepAliveURLLoader::AttemptRetryIfAllowed() {
if (retry_state_ == RetryState::kRetryFailed) {
return;
}
CHECK(retry_state_ == RetryState::kRetryScheduled ||
retry_state_ == RetryState::kWaitingForSameNetworkIsolationKeyDocument);
if (!storage_partition_->GetActiveDocumentCount(network_isolation_key_)) {
retry_state_ = RetryState::kWaitingForSameNetworkIsolationKeyDocument;
return;
}
retry_state_ = RetryState::kRetryInProgress;
request_id_ = GlobalRequestID::MakeBrowserInitiated().request_id;
devtools_request_id_ = base::UnguessableToken::Create().ToString();
resource_request_ = original_resource_request_;
if (features::kAddRetryHeader.Get()) {
resource_request_.headers.SetHeader(kRetryAttemptsHeader,
base::NumberToString(retry_count_));
}
request_tracker_ =
GetContentClient()->browser()->MaybeCreateKeepAliveRequestTracker(
resource_request_, ukm_source_id_,
base::BindRepeating(&KeepAliveURLLoader::IsContextDetached,
base::Unretained(this)));
StartInternal(true);
}
void KeepAliveURLLoader::DidObserveNewlyActiveDocumentWithNIK(
const net::NetworkIsolationKey& nik) {
if (nik == network_isolation_key_ &&
retry_state_ == RetryState::kWaitingForSameNetworkIsolationKeyDocument) {
retry_state_ = RetryState::kRetryScheduled;
GetUIThreadTaskRunner({})->PostTask(
FROM_HERE, base::BindOnce(&KeepAliveURLLoader::AttemptRetryIfAllowed,
base::Unretained(this)));
}
}
bool KeepAliveURLLoader::HasReceivedResponse() const {
return stored_url_load_ && stored_url_load_->response != nullptr;
}
void KeepAliveURLLoader::ForwardURLLoad() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
CHECK(IsRendererConnected());
CHECK(stored_url_load_);
max_age_handler_timer_.Stop();
stored_url_load_->forwarding = true;
if (!stored_url_load_->redirects.empty()) {
forwarding_client_->OnReceiveRedirect(
stored_url_load_->redirects.front()->info,
std::move(stored_url_load_->redirects.front()->head));
stored_url_load_->redirects.pop();
if (observer_for_testing_) {
CHECK_IS_TEST();
observer_for_testing_->OnReceiveRedirectForwarded(this);
}
return;
}
if (stored_url_load_->response) {
forwarding_client_->OnReceiveResponse(
std::move(stored_url_load_->response->head),
std::move(stored_url_load_->response->body),
std::move(stored_url_load_->response->metadata));
stored_url_load_->response = nullptr;
if (observer_for_testing_) {
CHECK_IS_TEST();
observer_for_testing_->OnReceiveResponseForwarded(this);
}
}
if (stored_url_load_->completion_status.has_value()) {
forwarding_client_->OnComplete(*(stored_url_load_->completion_status));
if (observer_for_testing_) {
CHECK_IS_TEST();
observer_for_testing_->OnCompleteForwarded(
this, *(stored_url_load_->completion_status));
}
stored_url_load_ = nullptr;
DeleteSelf();
}
}
bool KeepAliveURLLoader::IsForwardURLLoadStarted() const {
return stored_url_load_ && stored_url_load_->forwarding;
}
bool KeepAliveURLLoader::IsRendererConnected() const {
CHECK(forwarding_client_);
return forwarding_client_->IsConnected();
}
net::Error KeepAliveURLLoader::WillFollowRedirect(
const net::RedirectInfo& redirect_info) const {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (!IsSafeRedirectTarget(last_url_, redirect_info.new_url)) {
return net::ERR_UNSAFE_REDIRECT;
}
if (resource_request_.redirect_mode == network::mojom::RedirectMode::kError) {
return net::ERR_FAILED;
}
if (resource_request_.redirect_mode !=
network::mojom::RedirectMode::kManual) {
if (!IsRedirectAllowedByCSP(
policy_container_host_->policies().content_security_policies,
redirect_info.new_url, initial_url_, last_url_ != initial_url_)) {
return net::ERR_BLOCKED_BY_CSP;
}
if (auto* rfh = GetInitiator();
rfh && MixedContentChecker::ShouldBlockFetchKeepAlive(
rfh, redirect_info.new_url,
true)) {
return net::ERR_FAILED;
}
}
return net::OK;
}
bool KeepAliveURLLoader::RetryOrDelayErrorIfNeeded(
const network::URLLoaderCompletionStatus& status,
base::OnceClosure closure) {
auto retry_options = resource_request_.fetch_retry_options;
if (!retry_options.has_value()) {
return false;
}
if (MaybeScheduleRetry(status)) {
return true;
}
base::TimeDelta current_age =
(base::TimeTicks::Now() - first_request_start_time_);
if (IsRendererConnected() && current_age < GetMaxAgeForRetry()) {
max_age_handler_timer_.Start(FROM_HERE, GetMaxAgeForRetry() - current_age,
std::move(closure));
url_loader_.reset();
return true;
}
return false;
}
void KeepAliveURLLoader::CancelWithStatus(
const network::URLLoaderCompletionStatus& status) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
TRACE_EVENT("loading", "KeepAliveURLLoader::CancelWithStatus", "request_id",
request_id_);
if (IsAttemptingRetry(false)) {
retry_state_ = RetryState::kRetryFailed;
}
if (!stored_url_load_->completion_status.has_value()) {
LogFetchKeepAliveRequestMetric("Failed");
}
if (request_tracker_) {
request_tracker_->AdvanceToNextStage(
KeepAliveRequestTracker::RequestStageType::kRequestFailed, status);
}
if (RetryOrDelayErrorIfNeeded(
status, base::BindOnce(&KeepAliveURLLoader::CancelWithStatusInternal,
base::Unretained(this), status))) {
return;
}
CancelWithStatusInternal(status);
}
void KeepAliveURLLoader::CancelWithStatusInternal(
const network::URLLoaderCompletionStatus& status) {
if (IsRendererConnected()) {
if (!IsForwardURLLoadStarted()) {
ForwardURLLoad();
return;
}
return;
}
DeleteSelf();
}
void KeepAliveURLLoader::ForwardingClient::OnDisconnected() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
TRACE_EVENT("loading", "KeepAliveURLLoader::ForwardingClient::OnDisconnected",
"request_id", request_id());
target_.reset();
if (!keep_alive_url_loader_->IsForwardURLLoadStarted() &&
!keep_alive_url_loader_->HasReceivedResponse()) {
return;
}
keep_alive_url_loader_->DeleteSelf();
}
void KeepAliveURLLoader::OnURLLoaderDisconnected() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
TRACE_EVENT("loading", "KeepAliveURLLoader::OnURLLoaderDisconnected",
"request_id", request_id_);
if (request_tracker_) {
request_tracker_->AdvanceToNextStage(
KeepAliveRequestTracker::RequestStageType::
kLoaderDisconnectedFromRenderer);
}
if (!IsStarted()) {
LogFetchLaterMetric(
FetchLaterBrowserMetricType::kStartedAfterInitiatorDisconnected);
Start();
}
if (!disconnected_loader_timer_.IsRunning()) {
disconnected_loader_timer_.Start(
FROM_HERE, GetDisconnectedKeepAliveURLLoaderTimeout(),
base::BindOnce(&KeepAliveURLLoader::OnDisconnectedLoaderTimerFired,
base::Unretained(this)));
}
}
void KeepAliveURLLoader::OnDisconnectedLoaderTimerFired() {
if (IsAttemptingRetry(false)) {
retry_state_ = RetryState::kRetryFailed;
}
if (resource_request_.fetch_retry_options.has_value() &&
resource_request_.fetch_retry_options->retry_after_unload &&
(IsAttemptingRetry(false) ||
MaybeScheduleRetry(std::nullopt))) {
return;
}
if (request_tracker_) {
request_tracker_->AdvanceToNextStage(
KeepAliveRequestTracker::RequestStageType::
kRequestCancelledAfterTimeLimit);
}
if (IsFetchLater()) {
LogFetchLaterMetric(FetchLaterBrowserMetricType::kCancelledAfterTimeLimit);
}
DeleteSelf();
}
void KeepAliveURLLoader::Shutdown() {
base::UmaHistogramBoolean(
"FetchKeepAlive.Requests2.Shutdown.IsStarted.Browser", IsStarted());
if (request_tracker_) {
request_tracker_->AdvanceToNextStage(
KeepAliveRequestTracker::RequestStageType::kBrowserShutdown);
}
if (!IsStarted()) {
CHECK(IsFetchLater());
LogFetchLaterMetric(FetchLaterBrowserMetricType::kStartedWhenShutdown);
Start();
}
}
bool KeepAliveURLLoader::IsFetchLater() const {
return base::FeatureList::IsEnabled(blink::features::kFetchLaterAPI) &&
resource_request_.is_fetch_later_api;
}
void KeepAliveURLLoader::SendNow() {
if (!IsFetchLater()) {
mojo::ReportBadMessage("Unexpected call to KeepAliveURLLoader::SendNow()");
return;
}
LogFetchLaterMetric(FetchLaterBrowserMetricType::kStartedByInitiator);
if (!IsStarted()) {
Start();
}
}
void KeepAliveURLLoader::Cancel() {
if (!IsFetchLater()) {
mojo::ReportBadMessage("Unexpected call to KeepAliveURLLoader::Cancel()");
return;
}
if (request_tracker_) {
request_tracker_->AdvanceToNextStage(
KeepAliveRequestTracker::RequestStageType::kRequestCancelledByRenderer);
}
LogFetchLaterMetric(FetchLaterBrowserMetricType::kAbortedByInitiator);
DeleteSelf();
}
void KeepAliveURLLoader::DeleteSelf() {
CHECK(on_delete_callback_);
std::move(on_delete_callback_).Run();
}
void KeepAliveURLLoader::SetObserverForTesting(
scoped_refptr<TestObserver> observer) {
observer_for_testing_ = observer;
}
void KeepAliveURLLoader::LogFetchKeepAliveRequestMetric(
std::string_view request_state_name) {
if (IsFetchLater()) {
return;
}
auto resource_type =
static_cast<blink::mojom::ResourceType>(resource_request_.resource_type);
FetchKeepAliveRequestMetricType sample_type;
switch (resource_type) {
case blink::mojom::ResourceType::kXhr:
sample_type = FetchKeepAliveRequestMetricType::kFetch;
break;
case blink::mojom::ResourceType::kPing:
sample_type = FetchKeepAliveRequestMetricType::kPing;
break;
case blink::mojom::ResourceType::kCspReport:
sample_type = FetchKeepAliveRequestMetricType::kReporting;
break;
case blink::mojom::ResourceType::kImage:
sample_type = FetchKeepAliveRequestMetricType::kBackgroundFetchIcon;
break;
case blink::mojom::ResourceType::kMainFrame:
case blink::mojom::ResourceType::kSubFrame:
case blink::mojom::ResourceType::kStylesheet:
case blink::mojom::ResourceType::kScript:
case blink::mojom::ResourceType::kFontResource:
case blink::mojom::ResourceType::kSubResource:
case blink::mojom::ResourceType::kObject:
case blink::mojom::ResourceType::kMedia:
case blink::mojom::ResourceType::kWorker:
case blink::mojom::ResourceType::kSharedWorker:
case blink::mojom::ResourceType::kPrefetch:
case blink::mojom::ResourceType::kFavicon:
case blink::mojom::ResourceType::kServiceWorker:
case blink::mojom::ResourceType::kPluginResource:
case blink::mojom::ResourceType::kNavigationPreloadMainFrame:
case blink::mojom::ResourceType::kNavigationPreloadSubFrame:
case blink::mojom::ResourceType::kJson:
NOTREACHED();
}
CHECK(request_state_name == "Total" || request_state_name == "Started" ||
request_state_name == "Retried" || request_state_name == "Succeeded" ||
request_state_name == "Failed");
const std::string histogram_name = base::StrCat(
{"FetchKeepAlive.Requests2.", request_state_name, ".Browser"});
if (base::features::IsReducePPMsEnabled()) {
static base::NoDestructor<
absl::flat_hash_map<std::string, base::HistogramBase*>>
histograms;
static WrappedThreadChecker* thread_checker = new WrappedThreadChecker;
thread_checker->Check();
auto it = histograms->find(histogram_name);
if (it != histograms->end()) {
it->second->Add(static_cast<int32_t>(sample_type));
} else {
int32_t max_value =
static_cast<int32_t>(FetchKeepAliveRequestMetricType::kMaxValue);
base::HistogramBase* histo = base::LinearHistogram::FactoryGet(
histogram_name, 1,
max_value + 1,
max_value + 2,
base::HistogramBase::kUmaTargetedHistogramFlag);
histo->Add(static_cast<int32_t>(sample_type));
(*histograms)[histogram_name] = histo;
}
} else {
base::UmaHistogramEnumeration(histogram_name, sample_type);
}
if (bool is_context_detached = !GetInitiator();
request_state_name == "Started" || request_state_name == "Succeeded") {
base::UmaHistogramBoolean(
base::StrCat({"FetchKeepAlive.Requests2.", request_state_name,
".IsContextDetached.Browser"}),
is_context_detached);
}
}
}