blob: 1297c593db964ba7f1c43fc44029a06162b2cadb [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/reduce_accept_language/reduce_accept_language_throttle.h"
#include <algorithm>
#include "base/metrics/histogram_functions.h"
#include "content/browser/renderer_host/frame_tree_node.h"
#include "content/browser/renderer_host/navigation_request.h"
#include "content/public/browser/origin_trials_controller_delegate.h"
#include "content/public/browser/reduce_accept_language_controller_delegate.h"
#include "content/public/browser/reduce_accept_language_utils.h"
#include "services/network/public/cpp/features.h"
#include "third_party/blink/public/common/loader/url_loader_throttle.h"
namespace content {
namespace {
// Metrics on the count of requests restarted or the reason why not restarted
// when reducing accept-language HTTP header. These values are persisted to
// logs. Entries should not be renumbered and numeric values should never be
// reused.
enum class AcceptLanguageNegotiationRestart {
kNavigationStarted = 0,
kAvailLanguageAndContentLanguageHeaderPresent = 1,
kServiceWorkerPreloadRequest = 2,
kNavigationRestarted = 3,
kMaxValue = kNavigationRestarted,
};
// Logging the metric related to reduce accept language header and
// corresponding restart metric when language negotiation happens.
void LogAcceptLanguageStatus(AcceptLanguageNegotiationRestart status) {
base::UmaHistogramEnumeration(
"ReduceAcceptLanguage.AcceptLanguageNegotiationRestart", status);
}
} // namespace
ReduceAcceptLanguageThrottle::ReduceAcceptLanguageThrottle(
ReduceAcceptLanguageUtils reduce_accept_language_utils,
OriginTrialsControllerDelegate* origin_trials_delegate,
FrameTreeNodeId frame_tree_node_id)
: reduce_accept_language_utils_(std::move(reduce_accept_language_utils)),
origin_trials_delegate_(origin_trials_delegate),
frame_tree_node_id_(frame_tree_node_id) {
LogAcceptLanguageStatus(AcceptLanguageNegotiationRestart::kNavigationStarted);
}
ReduceAcceptLanguageThrottle::~ReduceAcceptLanguageThrottle() = default;
void ReduceAcceptLanguageThrottle::WillStartRequest(
network::ResourceRequest* request,
bool* defer) {
last_request_url_ = request->url;
initial_request_headers_ = request->headers;
}
void ReduceAcceptLanguageThrottle::BeforeWillRedirectRequest(
net::RedirectInfo* redirect_info,
const network::mojom::URLResponseHead& response_head,
RestartWithURLReset* restart_with_url_reset,
std::vector<std::string>* to_be_removed_request_headers,
net::HttpRequestHeaders* modified_request_headers,
net::HttpRequestHeaders* modified_cors_exempt_request_headers) {
// For redirect case, checking if a redirect response should result in a
// restart of the last requested URL with a better negotiation language,
// rather than following the redirect.
//
// Suppose origin A returns a response redirecting to origin B,
// `last_request_url_` will be A, and `response_head` contains
// Content-Language and Avail-Language headers suggesting whether we should
// follow the redirect response. If the response shows it supports one of
// user's other preferred language and needs to restart. We will restart the
// request to A with the new preferred language and expect A responses a
// different redirect. For detail example, see
// https://github.com/Tanych/accept-language/issues/3.
MaybeRestartWithLanguageNegotiation(response_head, restart_with_url_reset);
// Update the url with the redirect new url to make sure last_request_url_
// with be the response_url.
last_request_url_ = redirect_info->new_url;
}
void ReduceAcceptLanguageThrottle::BeforeWillProcessResponse(
const GURL& response_url,
const network::mojom::URLResponseHead& response_head,
RestartWithURLReset* restart_with_url_reset) {
DCHECK_EQ(response_url, last_request_url_);
MaybeRestartWithLanguageNegotiation(response_head, restart_with_url_reset);
}
void ReduceAcceptLanguageThrottle::MaybeRestartWithLanguageNegotiation(
const network::mojom::URLResponseHead& response_head,
RestartWithURLReset* restart_with_url_reset) {
// For responses that don't contains content-language and avail-language
// header, we skip language negotiation for them since we don't know whether
// we can get a better representation.
if (!response_head.parsed_headers ||
!response_head.parsed_headers->content_language ||
!response_head.parsed_headers->avail_language) {
return;
}
LogAcceptLanguageStatus(AcceptLanguageNegotiationRestart::
kAvailLanguageAndContentLanguageHeaderPresent);
// Skip restart when it's a service worker navigation preload request,
// otherwise request for the same origin can't guarantee restart at most once.
// All URLLoaderThrottles instances get recreated and `restarted_origins_` get
// reset when requests are the service worker preload requests. This could
// lead to browsers resending too many requests if
// `ParseAndPersistAcceptLanguageForNavigation` returns need to restart.
if (response_head.did_service_worker_navigation_preload) {
LogAcceptLanguageStatus(
AcceptLanguageNegotiationRestart::kServiceWorkerPreloadRequest);
return;
}
url::Origin last_request_origin = url::Origin::Create(last_request_url_);
if (!ReduceAcceptLanguageUtils::OriginCanReduceAcceptLanguage(
last_request_origin)) {
return;
}
FrameTreeNode* frame_tree_node =
FrameTreeNode::GloballyFindByID(frame_tree_node_id_);
// Skip if origin opted-in ReduceAcceptLanguage deprecation origin trial.
if (ReduceAcceptLanguageUtils::CheckDisableReduceAcceptLanguageOriginTrial(
last_request_url_, frame_tree_node, origin_trials_delegate_)) {
return;
}
// Only restart once per-Origin (per navigation).
if (!restarted_origins_.insert(last_request_origin).second)
return;
bool need_restart =
reduce_accept_language_utils_.ReadAndPersistAcceptLanguageForNavigation(
last_request_origin, initial_request_headers_,
response_head.parsed_headers);
// Only restart if the initial accept language doesn't match content language.
if (need_restart) {
LogAcceptLanguageStatus(
AcceptLanguageNegotiationRestart::kNavigationRestarted);
// RestartWithURLResetAndFlags will restart from the original requested URL.
// Ideally, we expect only restart last requested URL to avoid unnecessary
// restarts starting from the original request URL. However, for cross
// origin redirects, it won't pass the SiteForCookies equivalent check on
// URLLoader when using RestartWithFlags.
*restart_with_url_reset = RestartWithURLReset(true);
return;
}
}
} // namespace content