#include "content/browser/preloading/prefetch/prefetch_response_reader.h"
#include "base/debug/alias.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/strings/string_util.h"
#include "base/task/sequenced_task_runner.h"
#include "content/browser/preloading/prefetch/prefetch_data_pipe_tee.h"
#include "content/browser/preloading/prefetch/prefetch_params.h"
#include "content/browser/preloading/prefetch/prefetch_streaming_url_loader.h"
#include "content/browser/service_worker/service_worker_main_resource_handle.h"
#include "net/http/http_cookie_indices.h"
#include "net/http/http_response_headers.h"
#include "services/metrics/public/cpp/metrics_utils.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "services/network/public/mojom/early_hints.mojom.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
namespace content {
namespace {
PrefetchStreamingURLLoaderStatus
GetStatusForRecordingFromErrorOnResponseReceived(
PrefetchErrorOnResponseReceived status) {
switch (status) {
case PrefetchErrorOnResponseReceived::kPrefetchWasDecoy:
return PrefetchStreamingURLLoaderStatus::kPrefetchWasDecoy;
case PrefetchErrorOnResponseReceived::kFailedInvalidHead:
return PrefetchStreamingURLLoaderStatus::kFailedInvalidHead;
case PrefetchErrorOnResponseReceived::kFailedInvalidHeaders:
return PrefetchStreamingURLLoaderStatus::kFailedInvalidHeaders;
case PrefetchErrorOnResponseReceived::kFailedNon2XX:
return PrefetchStreamingURLLoaderStatus::kFailedNon2XX;
case PrefetchErrorOnResponseReceived::kFailedMIMENotSupported:
return PrefetchStreamingURLLoaderStatus::kFailedMIMENotSupported;
}
}
}
bool PrefetchResponseReader::Servable(
base::TimeDelta cacheable_duration) const {
switch (load_state()) {
case LoadState::kResponseReceived:
CHECK(!response_complete_time_);
return true;
case LoadState::kCompleted:
CHECK(response_complete_time_);
return base::TimeTicks::Now() <
response_complete_time_.value() + cacheable_duration;
case LoadState::kStarted:
case LoadState::kRedirectHandled:
case LoadState::kFailedResponseReceived:
case LoadState::kFailedRedirect:
CHECK(!response_complete_time_)
<< "LoadState: " << static_cast<int>(load_state());
return false;
case LoadState::kFailed:
CHECK(response_complete_time_);
return false;
}
}
bool PrefetchResponseReader::IsWaitingForResponse() const {
switch (load_state()) {
case LoadState::kStarted:
return true;
case LoadState::kResponseReceived:
case LoadState::kRedirectHandled:
case LoadState::kCompleted:
case LoadState::kFailedResponseReceived:
case LoadState::kFailed:
case LoadState::kFailedRedirect:
return false;
}
}
bool PrefetchResponseReader::VariesOnCookieIndices() const {
return cookie_indices_.has_value();
}
bool PrefetchResponseReader::MatchesCookieIndices(
base::span<const std::pair<std::string, std::string>> cookies) const {
CHECK(cookie_indices_.has_value());
net::CookieIndicesHash hash =
net::HashCookieIndices(cookie_indices_->cookie_names, cookies);
return hash == cookie_indices_->expected_hash;
}
PrefetchResponseReader::PrefetchResponseReader(
OnPrefetchDeterminedHeadCallback on_determined_head_callback,
OnPrefetchResponseCompletedCallback on_prefetch_response_completed_callback)
: on_determined_head_callback_(std::move(on_determined_head_callback)),
on_prefetch_response_completed_callback_(
std::move(on_prefetch_response_completed_callback)) {
serving_url_loader_receivers_.set_disconnect_handler(base::BindRepeating(
&PrefetchResponseReader::OnServingURLLoaderMojoDisconnect,
weak_ptr_factory_.GetWeakPtr()));
}
PrefetchResponseReader::~PrefetchResponseReader() {
if (should_record_metrics_) {
base::UmaHistogramEnumeration(
"PrefetchProxy.Prefetch.StreamingURLLoaderFinalStatus",
GetStatusForRecording());
}
}
void PrefetchResponseReader::SetStreamingURLLoader(
base::WeakPtr<PrefetchStreamingURLLoader> streaming_url_loader) {
CHECK(!streaming_url_loader_);
streaming_url_loader_ = std::move(streaming_url_loader);
}
base::WeakPtr<PrefetchStreamingURLLoader>
PrefetchResponseReader::GetStreamingLoader() const {
return streaming_url_loader_;
}
void PrefetchResponseReader::MaybeReleaseSoonSelfPointer() {
if (!self_pointer_) {
return;
}
if (!serving_url_loader_receivers_.empty()) {
return;
}
base::SequencedTaskRunner::GetCurrentDefault()->ReleaseSoon(
FROM_HERE, std::move(self_pointer_));
}
void PrefetchResponseReader::OnServingURLLoaderMojoDisconnect() {
MaybeReleaseSoonSelfPointer();
}
std::pair<PrefetchRequestHandler, base::WeakPtr<ServiceWorkerClient>>
PrefetchResponseReader::CreateRequestHandler() {
mojo::ScopedDataPipeConsumerHandle body;
switch (load_state()) {
case LoadState::kResponseReceived:
case LoadState::kCompleted:
case LoadState::kFailed:
if (body_tee_) {
body = body_tee_->Clone();
}
if (!body) {
base::UmaHistogramBoolean(
"Preloading.Prefetch."
"PrefetchResponseReaderCreateRequestHandlerInvalidBody",
true);
return {};
}
break;
case LoadState::kRedirectHandled:
CHECK(!body_tee_);
break;
case LoadState::kStarted:
case LoadState::kFailedResponseReceived:
case LoadState::kFailedRedirect:
return {};
}
if (streaming_url_loader_) {
streaming_url_loader_->OnStartServing();
}
return std::make_pair(
base::BindOnce(&PrefetchResponseReader::BindAndStart,
base::WrapRefCounted(this), std::move(body)),
service_worker_handle_ ? service_worker_handle_->service_worker_client()
: nullptr);
}
void PrefetchResponseReader::BindAndStart(
mojo::ScopedDataPipeConsumerHandle body,
const network::ResourceRequest& resource_request,
mojo::PendingReceiver<network::mojom::URLLoader> receiver,
mojo::PendingRemote<network::mojom::URLLoaderClient> client) {
serving_url_loader_receivers_.Add(this, std::move(receiver));
ServingUrlLoaderClientId client_id =
serving_url_loader_clients_.Add(std::move(client));
if (!self_pointer_) {
self_pointer_ = base::WrapRefCounted(this);
}
if (load_state() == LoadState::kCompleted) {
served_after_completion_ = true;
} else {
served_before_completion_ = true;
}
forward_body_ = std::move(body);
switch (load_state()) {
case LoadState::kResponseReceived:
CHECK(GetHead());
CHECK(forward_body_);
break;
case LoadState::kCompleted:
CHECK(GetHead());
CHECK(forward_body_);
break;
case LoadState::kFailed:
CHECK(GetHead());
CHECK(forward_body_);
break;
case LoadState::kRedirectHandled:
CHECK(!GetHead());
CHECK(!forward_body_);
break;
case LoadState::kStarted:
case LoadState::kFailedResponseReceived:
case LoadState::kFailedRedirect:
NOTREACHED();
}
RunEventQueue(client_id);
forward_body_.reset();
}
void PrefetchResponseReader::AddEventToQueue(EventCallback callback) {
CHECK_EQ(event_queue_status_, EventQueueStatus::kNotRunning);
event_queue_status_ = EventQueueStatus::kRunning;
std::vector<ServingUrlLoaderClientId> client_ids;
for (auto it = serving_url_loader_clients_.begin();
it != serving_url_loader_clients_.end(); ++it) {
client_ids.push_back(it.id());
}
CHECK_EQ(serving_url_loader_clients_.size(), client_ids.size());
for (auto client_id : client_ids) {
callback.Run(client_id);
}
CHECK_EQ(serving_url_loader_clients_.size(), client_ids.size());
event_queue_.push_back(std::move(callback));
event_queue_status_ = EventQueueStatus::kNotRunning;
}
void PrefetchResponseReader::RunEventQueue(ServingUrlLoaderClientId client_id) {
CHECK_GT(event_queue_.size(), 0u);
CHECK_EQ(event_queue_status_, EventQueueStatus::kNotRunning);
event_queue_status_ = EventQueueStatus::kRunning;
for (const auto& callback : event_queue_) {
callback.Run(client_id);
}
event_queue_status_ = EventQueueStatus::kNotRunning;
}
void PrefetchResponseReader::OnComplete(
network::URLLoaderCompletionStatus completion_status) {
auto old_load_state = load_state();
base::debug::Alias(&old_load_state);
auto new_load_state = [&]() {
switch (load_state()) {
case LoadState::kStarted:
CHECK_NE(completion_status.error_code, net::OK);
return LoadState::kFailed;
case LoadState::kResponseReceived:
if (completion_status.error_code == net::OK) {
return LoadState::kCompleted;
} else {
return LoadState::kFailed;
}
case LoadState::kFailedResponseReceived:
return LoadState::kFailed;
case LoadState::kRedirectHandled:
NOTREACHED();
case LoadState::kCompleted:
NOTREACHED();
case LoadState::kFailed:
NOTREACHED();
case LoadState::kFailedRedirect:
NOTREACHED();
}
}();
CHECK(!response_complete_time_);
CHECK(!completion_status_);
response_complete_time_ = base::TimeTicks::Now();
completion_status_ = completion_status;
SetLoadStateAndAddEventToQueue(
new_load_state,
base::BindRepeating(&PrefetchResponseReader::ForwardCompletionStatus,
base::Unretained(this)));
}
void PrefetchResponseReader::RecordOnPrefetchContainerDestroyed(
base::PassKey<PrefetchContainer>,
ukm::builders::PrefetchProxy_PrefetchedResource& builder) const {
CHECK(head_);
switch (load_state()) {
case LoadState::kResponseReceived:
case LoadState::kFailedResponseReceived:
case LoadState::kCompleted:
case LoadState::kFailed:
break;
case LoadState::kStarted:
case LoadState::kRedirectHandled:
case LoadState::kFailedRedirect:
NOTREACHED();
}
if (completion_status_) {
builder.SetDataLength(ukm::GetExponentialBucketMinForBytes(
completion_status_->encoded_data_length));
base::TimeDelta fetch_duration =
completion_status_->completion_time - head_->load_timing.request_start;
builder.SetFetchDurationMS(fetch_duration.InMilliseconds());
}
}
void PrefetchResponseReader::OnReceiveEarlyHints(
network::mojom::EarlyHintsPtr early_hints) {
CHECK(load_state() == LoadState::kStarted ||
load_state() == LoadState::kResponseReceived ||
load_state() == LoadState::kFailedResponseReceived);
AddEventToQueue(
base::BindRepeating(&PrefetchResponseReader::ForwardEarlyHints,
base::Unretained(this), std::move(early_hints)));
}
void PrefetchResponseReader::OnTransferSizeUpdated(int32_t transfer_size_diff) {
CHECK(load_state() == LoadState::kStarted ||
load_state() == LoadState::kResponseReceived ||
load_state() == LoadState::kFailedResponseReceived);
AddEventToQueue(
base::BindRepeating(&PrefetchResponseReader::ForwardTransferSizeUpdate,
base::Unretained(this), transfer_size_diff));
}
void PrefetchResponseReader::HandleRedirect(
PrefetchRedirectStatus redirect_status,
const net::RedirectInfo& redirect_info,
network::mojom::URLResponseHeadPtr redirect_head) {
CHECK_EQ(load_state(), LoadState::kStarted);
switch (redirect_status) {
case PrefetchRedirectStatus::kFollow:
should_record_metrics_ = false;
break;
case PrefetchRedirectStatus::kSwitchNetworkContext:
break;
case PrefetchRedirectStatus::kFail:
SetLoadStateAndAddEventToQueue(LoadState::kFailedRedirect, {});
return;
}
StoreInfoFromResponseHead(*redirect_head);
redirect_head->request_cookies.clear();
SetLoadStateAndAddEventToQueue(
LoadState::kRedirectHandled,
base::BindRepeating(&PrefetchResponseReader::ForwardRedirect,
base::Unretained(this), redirect_info,
std::move(redirect_head)));
}
void PrefetchResponseReader::OnReceiveResponse(
std::optional<PrefetchErrorOnResponseReceived> error,
network::mojom::URLResponseHeadPtr head,
mojo::ScopedDataPipeConsumerHandle body,
std::unique_ptr<ServiceWorkerMainResourceHandle> service_worker_handle) {
CHECK_EQ(load_state(), LoadState::kStarted);
CHECK(!head_);
CHECK(head);
CHECK(!body_tee_);
CHECK(!service_worker_handle_);
CHECK(serving_url_loader_clients_.empty());
const auto new_load_state =
error ? LoadState::kFailedResponseReceived : LoadState::kResponseReceived;
if (!error) {
head->navigation_delivery_type =
network::mojom::NavigationDeliveryType::kNavigationalPrefetch;
CHECK(body);
} else {
failure_reason_ = std::move(error);
body.reset();
}
service_worker_handle_ = std::move(service_worker_handle);
StoreInfoFromResponseHead(*head);
head->request_cookies.clear();
head_ = std::move(head);
body_tee_ = base::MakeRefCounted<PrefetchDataPipeTee>(
std::move(body), GetPrefetchDataPipeTeeBodySizeLimit());
SetLoadStateAndAddEventToQueue(
new_load_state,
base::BindRepeating(&PrefetchResponseReader::ForwardResponse,
base::Unretained(this)));
}
void PrefetchResponseReader::ForwardCompletionStatus(
ServingUrlLoaderClientId client_id) {
CHECK(completion_status_);
if (network::mojom::URLLoaderClient* client =
serving_url_loader_clients_.Get(client_id)) {
client->OnComplete(completion_status_.value());
}
}
void PrefetchResponseReader::ForwardEarlyHints(
const network::mojom::EarlyHintsPtr& early_hints,
ServingUrlLoaderClientId client_id) {
if (network::mojom::URLLoaderClient* client =
serving_url_loader_clients_.Get(client_id)) {
client->OnReceiveEarlyHints(early_hints->Clone());
}
}
void PrefetchResponseReader::ForwardTransferSizeUpdate(
int32_t transfer_size_diff,
ServingUrlLoaderClientId client_id) {
if (network::mojom::URLLoaderClient* client =
serving_url_loader_clients_.Get(client_id)) {
client->OnTransferSizeUpdated(transfer_size_diff);
}
}
void PrefetchResponseReader::ForwardRedirect(
const net::RedirectInfo& redirect_info,
const network::mojom::URLResponseHeadPtr& head,
ServingUrlLoaderClientId client_id) {
if (network::mojom::URLLoaderClient* client =
serving_url_loader_clients_.Get(client_id)) {
client->OnReceiveRedirect(redirect_info, head->Clone());
}
}
void PrefetchResponseReader::ForwardResponse(
ServingUrlLoaderClientId client_id) {
CHECK(head_);
CHECK(forward_body_);
if (network::mojom::URLLoaderClient* client =
serving_url_loader_clients_.Get(client_id)) {
client->OnReceiveResponse(head_->Clone(), std::move(forward_body_),
std::nullopt);
}
}
void PrefetchResponseReader::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) {
NOTREACHED();
}
void PrefetchResponseReader::SetPriority(net::RequestPriority priority,
int32_t intra_priority_value) {
if (streaming_url_loader_) {
streaming_url_loader_->SetPriority(priority, intra_priority_value);
}
}
PrefetchStreamingURLLoaderStatus PrefetchResponseReader::GetStatusForRecording()
const {
switch (load_state()) {
case LoadState::kStarted:
return PrefetchStreamingURLLoaderStatus::kWaitingOnHead;
case LoadState::kRedirectHandled:
if (served_before_completion_) {
return PrefetchStreamingURLLoaderStatus::
kServedSwitchInNetworkContextForRedirect;
} else {
return PrefetchStreamingURLLoaderStatus::
kStopSwitchInNetworkContextForRedirect;
}
case LoadState::kResponseReceived:
return PrefetchStreamingURLLoaderStatus::kHeadReceivedWaitingOnBody;
case LoadState::kCompleted:
if (served_before_completion_) {
return PrefetchStreamingURLLoaderStatus::
kSuccessfulServedBeforeCompletion;
} else if (served_after_completion_) {
return PrefetchStreamingURLLoaderStatus::
kSuccessfulServedAfterCompletion;
} else {
return PrefetchStreamingURLLoaderStatus::kSuccessfulNotServed;
}
case LoadState::kFailedRedirect:
return PrefetchStreamingURLLoaderStatus::kFailedInvalidRedirect;
case LoadState::kFailedResponseReceived:
case LoadState::kFailed:
if (failure_reason_) {
return GetStatusForRecordingFromErrorOnResponseReceived(
*failure_reason_);
} else if (served_before_completion_) {
return PrefetchStreamingURLLoaderStatus::kFailedNetErrorButServed;
} else {
return PrefetchStreamingURLLoaderStatus::kFailedNetError;
}
}
}
void PrefetchResponseReader::StoreInfoFromResponseHead(
const network::mojom::URLResponseHead& head) {
if (!head.headers || !head.parsed_headers) {
return;
}
CHECK(!cookie_indices_)
<< "This shouldn't happen more than once per PrefetchResponseReader.";
size_t iter = 0;
std::string request_header;
bool vary_on_cookie = false;
while (head.headers->EnumerateHeader(&iter, "vary", &request_header)) {
if (request_header == "*" ||
base::EqualsCaseInsensitiveASCII(request_header, "cookie")) {
vary_on_cookie = true;
break;
}
}
if (vary_on_cookie && head.parsed_headers->cookie_indices.has_value()) {
auto& indices = cookie_indices_.emplace();
indices.cookie_names = *head.parsed_headers->cookie_indices;
std::ranges::sort(indices.cookie_names);
auto repeated = std::ranges::unique(indices.cookie_names);
indices.cookie_names.erase(repeated.begin(), repeated.end());
indices.cookie_names.shrink_to_fit();
indices.expected_hash =
net::HashCookieIndices(indices.cookie_names, head.request_cookies);
}
}
void PrefetchResponseReader::SetLoadStateAndAddEventToQueue(
LoadState new_load_state,
EventCallback callback) {
auto old_load_state = load_state();
switch (old_load_state) {
case LoadState::kStarted:
CHECK_NE(new_load_state, LoadState::kCompleted);
break;
case LoadState::kResponseReceived:
CHECK(new_load_state == LoadState::kCompleted ||
new_load_state == LoadState::kFailed);
break;
case LoadState::kFailedResponseReceived:
CHECK_EQ(new_load_state, LoadState::kFailed);
break;
case LoadState::kRedirectHandled:
NOTREACHED();
case LoadState::kCompleted:
NOTREACHED();
case LoadState::kFailed:
NOTREACHED();
case LoadState::kFailedRedirect:
NOTREACHED();
}
load_state_ = new_load_state;
if (callback) {
AddEventToQueue(std::move(callback));
}
switch (load_state()) {
case LoadState::kStarted:
NOTREACHED();
case LoadState::kRedirectHandled:
break;
case LoadState::kResponseReceived:
CHECK(on_determined_head_callback_);
std::move(on_determined_head_callback_)
.Run(true);
break;
case LoadState::kFailedResponseReceived:
case LoadState::kFailedRedirect:
CHECK(on_determined_head_callback_);
std::move(on_determined_head_callback_)
.Run(false);
break;
case LoadState::kFailed:
if (old_load_state == LoadState::kStarted) {
CHECK(on_determined_head_callback_);
std::move(on_determined_head_callback_)
.Run(false);
} else {
CHECK(!on_determined_head_callback_);
}
[[fallthrough]];
case LoadState::kCompleted:
CHECK(!on_determined_head_callback_);
CHECK(on_prefetch_response_completed_callback_);
CHECK(completion_status_);
std::move(on_prefetch_response_completed_callback_)
.Run(load_state() == LoadState::kCompleted,
*completion_status_);
break;
}
}
PrefetchResponseReader::CookieIndicesInfo::CookieIndicesInfo() = default;
PrefetchResponseReader::CookieIndicesInfo::~CookieIndicesInfo() = default;
}