blob: 40da4ee72080452223b6a8646a1a5262523fefcd [file] [log] [blame]
// Copyright 2017 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/navigation_url_loader_impl.h"
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/strings/stringprintf.h"
#include "base/test/scoped_feature_list.h"
#include "base/unguessable_token.h"
#include "build/build_config.h"
#include "components/ukm/test_ukm_recorder.h"
#include "content/browser/loader/navigation_loader_interceptor.h"
#include "content/browser/loader/navigation_url_loader.h"
#include "content/browser/renderer_host/frame_tree_node.h"
#include "content/browser/renderer_host/navigation_request_info.h"
#include "content/browser/web_package/prefetched_signed_exchange_cache.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/navigation_ui_data.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/common/buildflags.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/mock_client_hints_controller_delegate.h"
#include "content/public/test/navigation_simulator.h"
#include "content/public/test/test_browser_context.h"
#include "content/public/test/test_renderer_host.h"
#include "content/test/test_navigation_url_loader_delegate.h"
#include "content/test/test_web_contents.h"
#include "net/base/load_flags.h"
#include "net/base/mock_network_change_notifier.h"
#include "net/proxy_resolution/configured_proxy_resolution_service.h"
#include "net/storage_access_api/status.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
#include "net/url_request/url_request_context.h"
#include "net/url_request/url_request_context_builder.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "services/network/cookie_settings.h"
#include "services/network/public/cpp/cors/origin_access_list.h"
#include "services/network/public/cpp/features.h"
#include "services/network/public/cpp/permissions_policy/permissions_policy.h"
#include "services/network/public/cpp/single_request_url_loader_factory.h"
#include "services/network/public/cpp/url_loader_completion_status.h"
#include "services/network/public/mojom/fetch_api.mojom.h"
#include "services/network/resource_scheduler/resource_scheduler_client.h"
#include "services/network/shared_resource_checker.h"
#include "services/network/test/url_loader_context_for_tests.h"
#include "services/network/url_loader.h"
#include "services/network/url_request_context_owner.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/navigation/navigation_params.h"
#include "third_party/blink/public/mojom/loader/mixed_content.mojom.h"
#include "third_party/blink/public/mojom/navigation/navigation_params.mojom.h"
#if BUILDFLAG(ENABLE_PLUGINS)
#include "content/public/browser/plugin_service.h"
#endif
namespace content {
using testing::Optional;
class NavigationURLLoaderImplTest : public testing::Test {
public:
NavigationURLLoaderImplTest()
: task_environment_(std::make_unique<BrowserTaskEnvironment>(
base::test::TaskEnvironment::MainThreadType::IO,
content::BrowserTaskEnvironment::TimeSource::MOCK_TIME)),
network_change_notifier_(
net::test::MockNetworkChangeNotifier::Create()) {
browser_context_ = std::make_unique<TestBrowserContext>();
http_test_server_.AddDefaultHandlers(
base::FilePath(FILE_PATH_LITERAL("content/test/data")));
#if BUILDFLAG(ENABLE_PLUGINS)
PluginService::GetInstance()->Init();
#endif
}
~NavigationURLLoaderImplTest() override {
browser_context_.reset();
// Reset the BrowserTaskEnvironment to force destruction of the local
// NetworkService, which is held in SequenceLocalStorage. This must happen
// before destruction of |network_change_notifier_|, to allow observers to
// be unregistered.
task_environment_.reset();
}
void SetUp() override {
// Do not create TestNavigationURLLoaderFactory as this tests creates
// NavigationURLLoaders explicitly and TestNavigationURLLoaderFactory
// interferes with that.
rvh_test_enabler_ = std::make_unique<RenderViewHostTestEnabler>(
RenderViewHostTestEnabler::NavigationURLLoaderFactoryType::kNone);
web_contents_ = TestWebContents::Create(
browser_context_.get(),
SiteInstanceImpl::Create(browser_context_.get()));
}
void TearDown() override {
pending_navigation_.reset();
web_contents_.reset();
if (client_hints_controller_delegate_.get()) {
browser_context_->SetClientHintsControllerDelegate(nullptr);
client_hints_controller_delegate_.reset();
}
rvh_test_enabler_.reset();
}
std::unique_ptr<NavigationURLLoaderImpl> CreateTestLoader(
const GURL& url,
const std::string& headers,
const std::string& method,
NavigationURLLoaderDelegate* delegate,
blink::NavigationDownloadPolicy download_policy =
blink::NavigationDownloadPolicy(),
bool is_main_frame = true,
bool upgrade_if_insecure = false,
bool is_ad_tagged = false,
std::vector<std::unique_ptr<NavigationLoaderInterceptor>> interceptors =
{}) {
// NavigationURLLoader assumes that the corresponding FrameTreeNode has an
// associated NavigationRequest.
// NOTE: This also creates and starts another `NavigationURLLoaderImpl`
// (`NavigationRequest::loader_`) but it's not the `NavigationURLLoaderImpl`
// being tested (=the return value of this method).
pending_navigation_ = NavigationSimulator::CreateBrowserInitiated(
GURL("https://example.com"), web_contents_.get());
pending_navigation_->Start();
blink::mojom::BeginNavigationParamsPtr begin_params =
blink::mojom::BeginNavigationParams::New(
std::nullopt /* initiator_frame_token */, headers, net::LOAD_NORMAL,
false /* skip_service_worker */,
blink::mojom::RequestContextType::LOCATION,
blink::mojom::MixedContentContextType::kBlockable,
false /* is_form_submission */,
false /* was_initiated_by_link_click */,
blink::mojom::ForceHistoryPush::kNo,
GURL() /* searchable_form_url */,
std::string() /* searchable_form_encoding */,
GURL() /* client_side_redirect_url */,
std::nullopt /* devtools_initiator_info */,
nullptr /* trust_token_params */, std::nullopt /* impression */,
base::TimeTicks() /* renderer_before_unload_start */,
base::TimeTicks() /* renderer_before_unload_end */,
blink::mojom::NavigationInitiatorActivationAndAdStatus::
kDidNotStartWithTransientActivation,
false /* is_container_initiated */,
net::StorageAccessApiStatus::kNone, false /* has_rel_opener */);
auto common_params = blink::CreateCommonNavigationParams();
common_params->url = url;
common_params->initiator_origin = url::Origin::Create(url);
common_params->method = method;
common_params->download_policy = download_policy;
common_params->request_destination =
network::mojom::RequestDestination::kDocument;
url::Origin origin = url::Origin::Create(url);
FrameTreeNodeId frame_tree_node_id =
web_contents_->GetPrimaryMainFrame()->GetFrameTreeNodeId();
bool is_primary_main_frame = is_main_frame;
bool is_outermost_main_frame = is_main_frame;
std::unique_ptr<NavigationRequestInfo> request_info(
std::make_unique<NavigationRequestInfo>(
std::move(common_params), std::move(begin_params),
network::mojom::WebSandboxFlags::kNone,
net::IsolationInfo::Create(
net::IsolationInfo::RequestType::kMainFrame, origin, origin,
net::SiteForCookies::FromUrl(url)),
is_primary_main_frame, is_outermost_main_frame, is_main_frame,
false /* are_ancestors_secure */, frame_tree_node_id,
false /* report_raw_headers */,
upgrade_if_insecure /* upgrade_if_insecure */,
nullptr /* blob_url_loader_factory */,
base::UnguessableToken::Create() /* devtools_navigation_token */,
base::UnguessableToken::Create() /* devtools_frame_token */,
net::HttpRequestHeaders() /* cors_exempt_headers */,
nullptr /* client_security_state */,
std::nullopt /* devtools_accepted_stream_types */,
false /* is_pdf */,
ChildProcessHost::kInvalidUniqueID /* initiator_process_id */,
std::nullopt /* initiator_document_token */,
GlobalRenderFrameHostId() /* previous_render_frame_host_id */,
nullptr /* serving_page_metrics_container */,
false /* allow_cookies_from_browser */, 0 /* navigation_id */,
false /* shared_storage_writable */,
is_ad_tagged /* is_ad_tagged */,
false /* force_no_https_upgrade */));
return std::make_unique<NavigationURLLoaderImpl>(
browser_context_.get(), browser_context_->GetDefaultStoragePartition(),
std::move(request_info), nullptr /* navigation_ui_data */,
nullptr /* service_worker_handle */,
nullptr /* prefetched_signed_exchange_cache */, delegate,
mojo::NullRemote() /* cookie_access_obsever */,
mojo::NullRemote() /* trust_token_observer */,
mojo::NullRemote() /* shared_dictionary_observer */,
mojo::NullRemote() /* url_loader_network_observer */,
/*devtools_observer=*/mojo::NullRemote(),
/*device_bound_session_observer=*/mojo::NullRemote(),
std::move(interceptors));
}
// Requests |redirect_url|, which must return a HTTP 3xx redirect. It's also
// used as the initial origin.
// |request_method| is the method to use for the initial request.
// |expected_redirect_method| is the method that is expected to be used for
// the second request, after redirection.
// |expected_origin_value| is the expected value for the Origin header after
// redirection. If empty, expects that there will be no Origin header.
// Returns the last `network::ResourceRequest` used.
network::ResourceRequest HTTPRedirectOriginHeaderTest(
const GURL& redirect_url,
const std::string& request_method,
const std::string& expected_redirect_method,
const std::string& expected_origin_value,
bool expect_request_fail = false) {
TestNavigationURLLoaderDelegate delegate;
auto loader = CreateTestLoader(
redirect_url,
base::StringPrintf(
"%s: %s", net::HttpRequestHeaders::kOrigin,
redirect_url.DeprecatedGetOriginAsURL().spec().c_str()),
request_method, &delegate);
loader->Start();
delegate.WaitForRequestRedirected();
loader->FollowRedirect({}, {}, {});
EXPECT_EQ(expected_redirect_method, delegate.redirect_info().new_method);
if (expect_request_fail) {
delegate.WaitForRequestFailed();
} else {
delegate.WaitForResponseStarted();
}
network::ResourceRequest resource_request =
loader->GetResourceRequestForTesting();
// Note that there is no check for request success here because, for
// purposes of testing, the request very well may fail. For example, if the
// test redirects to an HTTPS server from an HTTP origin, thus it is cross
// origin, there is not an HTTPS server in this unit test framework, so the
// request would fail. However, that's fine, as long as the request headers
// are in order and pass the checks below.
if (expected_origin_value.empty()) {
EXPECT_FALSE(
resource_request.headers.HasHeader(net::HttpRequestHeaders::kOrigin));
} else {
EXPECT_THAT(
resource_request.headers.GetHeader(net::HttpRequestHeaders::kOrigin),
Optional(expected_origin_value));
}
return resource_request;
}
net::RedirectInfo NavigateAndReturnRedirectInfo(const GURL& url,
bool upgrade_if_insecure,
bool expect_request_fail) {
TestNavigationURLLoaderDelegate delegate;
auto loader = CreateTestLoader(
url,
base::StringPrintf("%s: %s", net::HttpRequestHeaders::kOrigin,
url.DeprecatedGetOriginAsURL().spec().c_str()),
"GET", &delegate, blink::NavigationDownloadPolicy(),
true /*is_main_frame*/, upgrade_if_insecure);
loader->Start();
delegate.WaitForRequestRedirected();
loader->FollowRedirect({}, {}, {});
if (expect_request_fail) {
delegate.WaitForRequestFailed();
} else {
delegate.WaitForResponseStarted();
}
return delegate.redirect_info();
}
protected:
void SetupClientHintsControllerDelegate(
const std::vector<network::mojom::WebClientHintsType>& client_hints) {
blink::UserAgentMetadata ua_metadata;
client_hints_controller_delegate_ =
std::make_unique<MockClientHintsControllerDelegate>(ua_metadata);
client_hints_controller_delegate_->SetAdditionalClientHints(client_hints);
browser_context_->SetClientHintsControllerDelegate(
client_hints_controller_delegate_.get());
}
std::unique_ptr<BrowserTaskEnvironment> task_environment_;
std::unique_ptr<net::test::MockNetworkChangeNotifier>
network_change_notifier_;
std::unique_ptr<MockClientHintsControllerDelegate>
client_hints_controller_delegate_;
std::unique_ptr<TestBrowserContext> browser_context_;
net::EmbeddedTestServer http_test_server_;
std::unique_ptr<RenderViewHostTestEnabler> rvh_test_enabler_;
std::unique_ptr<TestWebContents> web_contents_;
// NavigationURLLoaderImpl relies on the existence of the
// |frame_tree_node->navigation_request()|.
std::unique_ptr<NavigationSimulator> pending_navigation_;
};
// 304 responses should abort the navigation, rather than display the page.
TEST_F(NavigationURLLoaderImplTest, Response304) {
ASSERT_TRUE(http_test_server_.Start());
const GURL url = http_test_server_.GetURL("/page304.html");
TestNavigationURLLoaderDelegate delegate;
auto loader = CreateTestLoader(url, std::string(), "GET", &delegate);
loader->Start();
delegate.WaitForRequestFailed();
EXPECT_EQ(net::ERR_ABORTED, delegate.net_error());
}
TEST_F(NavigationURLLoaderImplTest, IsolationInfoOfMainFrameNavigation) {
ASSERT_TRUE(http_test_server_.Start());
const GURL url = http_test_server_.GetURL("/foo");
const url::Origin origin = url::Origin::Create(url);
TestNavigationURLLoaderDelegate delegate;
auto loader = CreateTestLoader(
url,
base::StringPrintf("%s: %s", net::HttpRequestHeaders::kOrigin,
url.DeprecatedGetOriginAsURL().spec().c_str()),
"GET", &delegate, blink::NavigationDownloadPolicy(),
true /*is_main_frame*/, false /*upgrade_if_insecure*/);
loader->Start();
delegate.WaitForResponseStarted();
ASSERT_TRUE(loader->GetResourceRequestForTesting().trusted_params);
EXPECT_TRUE(net::IsolationInfo::Create(
net::IsolationInfo::RequestType::kMainFrame, origin, origin,
net::SiteForCookies::FromOrigin(origin))
.IsEqualForTesting(loader->GetResourceRequestForTesting()
.trusted_params->isolation_info));
}
TEST_F(NavigationURLLoaderImplTest,
IsolationInfoOfRedirectedMainFrameNavigation) {
ASSERT_TRUE(http_test_server_.Start());
const GURL url = http_test_server_.GetURL("/redirect301-to-echo");
const GURL final_url = http_test_server_.GetURL("/echo");
const url::Origin origin = url::Origin::Create(url);
network::ResourceRequest resource_request = HTTPRedirectOriginHeaderTest(
url, "GET", "GET", url.DeprecatedGetOriginAsURL().spec());
ASSERT_TRUE(resource_request.trusted_params);
EXPECT_TRUE(
net::IsolationInfo::Create(net::IsolationInfo::RequestType::kMainFrame,
origin, origin,
net::SiteForCookies::FromOrigin(origin))
.IsEqualForTesting(resource_request.trusted_params->isolation_info));
}
TEST_F(NavigationURLLoaderImplTest, EnsureEnabledClientHints) {
base::test::ScopedFeatureList feature_list{
network::features::kOffloadAcceptCHFrameCheck};
ASSERT_TRUE(http_test_server_.Start());
const GURL url = http_test_server_.GetURL("/foo");
const url::Origin origin = url::Origin::Create(url);
std::vector<network::mojom::WebClientHintsType> expected_client_hints = {
network::mojom::WebClientHintsType::kUAArch,
network::mojom::WebClientHintsType::kUAWoW64,
};
SetupClientHintsControllerDelegate(expected_client_hints);
TestNavigationURLLoaderDelegate delegate;
auto loader =
CreateTestLoader(url, /*headers=*/"", /*method=*/"GET", &delegate);
loader->Start();
delegate.WaitForResponseStarted();
ASSERT_TRUE(loader->GetResourceRequestForTesting().trusted_params);
EXPECT_TRUE(loader->GetResourceRequestForTesting()
.trusted_params->enabled_client_hints.has_value());
// The default types are added in addition, and that is why `IsSupersetOf()`
// is used.
EXPECT_THAT(loader->GetResourceRequestForTesting()
.trusted_params->enabled_client_hints->hints,
testing::IsSupersetOf(expected_client_hints));
EXPECT_EQ(origin, loader->GetResourceRequestForTesting()
.trusted_params->enabled_client_hints->origin);
}
TEST_F(NavigationURLLoaderImplTest, EnsureEnabledClientHintsDisabled) {
base::test::ScopedFeatureList feature_list;
feature_list.InitWithFeatures(
{}, {network::features::kOffloadAcceptCHFrameCheck});
ASSERT_TRUE(http_test_server_.Start());
const GURL url = http_test_server_.GetURL("/foo");
const url::Origin origin = url::Origin::Create(url);
std::vector<network::mojom::WebClientHintsType> client_hints = {
network::mojom::WebClientHintsType::kUAArch,
network::mojom::WebClientHintsType::kUAWoW64,
};
SetupClientHintsControllerDelegate(client_hints);
blink::UserAgentMetadata ua_metadata;
TestNavigationURLLoaderDelegate delegate;
auto loader =
CreateTestLoader(url, /*headers=*/"", /*method=*/"GET", &delegate);
loader->Start();
delegate.WaitForResponseStarted();
ASSERT_TRUE(loader->GetResourceRequestForTesting().trusted_params);
EXPECT_FALSE(loader->GetResourceRequestForTesting()
.trusted_params->enabled_client_hints.has_value());
}
TEST_F(NavigationURLLoaderImplTest, EnsureEnabledClientHintsOnRedirect) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeatureWithParameters(
network::features::kOffloadAcceptCHFrameCheck,
{{"AcceptCHOffloadWithRedirect", "true"}});
ASSERT_TRUE(http_test_server_.Start());
const GURL url = http_test_server_.GetURL("/redirect301-to-echo");
const GURL final_url = http_test_server_.GetURL("/echo");
const url::Origin final_origin = url::Origin::Create(final_url);
std::vector<network::mojom::WebClientHintsType> expected_client_hints = {
network::mojom::WebClientHintsType::kUAArch,
network::mojom::WebClientHintsType::kUAWoW64,
};
SetupClientHintsControllerDelegate(expected_client_hints);
TestNavigationURLLoaderDelegate delegate;
auto loader =
CreateTestLoader(url, /*headers=*/"", /*method=*/"GET", &delegate);
loader->Start();
delegate.WaitForRequestRedirected();
loader->FollowRedirect({}, {}, {});
delegate.WaitForResponseStarted();
const auto& resource_request = loader->GetResourceRequestForTesting();
ASSERT_TRUE(resource_request.trusted_params);
EXPECT_TRUE(
resource_request.trusted_params->enabled_client_hints.has_value());
// The default types are added in addition, and that is why `IsSupersetOf()`
// is used.
EXPECT_THAT(resource_request.trusted_params->enabled_client_hints->hints,
testing::IsSupersetOf(expected_client_hints));
EXPECT_EQ(final_origin,
resource_request.trusted_params->enabled_client_hints->origin);
}
TEST_F(NavigationURLLoaderImplTest,
EnsureEnabledClientHintsOnCrossOriginRedirect) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeatureWithParameters(
network::features::kOffloadAcceptCHFrameCheck,
{{"AcceptCHOffloadWithRedirect", "true"}});
ASSERT_TRUE(http_test_server_.Start());
net::EmbeddedTestServer http_test_server2;
http_test_server2.AddDefaultHandlers(
base::FilePath(FILE_PATH_LITERAL("content/test/data")));
ASSERT_TRUE(http_test_server2.Start());
const GURL final_url = http_test_server2.GetURL("/echo");
const GURL url =
http_test_server_.GetURL("/server-redirect?" + final_url.spec());
const url::Origin final_origin = url::Origin::Create(final_url);
std::vector<network::mojom::WebClientHintsType> expected_client_hints = {
network::mojom::WebClientHintsType::kUAArch,
network::mojom::WebClientHintsType::kUAWoW64,
};
SetupClientHintsControllerDelegate(expected_client_hints);
TestNavigationURLLoaderDelegate delegate;
auto loader =
CreateTestLoader(url, /*headers=*/"", /*method=*/"GET", &delegate);
loader->Start();
delegate.WaitForRequestRedirected();
loader->FollowRedirect({}, {}, {});
delegate.WaitForResponseStarted();
const auto& resource_request_after_redirect =
loader->GetResourceRequestForTesting();
ASSERT_TRUE(resource_request_after_redirect.trusted_params);
EXPECT_TRUE(resource_request_after_redirect.trusted_params
->enabled_client_hints.has_value());
// The default types are added in addition, and that is why `IsSupersetOf()`
// is used.
EXPECT_THAT(resource_request_after_redirect.trusted_params
->enabled_client_hints->hints,
testing::IsSupersetOf(expected_client_hints));
EXPECT_EQ(final_origin, resource_request_after_redirect.trusted_params
->enabled_client_hints->origin);
}
TEST_F(NavigationURLLoaderImplTest, Redirect301Tests) {
ASSERT_TRUE(http_test_server_.Start());
const GURL url = http_test_server_.GetURL("/redirect301-to-echo");
const GURL https_redirect_url =
http_test_server_.GetURL("/redirect301-to-https");
HTTPRedirectOriginHeaderTest(url, "GET", "GET",
url.DeprecatedGetOriginAsURL().spec());
HTTPRedirectOriginHeaderTest(https_redirect_url, "GET", "GET", "null", true);
HTTPRedirectOriginHeaderTest(url, "POST", "GET", std::string());
HTTPRedirectOriginHeaderTest(https_redirect_url, "POST", "GET", std::string(),
true);
}
TEST_F(NavigationURLLoaderImplTest, Redirect302Tests) {
ASSERT_TRUE(http_test_server_.Start());
const GURL url = http_test_server_.GetURL("/redirect302-to-echo");
const GURL https_redirect_url =
http_test_server_.GetURL("/redirect302-to-https");
HTTPRedirectOriginHeaderTest(url, "GET", "GET",
url.DeprecatedGetOriginAsURL().spec());
HTTPRedirectOriginHeaderTest(https_redirect_url, "GET", "GET", "null", true);
HTTPRedirectOriginHeaderTest(url, "POST", "GET", std::string());
HTTPRedirectOriginHeaderTest(https_redirect_url, "POST", "GET", std::string(),
true);
}
TEST_F(NavigationURLLoaderImplTest, Redirect303Tests) {
ASSERT_TRUE(http_test_server_.Start());
const GURL url = http_test_server_.GetURL("/redirect303-to-echo");
const GURL https_redirect_url =
http_test_server_.GetURL("/redirect303-to-https");
HTTPRedirectOriginHeaderTest(url, "GET", "GET",
url.DeprecatedGetOriginAsURL().spec());
HTTPRedirectOriginHeaderTest(https_redirect_url, "GET", "GET", "null", true);
HTTPRedirectOriginHeaderTest(url, "POST", "GET", std::string());
HTTPRedirectOriginHeaderTest(https_redirect_url, "POST", "GET", std::string(),
true);
}
TEST_F(NavigationURLLoaderImplTest, Redirect307Tests) {
ASSERT_TRUE(http_test_server_.Start());
const GURL url = http_test_server_.GetURL("/redirect307-to-echo");
const GURL https_redirect_url =
http_test_server_.GetURL("/redirect307-to-https");
HTTPRedirectOriginHeaderTest(url, "GET", "GET",
url.DeprecatedGetOriginAsURL().spec());
HTTPRedirectOriginHeaderTest(https_redirect_url, "GET", "GET", "null", true);
HTTPRedirectOriginHeaderTest(url, "POST", "POST",
url.DeprecatedGetOriginAsURL().spec());
HTTPRedirectOriginHeaderTest(https_redirect_url, "POST", "POST", "null",
true);
}
TEST_F(NavigationURLLoaderImplTest, Redirect308Tests) {
ASSERT_TRUE(http_test_server_.Start());
const GURL url = http_test_server_.GetURL("/redirect308-to-echo");
const GURL https_redirect_url =
http_test_server_.GetURL("/redirect308-to-https");
HTTPRedirectOriginHeaderTest(url, "GET", "GET",
url.DeprecatedGetOriginAsURL().spec());
HTTPRedirectOriginHeaderTest(https_redirect_url, "GET", "GET", "null", true);
HTTPRedirectOriginHeaderTest(url, "POST", "POST",
url.DeprecatedGetOriginAsURL().spec());
HTTPRedirectOriginHeaderTest(https_redirect_url, "POST", "POST", "null",
true);
}
namespace {
// A `NavigationLoaderInterceptor` to test operations during an interceptor is
// running (between `MaybeCreateLoader()` call and its callback invocation, i.e.
// between `WaitUntilMaybeCreateLoader()` and `Unblock()`).
class TestAsyncInterceptor final : public NavigationLoaderInterceptor {
public:
TestAsyncInterceptor() { ResetRunLoop(); }
~TestAsyncInterceptor() override = default;
// Waits until the start of `MaybeCreateLoader()`.
void WaitUntilMaybeCreateLoader() { run_loop_->Run(); }
// Finishes `MaybeCreateLoader()` without intercepting the request.
void Unblock() {
// Allow `WaitUntilMaybeCreateLoader()` for the next redirect request.
ResetRunLoop();
std::move(loader_callback_).Run(std::nullopt);
}
private:
void MaybeCreateLoader(
const network::ResourceRequest& tentative_resource_request,
BrowserContext* browser_context,
LoaderCallback loader_callback,
FallbackCallback fallback_callback) override {
run_loop_->Quit();
loader_callback_ = std::move(loader_callback);
}
bool MaybeCreateLoaderForResponse(
const network::URLLoaderCompletionStatus& status,
const network::ResourceRequest& request,
network::mojom::URLResponseHeadPtr* response_head,
mojo::ScopedDataPipeConsumerHandle* response_body,
mojo::PendingRemote<network::mojom::URLLoader>* loader,
mojo::PendingReceiver<network::mojom::URLLoaderClient>* client_receiver,
blink::ThrottlingURLLoader* url_loader,
bool* skip_other_interceptors) override {
return false;
}
void ResetRunLoop() { run_loop_ = std::make_unique<base::RunLoop>(); }
std::unique_ptr<base::RunLoop> run_loop_;
LoaderCallback loader_callback_;
};
// A `NavigationLoaderInterceptor` to test `MaybeCreateLoaderForResponse()`
// triggering redirects.
class TestResponseInterceptor final : public NavigationLoaderInterceptor {
public:
explicit TestResponseInterceptor(const GURL& redirect_url)
: redirect_url_(redirect_url) {}
~TestResponseInterceptor() override = default;
int response_count() const { return response_count_; }
void set_should_redirect(bool should_redirect) {
should_redirect_ = should_redirect;
}
private:
void MaybeCreateLoader(
const network::ResourceRequest& tentative_resource_request,
BrowserContext* browser_context,
LoaderCallback callback,
FallbackCallback fallback_callback) override {
std::move(callback).Run(std::nullopt);
}
bool MaybeCreateLoaderForResponse(
const network::URLLoaderCompletionStatus& status,
const network::ResourceRequest& request,
network::mojom::URLResponseHeadPtr* response_head,
mojo::ScopedDataPipeConsumerHandle* response_body,
mojo::PendingRemote<network::mojom::URLLoader>* loader,
mojo::PendingReceiver<network::mojom::URLLoaderClient>* client_receiver,
blink::ThrottlingURLLoader* url_loader,
bool* skip_other_interceptors) override {
if (!should_redirect_) {
return false;
}
++response_count_;
mojo::Remote<network::mojom::URLLoaderClient> client;
*client_receiver = client.BindNewPipeAndPassReceiver();
// Create an artificial redirect back to the fallback URL.
auto new_response_head = network::mojom::URLResponseHead::New();
net::RedirectInfo redirect_info = net::RedirectInfo::ComputeRedirectInfo(
request.method, request.url, request.site_for_cookies,
request.update_first_party_url_on_redirect
? net::RedirectInfo::FirstPartyURLPolicy::UPDATE_URL_ON_REDIRECT
: net::RedirectInfo::FirstPartyURLPolicy::NEVER_CHANGE_URL,
request.referrer_policy, request.referrer.spec(),
request.request_initiator, net::HTTP_TEMPORARY_REDIRECT, redirect_url_,
/*referrer_policy_header=*/std::nullopt,
/*insecure_scheme_was_upgraded=*/false);
client->OnReceiveRedirect(redirect_info, std::move(new_response_head));
return true;
}
const GURL redirect_url_;
int response_count_ = 0;
bool should_redirect_ = true;
};
// This sets the timeout timer but doesn't expect the timer is fired
// automatically. If needed, the timer should be fired explicitly e.g. via
// `TriggerTimeoutForTesting()`.
void SetLargeNavigationTimeout(NavigationURLLoaderImpl& loader) {
loader.SetNavigationTimeout(base::Seconds(1000));
}
} // namespace
// Timeout case (failure) while waiting for the response from URLLoader.
TEST_F(NavigationURLLoaderImplTest, TimeoutDuringURLLoader) {
ASSERT_TRUE(http_test_server_.Start());
const GURL final_url = http_test_server_.GetURL("/echo");
TestNavigationURLLoaderDelegate delegate;
auto loader = CreateTestLoader(final_url, std::string(), "GET", &delegate);
loader->Start();
SetLargeNavigationTimeout(*loader);
// Simulate timeout during the async operation.
// `delegate` shouldn't be notified synchronously.
static_cast<NavigationURLLoaderImpl*>(loader.get())
->TriggerTimeoutForTesting();
EXPECT_EQ(delegate.on_redirect_handled_counter(), 0);
EXPECT_EQ(delegate.on_request_handled_counter(), 0);
// Wait for the failure due to the timeout is notified.
//
// Note [*1]:
// In the expected non-test scenario, the operations below should occur
// between `TriggerTimeoutForTesting()` and `WaitForRequestFailed()` (which
// should be quite rare though), because `NavigationRequest` destroys
// `NavigationURLLoaderImpl` upon `OnRequestFailed()`.
// In tests, `WaitForRequestFailed()` is called before the operations below to
// avoid race conditions, but we can consider as if the `OnRequestFailed()`
// waited here is received after the operations below, because
// `OnRequestFailed()` can delay as a pending posted task without interfering
// with other parts of `NavigationURLLoaderImpl`.
delegate.WaitForRequestFailed();
EXPECT_EQ(net::ERR_TIMED_OUT, delegate.net_error());
EXPECT_EQ(delegate.on_redirect_handled_counter(), 0);
EXPECT_EQ(delegate.on_request_handled_counter(), 1);
// Check that no further loading should occur.
task_environment_->RunUntilIdle();
EXPECT_EQ(delegate.on_redirect_handled_counter(), 0);
EXPECT_EQ(delegate.on_request_handled_counter(), 1);
}
// Timeout + MaybeCreateLoaderForResponse() + redirect case (failure) while
// waiting for the response from URLLoader.
TEST_F(NavigationURLLoaderImplTest, RedirectDuringURLLoader) {
ASSERT_TRUE(http_test_server_.Start());
const GURL final_url = http_test_server_.GetURL("/echo");
const GURL interceptor_url = http_test_server_.GetURL("/foo");
TestNavigationURLLoaderDelegate delegate;
auto response_interceptor =
std::make_unique<TestResponseInterceptor>(interceptor_url);
auto* response_interceptor_ptr = response_interceptor.get();
std::vector<std::unique_ptr<NavigationLoaderInterceptor>> interceptors;
interceptors.push_back(std::move(response_interceptor));
auto loader =
CreateTestLoader(final_url, std::string(), "GET", &delegate,
blink::NavigationDownloadPolicy(),
/*is_main_frame=*/true,
/*upgrade_if_insecure=*/false,
/*is_ad_tagged=*/false, std::move(interceptors));
loader->Start();
SetLargeNavigationTimeout(*loader);
// Simulate timeout during the async operation.
// `delegate` shouldn't be notified synchronously.
static_cast<NavigationURLLoaderImpl*>(loader.get())
->TriggerTimeoutForTesting();
EXPECT_EQ(delegate.on_redirect_handled_counter(), 0);
EXPECT_EQ(delegate.on_request_handled_counter(), 0);
// Interceptor's MaybeCreateLoaderForResponse() is processed synchronously,
// which triggers `OnReceiveRedirect()` asynchronously.
ASSERT_EQ(response_interceptor_ptr->response_count(), 1);
// Note [*2]:
// Avoid `MaybeCreateLoaderForResponse()` intercepting the response below, to
// simplify the expectation a bit. The main thing to test is possibly
// triggering `MaybeCreateLoaderForResponse()` from `OnComplete()` (i.e. from
// `TriggerTimeoutForTesting()` above), not the response possibly received
// (unexpectedly) below after the timeout.
response_interceptor_ptr->set_should_redirect(false);
// Wait for the redirect due to the timeout + interceptor is notified.
delegate.WaitForRequestRedirected();
EXPECT_EQ(delegate.on_redirect_handled_counter(), 1);
EXPECT_EQ(delegate.on_request_handled_counter(), 0);
// Check that no further loading should occur.
task_environment_->RunUntilIdle();
EXPECT_EQ(delegate.on_redirect_handled_counter(), 1);
EXPECT_EQ(delegate.on_request_handled_counter(), 0);
}
// Timeout case (failure) with async `OnRequestRedirected()` ->
// `FollowRedirect()`.
TEST_F(NavigationURLLoaderImplTest, TimeoutDuringFollowRedirect) {
ASSERT_TRUE(http_test_server_.Start());
const GURL redirect_url = http_test_server_.GetURL("/redirect301-to-echo");
TestNavigationURLLoaderDelegate delegate;
auto loader = CreateTestLoader(redirect_url, std::string(), "GET", &delegate);
loader->Start();
SetLargeNavigationTimeout(*loader);
// Wait for the async operation starts.
delegate.WaitForRequestRedirected();
EXPECT_EQ(delegate.on_redirect_handled_counter(), 1);
EXPECT_EQ(delegate.on_request_handled_counter(), 0);
// Simulate timeout during the async operation.
// `delegate` shouldn't be notified synchronously.
static_cast<NavigationURLLoaderImpl*>(loader.get())
->TriggerTimeoutForTesting();
EXPECT_EQ(delegate.on_redirect_handled_counter(), 1);
EXPECT_EQ(delegate.on_request_handled_counter(), 0);
// Wait for the failure due to the timeout is notified (See Note [*1] above).
delegate.WaitForRequestFailed();
EXPECT_EQ(net::ERR_TIMED_OUT, delegate.net_error());
EXPECT_EQ(delegate.on_redirect_handled_counter(), 1);
EXPECT_EQ(delegate.on_request_handled_counter(), 1);
// Finish the async operation.
loader->FollowRedirect({}, {}, {});
// Check that no further loading should occur.
task_environment_->RunUntilIdle();
EXPECT_EQ(delegate.on_redirect_handled_counter(), 1);
EXPECT_EQ(delegate.on_request_handled_counter(), 1);
}
// Timeout + MaybeCreateLoaderForResponse() + redirect case (failure) with async
// `OnRequestRedirected()` -> `FollowRedirect()`.
TEST_F(NavigationURLLoaderImplTest, RedirectDuringFollowRedirect) {
ASSERT_TRUE(http_test_server_.Start());
const GURL redirect_url = http_test_server_.GetURL("/redirect301-to-echo");
const GURL interceptor_url = http_test_server_.GetURL("/foo");
TestNavigationURLLoaderDelegate delegate;
auto response_interceptor =
std::make_unique<TestResponseInterceptor>(interceptor_url);
auto* response_interceptor_ptr = response_interceptor.get();
std::vector<std::unique_ptr<NavigationLoaderInterceptor>> interceptors;
interceptors.push_back(std::move(response_interceptor));
auto loader =
CreateTestLoader(redirect_url, std::string(), "GET", &delegate,
blink::NavigationDownloadPolicy(),
/*is_main_frame=*/true,
/*upgrade_if_insecure=*/false,
/*is_ad_tagged=*/false, std::move(interceptors));
loader->Start();
SetLargeNavigationTimeout(*loader);
// Wait for the async operation starts.
delegate.WaitForRequestRedirected();
EXPECT_EQ(delegate.on_redirect_handled_counter(), 1);
EXPECT_EQ(delegate.on_request_handled_counter(), 0);
// Simulate timeout during the async operation.
// `delegate` shouldn't be notified synchronously.
static_cast<NavigationURLLoaderImpl*>(loader.get())
->TriggerTimeoutForTesting();
EXPECT_EQ(delegate.on_redirect_handled_counter(), 1);
EXPECT_EQ(delegate.on_request_handled_counter(), 0);
// `MaybeCreateLoaderForResponse()` shouldn't be called during an exclusive
// task.
ASSERT_EQ(response_interceptor_ptr->response_count(), 0);
// See Note [*2] above.
response_interceptor_ptr->set_should_redirect(false);
// Wait for the failure due to the timeout is notified (See Note [*1] above).
delegate.WaitForRequestFailed();
EXPECT_EQ(net::ERR_TIMED_OUT, delegate.net_error());
EXPECT_EQ(delegate.on_redirect_handled_counter(), 1);
EXPECT_EQ(delegate.on_request_handled_counter(), 1);
// Finish the async operation.
loader->FollowRedirect({}, {}, {});
// Check that no further loading should occur.
task_environment_->RunUntilIdle();
EXPECT_EQ(delegate.on_redirect_handled_counter(), 1);
EXPECT_EQ(delegate.on_request_handled_counter(), 1);
}
// Successful case with async `ParseHeaders()`.
TEST_F(NavigationURLLoaderImplTest, ForceAsyncParseHeaders) {
ASSERT_TRUE(http_test_server_.Start());
const GURL redirect_url = http_test_server_.GetURL("/redirect301-to-echo");
const GURL final_url = http_test_server_.GetURL("/echo");
TestNavigationURLLoaderDelegate delegate;
auto loader = CreateTestLoader(redirect_url, std::string(), "GET", &delegate);
loader->Start();
SetLargeNavigationTimeout(*loader);
// Force the async ParseHeaders() path for redirects.
delegate.set_clear_parsed_headers_on_redirect(true);
// Wait for the async operation starts.
delegate.WaitForOnReceiveRedirect();
EXPECT_EQ(delegate.on_redirect_handled_counter(), 0);
EXPECT_EQ(delegate.on_request_handled_counter(), 0);
// Finish the async operation (`ParseHeaders()` automatically completes while
// running the task queue) and wait for redirect/response received after the
// async `ParseHeaders()`.
delegate.WaitForRequestRedirected();
EXPECT_EQ(delegate.redirect_info().new_url, final_url);
EXPECT_EQ(delegate.on_redirect_handled_counter(), 1);
EXPECT_EQ(delegate.on_request_handled_counter(), 0);
loader->FollowRedirect({}, {}, {});
delegate.WaitForResponseStarted();
EXPECT_EQ(net::OK, delegate.net_error());
EXPECT_EQ(delegate.on_redirect_handled_counter(), 1);
EXPECT_EQ(delegate.on_request_handled_counter(), 1);
}
// Timouet case (failure) with async `ParseHeaders()`.
TEST_F(NavigationURLLoaderImplTest, TimeoutDuringParseHeaders) {
ASSERT_TRUE(http_test_server_.Start());
const GURL redirect_url = http_test_server_.GetURL("/redirect301-to-echo");
TestNavigationURLLoaderDelegate delegate;
auto loader = CreateTestLoader(redirect_url, std::string(), "GET", &delegate);
loader->Start();
SetLargeNavigationTimeout(*loader);
// Force the async ParseHeaders() path for redirects.
delegate.set_clear_parsed_headers_on_redirect(true);
// Wait for the async operation starts.
delegate.WaitForOnReceiveRedirect();
EXPECT_EQ(delegate.on_redirect_handled_counter(), 0);
EXPECT_EQ(delegate.on_request_handled_counter(), 0);
// Simulate timeout during the async operation.
static_cast<NavigationURLLoaderImpl*>(loader.get())
->TriggerTimeoutForTesting();
EXPECT_EQ(delegate.on_redirect_handled_counter(), 0);
EXPECT_EQ(delegate.on_request_handled_counter(), 0);
// Wait for the failure due to the timeout is notified (See Note [*1] above).
delegate.WaitForRequestFailed();
EXPECT_EQ(net::ERR_TIMED_OUT, delegate.net_error());
EXPECT_EQ(delegate.on_redirect_handled_counter(), 0);
EXPECT_EQ(delegate.on_request_handled_counter(), 1);
// Finish the async operation (`ParseHeaders()` automatically completes while
// running the task queue). No further loading should occur.
task_environment_->RunUntilIdle();
EXPECT_EQ(delegate.on_redirect_handled_counter(), 0);
EXPECT_EQ(delegate.on_request_handled_counter(), 1);
}
// Timeout + MaybeCreateLoaderForResponse() + redirect case (failure) with async
// `ParseHeaders()`.
TEST_F(NavigationURLLoaderImplTest, RedirectDuringParseHeaders) {
ASSERT_TRUE(http_test_server_.Start());
const GURL redirect_url = http_test_server_.GetURL("/redirect301-to-echo");
const GURL interceptor_url = http_test_server_.GetURL("/foo");
TestNavigationURLLoaderDelegate delegate;
auto response_interceptor =
std::make_unique<TestResponseInterceptor>(interceptor_url);
auto* response_interceptor_ptr = response_interceptor.get();
std::vector<std::unique_ptr<NavigationLoaderInterceptor>> interceptors;
interceptors.push_back(std::move(response_interceptor));
auto loader =
CreateTestLoader(redirect_url, std::string(), "GET", &delegate,
blink::NavigationDownloadPolicy(),
/*is_main_frame=*/true,
/*upgrade_if_insecure=*/false,
/*is_ad_tagged=*/false, std::move(interceptors));
loader->Start();
SetLargeNavigationTimeout(*loader);
// Force the async ParseHeaders() path for redirects.
delegate.set_clear_parsed_headers_on_redirect(true);
// Wait for the async operation starts.
delegate.WaitForOnReceiveRedirect();
EXPECT_EQ(delegate.on_redirect_handled_counter(), 0);
EXPECT_EQ(delegate.on_request_handled_counter(), 0);
EXPECT_EQ(response_interceptor_ptr->response_count(), 0);
// Simulate timeout during the async operation.
// `delegate` shouldn't be notified synchronously.
static_cast<NavigationURLLoaderImpl*>(loader.get())
->TriggerTimeoutForTesting();
EXPECT_EQ(delegate.on_redirect_handled_counter(), 0);
EXPECT_EQ(delegate.on_request_handled_counter(), 0);
// `MaybeCreateLoaderForResponse()` shouldn't be called during an exclusive
// task.
ASSERT_EQ(response_interceptor_ptr->response_count(), 0);
// Wait for the failure due to the timeout is notified (See Note [*1] above).
delegate.WaitForRequestFailed();
EXPECT_EQ(net::ERR_TIMED_OUT, delegate.net_error());
EXPECT_EQ(delegate.on_redirect_handled_counter(), 0);
EXPECT_EQ(delegate.on_request_handled_counter(), 1);
// Finish the async operation (`ParseHeaders()` automatically completes while
// running the task queue). No further loading should occur.
task_environment_->RunUntilIdle();
EXPECT_EQ(delegate.on_redirect_handled_counter(), 0);
EXPECT_EQ(delegate.on_request_handled_counter(), 1);
}
// Successful case with an async interceptor (initial request).
TEST_F(NavigationURLLoaderImplTest, ForceAsyncInterceptor) {
ASSERT_TRUE(http_test_server_.Start());
const GURL final_url = http_test_server_.GetURL("/echo");
TestNavigationURLLoaderDelegate delegate;
auto async_interceptor = std::make_unique<TestAsyncInterceptor>();
auto* async_interceptor_ptr = async_interceptor.get();
std::vector<std::unique_ptr<NavigationLoaderInterceptor>> interceptors;
interceptors.push_back(std::move(async_interceptor));
auto loader =
CreateTestLoader(final_url, std::string(), "GET", &delegate,
blink::NavigationDownloadPolicy(),
/*is_main_frame=*/true,
/*upgrade_if_insecure=*/false,
/*is_ad_tagged=*/false, std::move(interceptors));
loader->Start();
SetLargeNavigationTimeout(*loader);
// Wait for the async operation starts.
async_interceptor_ptr->WaitUntilMaybeCreateLoader();
EXPECT_EQ(delegate.on_redirect_handled_counter(), 0);
EXPECT_EQ(delegate.on_request_handled_counter(), 0);
// Finish the async operation.
async_interceptor_ptr->Unblock();
EXPECT_EQ(delegate.on_redirect_handled_counter(), 0);
EXPECT_EQ(delegate.on_request_handled_counter(), 0);
// Wait for response received after the interceptor.
delegate.WaitForResponseStarted();
EXPECT_EQ(net::OK, delegate.net_error());
EXPECT_EQ(delegate.on_redirect_handled_counter(), 0);
EXPECT_EQ(delegate.on_request_handled_counter(), 1);
}
// Timeout case (failure) with an async interceptor (initial request).
TEST_F(NavigationURLLoaderImplTest, TimeoutDuringAsyncInterceptor) {
ASSERT_TRUE(http_test_server_.Start());
const GURL final_url = http_test_server_.GetURL("/echo");
TestNavigationURLLoaderDelegate delegate;
auto async_interceptor = std::make_unique<TestAsyncInterceptor>();
auto* async_interceptor_ptr = async_interceptor.get();
std::vector<std::unique_ptr<NavigationLoaderInterceptor>> interceptors;
interceptors.push_back(std::move(async_interceptor));
auto loader =
CreateTestLoader(final_url, std::string(), "GET", &delegate,
blink::NavigationDownloadPolicy(),
/*is_main_frame=*/true,
/*upgrade_if_insecure=*/false,
/*is_ad_tagged=*/false, std::move(interceptors));
loader->Start();
SetLargeNavigationTimeout(*loader);
// Wait for the async operation starts.
async_interceptor_ptr->WaitUntilMaybeCreateLoader();
EXPECT_EQ(delegate.on_redirect_handled_counter(), 0);
EXPECT_EQ(delegate.on_request_handled_counter(), 0);
// Simulate timeout during the async operation.
// `delegate` shouldn't be notified synchronously.
static_cast<NavigationURLLoaderImpl*>(loader.get())
->TriggerTimeoutForTesting();
EXPECT_EQ(delegate.on_redirect_handled_counter(), 0);
EXPECT_EQ(delegate.on_request_handled_counter(), 0);
// Wait for the failure due to the timeout is notified (See Note [*1] above).
delegate.WaitForRequestFailed();
EXPECT_EQ(net::ERR_TIMED_OUT, delegate.net_error());
EXPECT_EQ(delegate.on_redirect_handled_counter(), 0);
EXPECT_EQ(delegate.on_request_handled_counter(), 1);
// Finish the async operation.
async_interceptor_ptr->Unblock();
EXPECT_EQ(delegate.on_redirect_handled_counter(), 0);
EXPECT_EQ(delegate.on_request_handled_counter(), 1);
// Check that no further loading should occur.
task_environment_->RunUntilIdle();
EXPECT_EQ(delegate.on_redirect_handled_counter(), 0);
EXPECT_EQ(delegate.on_request_handled_counter(), 1);
}
// Timeout + MaybeCreateLoaderForResponse() + redirect case (failure) with an
// async interceptor (initial request).
TEST_F(NavigationURLLoaderImplTest, RedirectDuringAsyncInterceptor) {
ASSERT_TRUE(http_test_server_.Start());
const GURL final_url = http_test_server_.GetURL("/echo");
const GURL interceptor_url = http_test_server_.GetURL("/foo");
TestNavigationURLLoaderDelegate delegate;
auto response_interceptor =
std::make_unique<TestResponseInterceptor>(interceptor_url);
auto* response_interceptor_ptr = response_interceptor.get();
auto async_interceptor = std::make_unique<TestAsyncInterceptor>();
auto* async_interceptor_ptr = async_interceptor.get();
std::vector<std::unique_ptr<NavigationLoaderInterceptor>> interceptors;
interceptors.push_back(std::move(response_interceptor));
interceptors.push_back(std::move(async_interceptor));
auto loader =
CreateTestLoader(final_url, std::string(), "GET", &delegate,
blink::NavigationDownloadPolicy(),
/*is_main_frame=*/true,
/*upgrade_if_insecure=*/false,
/*is_ad_tagged=*/false, std::move(interceptors));
loader->Start();
SetLargeNavigationTimeout(*loader);
// Wait for the async operation starts.
async_interceptor_ptr->WaitUntilMaybeCreateLoader();
EXPECT_EQ(delegate.on_redirect_handled_counter(), 0);
EXPECT_EQ(delegate.on_request_handled_counter(), 0);
// Simulate timeout during the async operation.
// `delegate` shouldn't be notified synchronously.
static_cast<NavigationURLLoaderImpl*>(loader.get())
->TriggerTimeoutForTesting();
EXPECT_EQ(delegate.on_redirect_handled_counter(), 0);
EXPECT_EQ(delegate.on_request_handled_counter(), 0);
// Interceptor's MaybeCreateLoaderForResponse() isn't processed here, because
// `default_loader_used_` is false. Therefore falling back to the same
// scenario as the `TimeoutDuringAsyncInterceptor` test above.
// Anyway, `MaybeCreateLoaderForResponse()` shouldn't be called during an
// exclusive task.
ASSERT_EQ(response_interceptor_ptr->response_count(), 0);
// See Note [*2] above.
response_interceptor_ptr->set_should_redirect(false);
// Wait for the failure due to the timeout is notified (See Note [*1] above).
delegate.WaitForRequestFailed();
EXPECT_EQ(net::ERR_TIMED_OUT, delegate.net_error());
EXPECT_EQ(delegate.on_redirect_handled_counter(), 0);
EXPECT_EQ(delegate.on_request_handled_counter(), 1);
// Finish the async operation.
async_interceptor_ptr->Unblock();
EXPECT_EQ(delegate.on_redirect_handled_counter(), 0);
EXPECT_EQ(delegate.on_request_handled_counter(), 1);
// Check that no further loading should occur.
task_environment_->RunUntilIdle();
EXPECT_EQ(delegate.on_redirect_handled_counter(), 0);
EXPECT_EQ(delegate.on_request_handled_counter(), 1);
}
// Successful case with an async interceptor (redirected request).
TEST_F(NavigationURLLoaderImplTest, ForceAsyncInterceptorForRedirect) {
ASSERT_TRUE(http_test_server_.Start());
const GURL redirect_url = http_test_server_.GetURL("/redirect301-to-echo");
const GURL final_url = http_test_server_.GetURL("/echo");
TestNavigationURLLoaderDelegate delegate;
auto async_interceptor = std::make_unique<TestAsyncInterceptor>();
auto* async_interceptor_ptr = async_interceptor.get();
std::vector<std::unique_ptr<NavigationLoaderInterceptor>> interceptors;
interceptors.push_back(std::move(async_interceptor));
auto loader =
CreateTestLoader(redirect_url, std::string(), "GET", &delegate,
blink::NavigationDownloadPolicy(),
/*is_main_frame=*/true,
/*upgrade_if_insecure=*/false,
/*is_ad_tagged=*/false, std::move(interceptors));
loader->Start();
SetLargeNavigationTimeout(*loader);
// Process the initial request.
async_interceptor_ptr->WaitUntilMaybeCreateLoader();
async_interceptor_ptr->Unblock();
delegate.WaitForRequestRedirected();
loader->FollowRedirect({}, {}, {});
EXPECT_EQ(delegate.on_redirect_handled_counter(), 1);
EXPECT_EQ(delegate.on_request_handled_counter(), 0);
// Wait for the async operation starts.
// This processes the redirected request until the async interceptor starts.
async_interceptor_ptr->WaitUntilMaybeCreateLoader();
EXPECT_EQ(delegate.on_redirect_handled_counter(), 1);
EXPECT_EQ(delegate.on_request_handled_counter(), 0);
// Finish the async operation.
async_interceptor_ptr->Unblock();
EXPECT_EQ(delegate.on_redirect_handled_counter(), 1);
EXPECT_EQ(delegate.on_request_handled_counter(), 0);
// Wait for response received after the interceptor.
delegate.WaitForResponseStarted();
EXPECT_EQ(net::OK, delegate.net_error());
EXPECT_EQ(delegate.on_redirect_handled_counter(), 1);
EXPECT_EQ(delegate.on_request_handled_counter(), 1);
}
// Timeout case (failure) with an async interceptor (redirect request).
TEST_F(NavigationURLLoaderImplTest, TimeoutDuringAsyncInterceptorForRedirect) {
ASSERT_TRUE(http_test_server_.Start());
const GURL redirect_url = http_test_server_.GetURL("/redirect301-to-echo");
const GURL final_url = http_test_server_.GetURL("/echo");
TestNavigationURLLoaderDelegate delegate;
auto async_interceptor = std::make_unique<TestAsyncInterceptor>();
auto* async_interceptor_ptr = async_interceptor.get();
std::vector<std::unique_ptr<NavigationLoaderInterceptor>> interceptors;
interceptors.push_back(std::move(async_interceptor));
auto loader =
CreateTestLoader(redirect_url, std::string(), "GET", &delegate,
blink::NavigationDownloadPolicy(),
/*is_main_frame=*/true,
/*upgrade_if_insecure=*/false,
/*is_ad_tagged=*/false, std::move(interceptors));
loader->Start();
SetLargeNavigationTimeout(*loader);
// Process the initial request.
async_interceptor_ptr->WaitUntilMaybeCreateLoader();
async_interceptor_ptr->Unblock();
delegate.WaitForRequestRedirected();
loader->FollowRedirect({}, {}, {});
EXPECT_EQ(delegate.on_redirect_handled_counter(), 1);
EXPECT_EQ(delegate.on_request_handled_counter(), 0);
// Wait for the async operation starts.
// This processes the redirected request until the async interceptor starts.
async_interceptor_ptr->WaitUntilMaybeCreateLoader();
EXPECT_EQ(delegate.on_redirect_handled_counter(), 1);
EXPECT_EQ(delegate.on_request_handled_counter(), 0);
// Simulate timeout during the async operation.
// `delegate` shouldn't be notified synchronously.
static_cast<NavigationURLLoaderImpl*>(loader.get())
->TriggerTimeoutForTesting();
EXPECT_EQ(delegate.on_redirect_handled_counter(), 1);
EXPECT_EQ(delegate.on_request_handled_counter(), 0);
// Wait for the failure due to the timeout is notified (See Note [*1] above).
delegate.WaitForRequestFailed();
EXPECT_EQ(net::ERR_TIMED_OUT, delegate.net_error());
EXPECT_EQ(delegate.on_redirect_handled_counter(), 1);
EXPECT_EQ(delegate.on_request_handled_counter(), 1);
// Finish the async operation.
async_interceptor_ptr->Unblock();
EXPECT_EQ(delegate.on_redirect_handled_counter(), 1);
EXPECT_EQ(delegate.on_request_handled_counter(), 1);
// Check that no further loading should occur.
task_environment_->RunUntilIdle();
EXPECT_EQ(delegate.on_redirect_handled_counter(), 1);
EXPECT_EQ(delegate.on_request_handled_counter(), 1);
}
// Timeout + MaybeCreateLoaderForResponse() + redirect case (failure) with an
// async interceptor (redirect request).
TEST_F(NavigationURLLoaderImplTest, RedirectDuringAsyncInterceptorForRedirect) {
ASSERT_TRUE(http_test_server_.Start());
const GURL redirect_url = http_test_server_.GetURL("/redirect301-to-echo");
const GURL final_url = http_test_server_.GetURL("/echo");
const GURL interceptor_url = http_test_server_.GetURL("/foo");
TestNavigationURLLoaderDelegate delegate;
auto response_interceptor =
std::make_unique<TestResponseInterceptor>(interceptor_url);
auto* response_interceptor_ptr = response_interceptor.get();
auto async_interceptor = std::make_unique<TestAsyncInterceptor>();
auto* async_interceptor_ptr = async_interceptor.get();
std::vector<std::unique_ptr<NavigationLoaderInterceptor>> interceptors;
interceptors.push_back(std::move(response_interceptor));
interceptors.push_back(std::move(async_interceptor));
auto loader =
CreateTestLoader(redirect_url, std::string(), "GET", &delegate,
blink::NavigationDownloadPolicy(),
/*is_main_frame=*/true,
/*upgrade_if_insecure=*/false,
/*is_ad_tagged=*/false, std::move(interceptors));
loader->Start();
SetLargeNavigationTimeout(*loader);
// Process the initial request.
async_interceptor_ptr->WaitUntilMaybeCreateLoader();
async_interceptor_ptr->Unblock();
delegate.WaitForRequestRedirected();
loader->FollowRedirect({}, {}, {});
EXPECT_EQ(delegate.on_redirect_handled_counter(), 1);
EXPECT_EQ(delegate.on_request_handled_counter(), 0);
// Wait for the async operation starts.
// This processes the redirected request until the async interceptor starts.
async_interceptor_ptr->WaitUntilMaybeCreateLoader();
EXPECT_EQ(delegate.on_redirect_handled_counter(), 1);
EXPECT_EQ(delegate.on_request_handled_counter(), 0);
EXPECT_EQ(response_interceptor_ptr->response_count(), 0);
// Simulate timeout during the async operation.
// `delegate` shouldn't be notified synchronously.
static_cast<NavigationURLLoaderImpl*>(loader.get())
->TriggerTimeoutForTesting();
EXPECT_EQ(delegate.on_redirect_handled_counter(), 1);
EXPECT_EQ(delegate.on_request_handled_counter(), 0);
// `MaybeCreateLoaderForResponse()` shouldn't be called during an exclusive
// task.
ASSERT_EQ(response_interceptor_ptr->response_count(), 0);
// See Note [*2] above.
response_interceptor_ptr->set_should_redirect(false);
// Wait for the failure due to the timeout is notified (See Note [*1] above).
delegate.WaitForRequestFailed();
EXPECT_EQ(net::ERR_TIMED_OUT, delegate.net_error());
EXPECT_EQ(delegate.on_redirect_handled_counter(), 1);
EXPECT_EQ(delegate.on_request_handled_counter(), 1);
// Finish the async operation.
async_interceptor_ptr->Unblock();
EXPECT_EQ(delegate.on_redirect_handled_counter(), 1);
EXPECT_EQ(delegate.on_request_handled_counter(), 1);
// Check that no further loading should occur.
task_environment_->RunUntilIdle();
EXPECT_EQ(delegate.on_redirect_handled_counter(), 1);
EXPECT_EQ(delegate.on_request_handled_counter(), 1);
}
TEST_F(NavigationURLLoaderImplTest, RedirectModifiedHeaders) {
ASSERT_TRUE(http_test_server_.Start());
const GURL redirect_url = http_test_server_.GetURL("/redirect301-to-echo");
TestNavigationURLLoaderDelegate delegate;
auto loader = CreateTestLoader(
redirect_url, "Header1: Value1\r\nHeader2: Value2", "GET", &delegate);
loader->Start();
delegate.WaitForRequestRedirected();
// Initial request should only have initial headers.
EXPECT_THAT(
loader->GetResourceRequestForTesting().headers.GetHeader("Header1"),
Optional(std::string("Value1")));
EXPECT_THAT(
loader->GetResourceRequestForTesting().headers.GetHeader("Header2"),
Optional(std::string("Value2")));
EXPECT_FALSE(
loader->GetResourceRequestForTesting().headers.HasHeader("Header3"));
// Overwrite Header2 and add Header3.
net::HttpRequestHeaders redirect_headers;
redirect_headers.SetHeader("Header2", "");
redirect_headers.SetHeader("Header3", "Value3");
loader->FollowRedirect({}, redirect_headers, {});
delegate.WaitForResponseStarted();
// Redirected request should also have modified headers.
EXPECT_THAT(
loader->GetResourceRequestForTesting().headers.GetHeader("Header1"),
Optional(std::string("Value1")));
EXPECT_THAT(
loader->GetResourceRequestForTesting().headers.GetHeader("Header2"),
Optional(std::string("")));
EXPECT_THAT(
loader->GetResourceRequestForTesting().headers.GetHeader("Header3"),
Optional(std::string("Value3")));
}
// Tests that the Upgrade If Insecure flag is obeyed.
TEST_F(NavigationURLLoaderImplTest, UpgradeIfInsecureTest) {
ASSERT_TRUE(http_test_server_.Start());
const GURL url = http_test_server_.GetURL("/redirect301-to-http");
GURL expected_url = GURL("http://test.test/test");
// We expect the request to fail since there is no server listening at
// test.test, but for the purpose of this test we only need to validate the
// redirect URL was not changed.
net::RedirectInfo redirect_info = NavigateAndReturnRedirectInfo(
url, false /* upgrade_if_insecure */, true /* expect_request_fail */);
EXPECT_FALSE(redirect_info.insecure_scheme_was_upgraded);
EXPECT_EQ(expected_url, redirect_info.new_url);
GURL::Replacements replacements;
replacements.SetSchemeStr("https");
expected_url = expected_url.ReplaceComponents(replacements);
redirect_info = NavigateAndReturnRedirectInfo(
url, true /* upgrade_if_insecure */, true /* expect_request_fail */);
// Same as above, but validating the URL is upgraded to https.
EXPECT_TRUE(redirect_info.insecure_scheme_was_upgraded);
EXPECT_EQ(expected_url, redirect_info.new_url);
}
// Tests that when a navigation timeout is set and the navigation takes longer
// than that timeout, then the navigation load fails with ERR_TIMED_OUT.
TEST_F(NavigationURLLoaderImplTest, NavigationTimeoutTest) {
ASSERT_TRUE(http_test_server_.Start());
const GURL url = http_test_server_.GetURL("/hung");
TestNavigationURLLoaderDelegate delegate;
auto loader = CreateTestLoader(url, std::string(), "GET", &delegate);
loader->Start();
loader->SetNavigationTimeout(base::Seconds(3));
delegate.WaitForRequestFailed();
EXPECT_EQ(net::ERR_TIMED_OUT, delegate.net_error());
}
// Like NavigationTimeoutTest but the navigation initially results in a redirect
// before hanging, to test a slightly more complicated navigation.
// TODO(crbug.com/40805451): Flaky on Linux.
#if BUILDFLAG(IS_LINUX)
#define MAYBE_NavigationTimeoutRedirectTest \
DISABLED_NavigationTimeoutRedirectTest
#else
#define MAYBE_NavigationTimeoutRedirectTest NavigationTimeoutRedirectTest
#endif
TEST_F(NavigationURLLoaderImplTest, MAYBE_NavigationTimeoutRedirectTest) {
ASSERT_TRUE(http_test_server_.Start());
const GURL hang_url = http_test_server_.GetURL("/hung");
const GURL redirect_url =
http_test_server_.GetURL("/server-redirect?" + hang_url.spec());
TestNavigationURLLoaderDelegate delegate;
auto loader = CreateTestLoader(redirect_url, std::string(), "GET", &delegate);
loader->Start();
loader->SetNavigationTimeout(base::Seconds(3));
delegate.WaitForRequestRedirected();
delegate.WaitForRequestFailed();
EXPECT_EQ(net::ERR_TIMED_OUT, delegate.net_error());
}
// Verify that UKMs are recorded when OnAcceptCHFrameReceived is called.
TEST_F(NavigationURLLoaderImplTest, OnAcceptCHFrameReceivedUKM) {
ASSERT_TRUE(http_test_server_.Start());
const GURL url = http_test_server_.GetURL("/foo");
const url::Origin origin = url::Origin::Create(url);
TestNavigationURLLoaderDelegate delegate;
auto loader = CreateTestLoader(
url,
base::StringPrintf("%s: %s", net::HttpRequestHeaders::kOrigin,
url.DeprecatedGetOriginAsURL().spec().c_str()),
"GET", &delegate, blink::NavigationDownloadPolicy(),
true /*is_main_frame*/, false /*upgrade_if_insecure*/);
loader->Start();
// Try recording no hints.
{
ukm::TestAutoSetUkmRecorder ukm_recorder;
static_cast<NavigationURLLoaderImpl*>(loader.get())
->OnAcceptCHFrameReceived(origin, {},
base::BindOnce([](int32_t) { return; }));
auto ukm_entries = ukm_recorder.GetEntriesByName(
ukm::builders::ClientHints_AcceptCHFrameUsage::kEntryName);
ASSERT_EQ(ukm_entries.size(), 0u);
}
// Try recording one hint.
{
ukm::TestAutoSetUkmRecorder ukm_recorder;
static_cast<NavigationURLLoaderImpl*>(loader.get())
->OnAcceptCHFrameReceived(origin,
{network::mojom::WebClientHintsType::kDpr},
base::BindOnce([](int32_t) { return; }));
auto ukm_entries = ukm_recorder.GetEntriesByName(
ukm::builders::ClientHints_AcceptCHFrameUsage::kEntryName);
ASSERT_EQ(ukm_entries.size(), 1u);
EXPECT_EQ(*ukm_recorder.GetEntryMetric(
ukm_entries[0],
ukm::builders::ClientHints_AcceptCHFrameUsage::kTypeName),
static_cast<int64_t>(network::mojom::WebClientHintsType::kDpr));
}
// Try recording all hints.
{
ukm::TestAutoSetUkmRecorder ukm_recorder;
std::vector<network::mojom::WebClientHintsType> accept_ch_frame;
for (int64_t i = 0; i <= static_cast<int64_t>(
network::mojom::WebClientHintsType::kMaxValue);
++i) {
accept_ch_frame.push_back(
static_cast<network::mojom::WebClientHintsType>(i));
}
static_cast<NavigationURLLoaderImpl*>(loader.get())
->OnAcceptCHFrameReceived(origin, accept_ch_frame,
base::BindOnce([](int32_t) { return; }));
auto ukm_entries = ukm_recorder.GetEntriesByName(
ukm::builders::ClientHints_AcceptCHFrameUsage::kEntryName);
// If you're here because the test is failing when you added a new client
// hint be sure to increment the number below and add your new hint to the
// enum WebClientHintsType in tools/metrics/histograms/enums.xml.
ASSERT_EQ(ukm_entries.size(), 31u);
for (int64_t i = 0; i <= static_cast<int64_t>(
network::mojom::WebClientHintsType::kMaxValue);
++i) {
EXPECT_EQ(*ukm_recorder.GetEntryMetric(
ukm_entries[i],
ukm::builders::ClientHints_AcceptCHFrameUsage::kTypeName),
i);
}
}
}
TEST_F(NavigationURLLoaderImplTest, AdTaggedNavigation) {
ASSERT_TRUE(http_test_server_.Start());
const GURL redirect_url = http_test_server_.GetURL("/foo");
TestNavigationURLLoaderDelegate delegate;
auto loader = CreateTestLoader(redirect_url, "", "GET", &delegate,
blink::NavigationDownloadPolicy(),
/*is_main_frame=*/true,
/*upgrade_if_insecure=*/false,
/*is_ad_tagged=*/true);
loader->Start();
delegate.WaitForResponseStarted();
EXPECT_TRUE(loader->GetResourceRequestForTesting().is_ad_tagged);
}
TEST_F(NavigationURLLoaderImplTest, PopulatePermissionsPolicyOnRequest) {
// TODO(crbug.com/382291442): Remove `scoped_feature_list` once launched.
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndEnableFeature(
network::features::kPopulatePermissionsPolicyOnRequest);
ASSERT_TRUE(http_test_server_.Start());
const GURL url = http_test_server_.GetURL("/foo");
const url::Origin origin = url::Origin::Create(url);
TestNavigationURLLoaderDelegate delegate;
auto loader = CreateTestLoader(
url,
base::StringPrintf("%s: %s", net::HttpRequestHeaders::kOrigin,
url.DeprecatedGetOriginAsURL().spec().c_str()),
"GET", &delegate, blink::NavigationDownloadPolicy(),
/*is_main_frame=*/true, /*upgrade_if_insecure=*/false);
loader->Start();
delegate.WaitForResponseStarted();
EXPECT_EQ(loader->GetResourceRequestForTesting().permissions_policy,
std::make_optional(
*web_contents_->GetPrimaryMainFrame()->GetPermissionsPolicy()));
}
// TODO(crbug.com/382291442): Remove test once feature is launched.
TEST_F(NavigationURLLoaderImplTest,
PopulatePermissionsPolicyOnRequest_FeatureDisabled) {
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndDisableFeature(
network::features::kPopulatePermissionsPolicyOnRequest);
ASSERT_TRUE(http_test_server_.Start());
const GURL url = http_test_server_.GetURL("/foo");
const url::Origin origin = url::Origin::Create(url);
TestNavigationURLLoaderDelegate delegate;
auto loader = CreateTestLoader(
url,
base::StringPrintf("%s: %s", net::HttpRequestHeaders::kOrigin,
url.DeprecatedGetOriginAsURL().spec().c_str()),
"GET", &delegate, blink::NavigationDownloadPolicy(),
/*is_main_frame=*/true, /*upgrade_if_insecure=*/false);
loader->Start();
delegate.WaitForResponseStarted();
EXPECT_FALSE(loader->GetResourceRequestForTesting().permissions_policy);
}
} // namespace content