| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <memory> |
| #include <optional> |
| #include <tuple> |
| #include <vector> |
| |
| #include "base/feature_list.h" |
| #include "base/functional/bind.h" |
| #include "base/location.h" |
| #include "base/run_loop.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/test/allow_check_is_test_for_testing.h" |
| #include "base/test/bind.h" |
| #include "base/test/mock_callback.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "content/browser/attribution_reporting/test/mock_attribution_data_host_manager.h" |
| #include "content/browser/attribution_reporting/test/mock_attribution_manager.h" |
| #include "content/browser/back_forward_cache_test_util.h" |
| #include "content/browser/loader/keep_alive_url_loader.h" |
| #include "content/browser/loader/keep_alive_url_loader_service.h" |
| #include "content/browser/renderer_host/render_frame_host_impl.h" |
| #include "content/browser/storage_partition_impl.h" |
| #include "content/browser/web_contents/web_contents_impl.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/network_service_util.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/storage_partition.h" |
| #include "content/public/test/back_forward_cache_util.h" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "content/public/test/content_browser_test.h" |
| #include "content/public/test/content_browser_test_utils.h" |
| #include "content/public/test/keep_alive_url_loader_utils.h" |
| #include "content/public/test/test_utils.h" |
| #include "content/shell/browser/shell.h" |
| #include "content/test/content_browser_test_utils_internal.h" |
| #include "net/dns/mock_host_resolver.h" |
| #include "net/http/http_request_headers.h" |
| #include "net/http/http_status_code.h" |
| #include "net/test/embedded_test_server/controllable_http_response.h" |
| #include "net/test/embedded_test_server/request_handler_util.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "third_party/blink/public/common/features.h" |
| #include "url/origin.h" |
| #include "url/url_util.h" |
| |
| namespace content { |
| namespace { |
| |
| using testing::Contains; |
| using testing::Pair; |
| |
| constexpr char16_t kPromiseResolvedPageTitle[] = u"Resolved"; |
| |
| constexpr char kPrimaryHost[] = "a.test"; |
| constexpr char kSecondaryHost[] = "b.test"; |
| constexpr char kAllowedCspHost[] = "csp.test"; |
| |
| constexpr char kKeepAliveEndpoint[] = "/beacon"; |
| |
| constexpr char k200TextResponse[] = |
| "HTTP/1.1 200 OK\r\n" |
| "Content-Type: text/html; charset=utf-8\r\n" |
| "\r\n" |
| "Acked!"; |
| |
| constexpr char k301Response[] = |
| "HTTP/1.1 301 Moved Permanently\r\n" |
| "Location: %s\r\n" |
| "\r\n"; |
| |
| constexpr char kBeaconId[] = "beacon01"; |
| |
| constexpr char kFetchLaterEndpoint[] = "/fetch-later"; |
| |
| std::string GetKeepAliveEndpoint(std::optional<std::string> id = std::nullopt) { |
| std::string endpoint = kKeepAliveEndpoint; |
| if (id.has_value()) { |
| endpoint += "?id=" + *id; |
| } |
| return endpoint; |
| } |
| |
| std::string GetConnectSrcCSPHeader(const url::Origin& origin) { |
| return base::StringPrintf("Content-Security-Policy: connect-src 'self' %s", |
| origin.Serialize().c_str()); |
| } |
| |
| // Encodes the given `url` using the JS method encodeURIComponent. |
| std::string EncodeURL(const GURL& url) { |
| url::RawCanonOutputT<char> buffer; |
| url::EncodeURIComponent(url.spec(), &buffer); |
| return std::string(buffer.view()); |
| } |
| |
| MATCHER(IsFrameHidden, |
| base::StrCat({"Frame is", negation ? " not" : "", " hidden"})) { |
| return arg->GetVisibilityState() == PageVisibilityState::kHidden; |
| } |
| |
| } // namespace |
| |
| class KeepAliveURLBrowserTestBase : public ContentBrowserTest { |
| protected: |
| using FeaturesType = std::vector<base::test::FeatureRefAndParams>; |
| using DisabledFeaturesType = std::vector<base::test::FeatureRef>; |
| |
| KeepAliveURLBrowserTestBase() |
| : https_test_server_(std::make_unique<net::EmbeddedTestServer>( |
| net::EmbeddedTestServer::TYPE_HTTPS)) {} |
| ~KeepAliveURLBrowserTestBase() override = default; |
| // Not Copyable. |
| KeepAliveURLBrowserTestBase(const KeepAliveURLBrowserTestBase&) = delete; |
| KeepAliveURLBrowserTestBase& operator=(const KeepAliveURLBrowserTestBase&) = |
| delete; |
| |
| void SetUp() override { |
| feature_list_.InitWithFeaturesAndParameters(GetEnabledFeatures(), |
| GetDisabledFeatures()); |
| ContentBrowserTest::SetUp(); |
| } |
| virtual const FeaturesType& GetEnabledFeatures() = 0; |
| virtual const DisabledFeaturesType& GetDisabledFeatures() { |
| static const DisabledFeaturesType disabled_features = |
| GetDefaultDisabledBackForwardCacheFeaturesForTesting(); |
| return disabled_features; |
| } |
| |
| void SetUpOnMainThread() override { |
| // Support multiple sites on the test server. |
| host_resolver()->AddRule("*", "127.0.0.1"); |
| if (loader_service()) { |
| loaders_observer_ = std::make_unique<KeepAliveURLLoadersTestObserver>( |
| web_contents()->GetBrowserContext()); |
| } |
| |
| // Initialize an HTTPS server. Subclass may choose to use HTTPS by calling |
| // `SetUseHttps()`. |
| https_test_server_->AddDefaultHandlers(GetTestDataFilePath()); |
| https_test_server_->SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES); |
| |
| histogram_tester_ = std::make_unique<base::HistogramTester>(); |
| |
| ContentBrowserTest::SetUpOnMainThread(); |
| } |
| |
| void TearDownOnMainThread() override { |
| histogram_tester_.reset(); |
| ContentBrowserTest::TearDownOnMainThread(); |
| } |
| |
| protected: |
| [[nodiscard]] std::vector< |
| std::unique_ptr<net::test_server::ControllableHttpResponse>> |
| RegisterRequestHandlers(const std::vector<std::string>& relative_urls) { |
| std::vector<std::unique_ptr<net::test_server::ControllableHttpResponse>> |
| handlers; |
| for (const auto& relative_url : relative_urls) { |
| handlers.emplace_back( |
| std::make_unique<net::test_server::ControllableHttpResponse>( |
| server(), relative_url)); |
| } |
| return handlers; |
| } |
| |
| // Returns a cross-origin (kSecondaryHost) URL that causes the following |
| // redirect chain: |
| // http(s)://b.test:<port>/no-cors-server-redirect-307?... |
| // --> http(s)://b.test:<port>/server-redirect-307?... |
| // --> http(s)://b.test:<port>/no-cors-server-redirect-307?... |
| // --> `target_url |
| GURL GetCrossOriginMultipleRedirectsURL(const GURL& target_url) const { |
| const auto intermediate_url2 = server()->GetURL( |
| kSecondaryHost, base::StringPrintf("/no-cors-server-redirect-307?%s", |
| target_url.spec().c_str())); |
| const auto intermediate_url1 = server()->GetURL( |
| kSecondaryHost, base::StringPrintf("/server-redirect-307?%s", |
| intermediate_url2.spec().c_str())); |
| return server()->GetURL( |
| kSecondaryHost, base::StringPrintf("/no-cors-server-redirect-307?%s", |
| intermediate_url1.spec().c_str())); |
| } |
| |
| // Returns a same-origin (kPrimaryHost) URL that causes the following |
| // redirect chain: |
| // http(s)://a.test:<port>/server-redirect-307?... |
| // --> http(s)://a.test:<port>/no-cors-server-redirect-307?... |
| // --> `target_url` |
| GURL GetSameOriginMultipleRedirectsURL(const GURL& target_url) const { |
| const auto intermediate_url1 = server()->GetURL( |
| kPrimaryHost, base::StringPrintf("/no-cors-server-redirect-307?%s", |
| target_url.spec().c_str())); |
| return server()->GetURL( |
| kPrimaryHost, base::StringPrintf("/server-redirect-307?%s", |
| intermediate_url1.spec().c_str())); |
| } |
| |
| // Returns a same-origin (kPrimaryHost) URL that leads to cross-origin |
| // redirect chain: |
| // http(s)://a.test:<port>/server-redirect-307?... |
| // --> http(s)://b.test:<port>/no-cors-server-redirect-307?... |
| // --> `target_url` |
| GURL GetSameAndCrossOriginRedirectsURL(const GURL& target_url) const { |
| const auto intermediate_url1 = server()->GetURL( |
| kSecondaryHost, base::StringPrintf("/no-cors-server-redirect-307?%s", |
| target_url.spec().c_str())); |
| return server()->GetURL( |
| kPrimaryHost, base::StringPrintf("/server-redirect-307?%s", |
| intermediate_url1.spec().c_str())); |
| } |
| |
| // Returns a same-origin (kPrimaryHost) URL that redirects to `target_url`: |
| // http(s)://a.test:<port>/server-redirect-307?... |
| // --> `target_url` |
| GURL GetSameOriginRedirectURL(const GURL& target_url) const { |
| return server()->GetURL(kPrimaryHost, |
| base::StringPrintf("/server-redirect-307?%s", |
| target_url.spec().c_str())); |
| } |
| |
| using FetchKeepAliveRequestMetricType = |
| KeepAliveURLLoader::FetchKeepAliveRequestMetricType; |
| |
| // Note: `renderer` is made optional to support the use cases where its |
| // loggings happen after unloading a renderer, as the browser process might |
| // not be able to fetch UMA logged by renderer in time before the latter is |
| // shutting down, as described in https://crbug.com/40109064. |
| // It should be made required once the bug is resolved. |
| struct ExpectedRequestHistogram { |
| int browser = 0; |
| std::optional<int> renderer = std::nullopt; |
| }; |
| using ExpectedTotalRequests = ExpectedRequestHistogram; |
| using ExpectedStartedRequests = ExpectedRequestHistogram; |
| using ExpectedSucceededRequests = ExpectedRequestHistogram; |
| using ExpectedFailedRequests = ExpectedRequestHistogram; |
| |
| // A helper to assert on the number of UMA logged from both browser and |
| // renderer processes. |
| void ExpectFetchKeepAliveHistogram( |
| const FetchKeepAliveRequestMetricType& expected_sample, |
| const ExpectedTotalRequests& total, |
| const ExpectedStartedRequests& started_count, |
| const ExpectedSucceededRequests& succeeded_count, |
| const ExpectedFailedRequests& failed_count) { |
| // Collect metrics recorded in the renderer processes, if expecting any. |
| for (size_t retries = 0; |
| retries < 20 && |
| ((total.renderer.has_value() && *total.renderer > 0 && |
| histogram_tester() |
| .GetAllSamples("FetchKeepAlive.Requests2.Total.Renderer") |
| .empty()) || |
| (started_count.renderer.has_value() && *started_count.renderer > 0 && |
| histogram_tester() |
| .GetAllSamples("FetchKeepAlive.Requests2.Started.Renderer") |
| .empty()) || |
| (succeeded_count.renderer.has_value() && |
| *succeeded_count.renderer > 0 && |
| histogram_tester() |
| .GetAllSamples("FetchKeepAlive.Requests2.Succeeded.Renderer") |
| .empty()) || |
| (failed_count.renderer.has_value() && *failed_count.renderer > 0 && |
| histogram_tester() |
| .GetAllSamples("FetchKeepAlive.Requests2.Failed.Renderer") |
| .empty())); |
| retries++) { |
| FetchHistogramsFromChildProcesses(); |
| } |
| |
| const int renderer_sample = static_cast<int>(expected_sample); |
| const int browser_sample = |
| (expected_sample == FetchKeepAliveRequestMetricType::kBeacon || |
| expected_sample == FetchKeepAliveRequestMetricType::kPing || |
| expected_sample == FetchKeepAliveRequestMetricType::kAttribution) |
| ? static_cast<int>(FetchKeepAliveRequestMetricType::kPing) |
| : static_cast<int>(expected_sample); |
| histogram_tester().ExpectUniqueSample( |
| "FetchKeepAlive.Requests2.Total.Browser", browser_sample, |
| total.browser); |
| if (total.renderer.has_value()) { |
| histogram_tester().ExpectUniqueSample( |
| "FetchKeepAlive.Requests2.Total.Renderer", renderer_sample, |
| *total.renderer); |
| } |
| histogram_tester().ExpectUniqueSample( |
| "FetchKeepAlive.Requests2.Started.Browser", browser_sample, |
| started_count.browser); |
| if (started_count.renderer.has_value()) { |
| histogram_tester().ExpectUniqueSample( |
| "FetchKeepAlive.Requests2.Started.Renderer", renderer_sample, |
| *started_count.renderer); |
| } |
| histogram_tester().ExpectUniqueSample( |
| "FetchKeepAlive.Requests2.Succeeded.Browser", browser_sample, |
| succeeded_count.browser); |
| if (succeeded_count.renderer.has_value()) { |
| histogram_tester().ExpectUniqueSample( |
| "FetchKeepAlive.Requests2.Succeeded.Renderer", renderer_sample, |
| *succeeded_count.renderer); |
| } |
| histogram_tester().ExpectUniqueSample( |
| "FetchKeepAlive.Requests2.Failed.Browser", browser_sample, |
| failed_count.browser); |
| if (failed_count.renderer.has_value()) { |
| histogram_tester().ExpectUniqueSample( |
| "FetchKeepAlive.Requests2.Failed.Renderer", renderer_sample, |
| *failed_count.renderer); |
| } |
| } |
| |
| WebContentsImpl* web_contents() const { |
| return static_cast<WebContentsImpl*>(shell()->web_contents()); |
| } |
| RenderFrameHostImpl* current_frame_host() { |
| return web_contents()->GetPrimaryFrameTree().root()->current_frame_host(); |
| } |
| KeepAliveURLLoaderService* loader_service() { |
| return static_cast<StoragePartitionImpl*>( |
| web_contents() |
| ->GetBrowserContext() |
| ->GetDefaultStoragePartition()) |
| ->GetKeepAliveURLLoaderService(); |
| } |
| |
| void DisableBackForwardCache(WebContents* web_contents) { |
| DisableBackForwardCacheForTesting( |
| web_contents, BackForwardCache::TEST_REQUIRES_NO_CACHING); |
| } |
| |
| KeepAliveURLLoadersTestObserver& loaders_observer() { |
| return *loaders_observer_; |
| } |
| void SetUseHttps() { use_https_ = true; } |
| net::EmbeddedTestServer* server() { |
| return use_https_ ? https_test_server_.get() : embedded_test_server(); |
| } |
| const net::EmbeddedTestServer* server() const { |
| return use_https_ ? https_test_server_.get() : embedded_test_server(); |
| } |
| |
| const base::HistogramTester& histogram_tester() { return *histogram_tester_; } |
| |
| private: |
| base::test::ScopedFeatureList feature_list_; |
| std::unique_ptr<KeepAliveURLLoadersTestObserver> loaders_observer_; |
| bool use_https_ = false; |
| const std::unique_ptr<net::EmbeddedTestServer> https_test_server_; |
| std::unique_ptr<base::HistogramTester> histogram_tester_; |
| }; |
| |
| class FetchKeepAliveCommonTestBase : public KeepAliveURLBrowserTestBase { |
| protected: |
| // Navigates to a page specified by `keepalive_page_url`, which must fire a |
| // fetch keepalive request. |
| // The method then postpones the request handling until RFH of the page is |
| // fully unloaded (by navigating to another cross-origin page). |
| // After that, `response` will be sent back. |
| // `keepalive_request_handler` must handle the fetch keepalive request. |
| void LoadPageWithKeepAliveRequestAndSendResponseAfterUnload( |
| const GURL& keepalive_page_url, |
| net::test_server::ControllableHttpResponse* keepalive_request_handler, |
| const std::string& response) { |
| ASSERT_TRUE(NavigateToURL(web_contents(), keepalive_page_url)); |
| RenderFrameHostImplWrapper rfh_1(current_frame_host()); |
| // Ensure the current page can be unloaded instead of being cached. |
| DisableBackForwardCache(web_contents()); |
| // Ensure the keepalive request is sent before leaving the current page. |
| keepalive_request_handler->WaitForRequest(); |
| if (loader_service()) { |
| ASSERT_EQ(loader_service()->NumLoadersForTesting(), 1u); |
| } |
| // Collects any potential histogram before the process is gone. |
| FetchHistogramsFromChildProcesses(); |
| |
| // Navigate to cross-origin page to ensure the 1st page can be unloaded. |
| ASSERT_TRUE(NavigateToURL(web_contents(), GetCrossOriginPageURL())); |
| ASSERT_NE(current_frame_host(), rfh_1.get()); |
| // Ensure the 1st page has been unloaded. |
| ASSERT_TRUE(rfh_1.WaitUntilRenderFrameDeleted()); |
| if (loader_service()) { |
| // Ensure there is still a loader pending to receive response. |
| ASSERT_EQ(loader_service()->NumLoadersForTesting(), 1u); |
| // While the 1st page is unloaded, the disconnection may not propagate to |
| // browser process in time, such that assertion on the number of |
| // disconnected loader here might become flaky. |
| } |
| |
| // Send back response to terminate in-browser request handling for the |
| // pending request from 1st page. |
| keepalive_request_handler->Send(response); |
| keepalive_request_handler->Done(); |
| } |
| |
| // Navigates to a page specified by `keepalive_page_url`, which must fire a |
| // fetch keepalive request. |
| // This method ensure request handling happens. After that, `response` will be |
| // sent back. |
| // `keepalive_request_handler` must handle the fetch keepalive request. |
| void LoadPageWithKeepAliveRequestAndSendResponse( |
| const GURL& keepalive_page_url, |
| net::test_server::ControllableHttpResponse* keepalive_request_handler, |
| const std::string& response) { |
| ASSERT_TRUE(NavigateToURL(web_contents(), keepalive_page_url)); |
| RenderFrameHostImplWrapper rfh_1(current_frame_host()); |
| // Ensure the keepalive request is sent. |
| keepalive_request_handler->WaitForRequest(); |
| ASSERT_EQ(loader_service()->NumLoadersForTesting(), 1u); |
| ASSERT_EQ(loader_service()->NumDisconnectedLoadersForTesting(), 0u); |
| |
| // Send back response to terminate in-browser request handling. |
| keepalive_request_handler->Send(response); |
| keepalive_request_handler->Done(); |
| } |
| |
| GURL GetKeepAlivePageURL( |
| const std::string& method, |
| size_t num_requests = 1, |
| std::optional<std::string> headers = std::nullopt) const { |
| std::string url = base::StringPrintf( |
| "/set-header-with-file/content/test/data/fetch-keepalive.html?" |
| "method=%s&requests=%zu", |
| method.c_str(), num_requests); |
| if (headers.has_value()) { |
| url += "&" + *headers; |
| } |
| return server()->GetURL(kPrimaryHost, url); |
| } |
| |
| GURL GetCrossOriginPageURL() { |
| return server()->GetURL(kSecondaryHost, "/title2.html"); |
| } |
| }; |
| |
| // Contains the integration tests for loading fetch(url, {keepalive: true}) |
| // requests via browser process that are difficult to reliably reproduce in web |
| // tests. |
| // |
| // Note that due to using different approach, tests to cover implementation |
| // before `kKeepAliveInBrowserMigration`, i.e. loading via delaying renderer |
| // shutdown, cannot be verified with inspecting KeepAliveURLLoaderService here |
| // and still live in a different file |
| // content/browser/renderer_host/render_process_host_browsertest.cc |
| class KeepAliveURLBrowserTest |
| : public FetchKeepAliveCommonTestBase, |
| public ::testing::WithParamInterface<std::string> { |
| protected: |
| const FeaturesType& GetEnabledFeatures() override { |
| static const FeaturesType enabled_features = |
| GetDefaultEnabledBackForwardCacheFeaturesForTesting( |
| {{blink::features::kKeepAliveInBrowserMigration, {}}}); |
| return enabled_features; |
| } |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P( |
| All, |
| KeepAliveURLBrowserTest, |
| ::testing::Values(net::HttpRequestHeaders::kGetMethod, |
| net::HttpRequestHeaders::kPostMethod), |
| [](const testing::TestParamInfo<KeepAliveURLBrowserTest::ParamType>& info) { |
| return info.param; |
| }); |
| |
| IN_PROC_BROWSER_TEST_P(KeepAliveURLBrowserTest, OneRequest) { |
| const std::string method = GetParam(); |
| auto request_handler = |
| std::move(RegisterRequestHandlers({kKeepAliveEndpoint})[0]); |
| ASSERT_TRUE(server()->Start()); |
| |
| ASSERT_TRUE(NavigateToURL(web_contents(), GetKeepAlivePageURL(method))); |
| // Ensure the keepalive request is sent, but delay response. |
| request_handler->WaitForRequest(); |
| ASSERT_EQ(loader_service()->NumLoadersForTesting(), 1u); |
| |
| // End the keepalive request by sending back response. |
| request_handler->Send(k200TextResponse); |
| request_handler->Done(); |
| |
| TitleWatcher watcher(web_contents(), kPromiseResolvedPageTitle); |
| EXPECT_EQ(watcher.WaitAndGetTitle(), kPromiseResolvedPageTitle); |
| loaders_observer().WaitForTotalOnReceiveResponseForwarded(1); |
| loaders_observer().WaitForTotalOnCompleteForwarded({net::OK}); |
| EXPECT_EQ(loader_service()->NumLoadersForTesting(), 0u); |
| ExpectFetchKeepAliveHistogram( |
| FetchKeepAliveRequestMetricType::kFetch, |
| ExpectedTotalRequests(/*browser=*/1, /*renderer=*/1), |
| ExpectedStartedRequests(/*browser=*/1, /*renderer=*/1), |
| ExpectedSucceededRequests(/*browser=*/1, /*renderer=*/1), |
| ExpectedFailedRequests(/*browser=*/0, /*renderer=*/0)); |
| } |
| |
| // Verify keepalive request loading works given 2 concurrent requests to the |
| // same host. |
| // |
| // Note: Chromium allows at most 6 concurrent connections to the same host under |
| // HTTP 1.1 protocol, which `server()` uses by default. |
| // Exceeding this limit will hang the browser. |
| // TODO(crbug.com/40262244): Flaky on Fuchsia and Android. |
| IN_PROC_BROWSER_TEST_P(KeepAliveURLBrowserTest, |
| DISABLED_TwoConcurrentRequestsPerHost) { |
| const std::string method = GetParam(); |
| const size_t num_requests = 2; |
| auto request_handlers = |
| RegisterRequestHandlers({kKeepAliveEndpoint, kKeepAliveEndpoint}); |
| ASSERT_TRUE(server()->Start()); |
| |
| ASSERT_TRUE( |
| NavigateToURL(web_contents(), GetKeepAlivePageURL(method, num_requests))); |
| // Ensure all keepalive requests are sent, but delay responses. |
| request_handlers[0]->WaitForRequest(); |
| request_handlers[1]->WaitForRequest(); |
| ASSERT_EQ(loader_service()->NumLoadersForTesting(), num_requests); |
| |
| // End the keepalive request by sending back responses. |
| request_handlers[0]->Send(k200TextResponse); |
| request_handlers[1]->Send(k200TextResponse); |
| request_handlers[0]->Done(); |
| request_handlers[1]->Done(); |
| |
| TitleWatcher watcher(web_contents(), kPromiseResolvedPageTitle); |
| EXPECT_EQ(watcher.WaitAndGetTitle(), kPromiseResolvedPageTitle); |
| loaders_observer().WaitForTotalOnReceiveResponseForwarded(2); |
| loaders_observer().WaitForTotalOnCompleteForwarded({net::OK, net::OK}); |
| EXPECT_EQ(loader_service()->NumLoadersForTesting(), 0u); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(KeepAliveURLBrowserTest, RequestWithCookie) { |
| const std::string cookie = "keepaliveTestCookie=testCookieValue"; |
| const std::string method = GetParam(); |
| auto request_handler = |
| std::move(RegisterRequestHandlers({kKeepAliveEndpoint})[0]); |
| ASSERT_TRUE(server()->Start()); |
| |
| // Navigate to an empty page first without making any requests. |
| ASSERT_TRUE(NavigateToURL(web_contents(), server()->GetURL("/empty.html"))); |
| // Make a fetch keepalive request, expected to succeed. |
| ASSERT_TRUE(ExecJs(web_contents(), |
| JsReplace(R"( |
| document.cookie = $1 + '; path=/'; |
| fetch($2, {keepalive: true, method: $3}); |
| )", |
| cookie, kKeepAliveEndpoint, method), |
| content::EXECUTE_SCRIPT_NO_RESOLVE_PROMISES)); |
| |
| // Ensure the keepalive request is sent, but delay response. |
| request_handler->WaitForRequest(); |
| ASSERT_EQ(loader_service()->NumLoadersForTesting(), 1u); |
| // End the keepalive request by sending back response. |
| request_handler->Send(k200TextResponse); |
| request_handler->Done(); |
| |
| loaders_observer().WaitForTotalOnReceiveResponseForwarded(1); |
| loaders_observer().WaitForTotalOnCompleteForwarded({net::OK}); |
| EXPECT_EQ(loader_service()->NumLoadersForTesting(), 0u); |
| // Expect the request to contain the cookie. |
| EXPECT_THAT(request_handler->http_request()->headers, |
| Contains(Pair(net::HttpRequestHeaders::kCookie, cookie))); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(KeepAliveURLBrowserTest, |
| RequestAfterNetworkServiceCrashes) { |
| // Can't test this on bots that use an in-process network service. |
| if (IsInProcessNetworkService()) { |
| return; |
| } |
| |
| const std::string method = GetParam(); |
| auto request_handler = |
| std::move(RegisterRequestHandlers({kKeepAliveEndpoint})[0]); |
| ASSERT_TRUE(server()->Start()); |
| |
| // Navigate to an empty page first without making any requests. |
| ASSERT_TRUE(NavigateToURL(web_contents(), server()->GetURL("/empty.html"))); |
| // Crash the NetworkService process on the page. |
| SimulateNetworkServiceCrash(); |
| // Make a fetch keepalive request, expected to succeed. |
| ASSERT_TRUE(ExecJs(web_contents(), |
| JsReplace(R"( |
| fetch($1, {keepalive: true, method: $2}); |
| )", |
| kKeepAliveEndpoint, method), |
| content::EXECUTE_SCRIPT_NO_RESOLVE_PROMISES)); |
| |
| // Ensure the keepalive request is sent, but delay response. |
| request_handler->WaitForRequest(); |
| ASSERT_EQ(loader_service()->NumLoadersForTesting(), 1u); |
| // End the keepalive request by sending back response. |
| request_handler->Send(k200TextResponse); |
| request_handler->Done(); |
| |
| loaders_observer().WaitForTotalOnReceiveResponseForwarded(1); |
| loaders_observer().WaitForTotalOnCompleteForwarded({net::OK}); |
| EXPECT_EQ(loader_service()->NumLoadersForTesting(), 0u); |
| } |
| |
| // TODO(crbug.com/40236167): Re-enable this test on Mac. |
| #if BUILDFLAG(IS_MAC) |
| #define MAYBE_ReceiveResponseAfterPageUnload \ |
| DISABLED_ReceiveResponseAfterPageUnload |
| #else |
| #define MAYBE_ReceiveResponseAfterPageUnload ReceiveResponseAfterPageUnload |
| #endif |
| // Delays response to a keepalive ping until after the page making the keepalive |
| // ping has been unloaded. The browser must ensure the response is received and |
| // processed by the browser. |
| IN_PROC_BROWSER_TEST_P(KeepAliveURLBrowserTest, |
| MAYBE_ReceiveResponseAfterPageUnload) { |
| const std::string method = GetParam(); |
| auto request_handler = |
| std::move(RegisterRequestHandlers({kKeepAliveEndpoint})[0]); |
| ASSERT_TRUE(server()->Start()); |
| |
| ASSERT_NO_FATAL_FAILURE( |
| LoadPageWithKeepAliveRequestAndSendResponseAfterUnload( |
| GetKeepAlivePageURL(method), request_handler.get(), |
| k200TextResponse)); |
| |
| // The response should be processed in browser. |
| loaders_observer().WaitForTotalOnReceiveResponseProcessed(1); |
| // `KeepAliveURLLoader::OnComplete` may not be called, as renderer is dead. |
| EXPECT_EQ(loader_service()->NumDisconnectedLoadersForTesting(), 0u); |
| ExpectFetchKeepAliveHistogram( |
| FetchKeepAliveRequestMetricType::kFetch, |
| ExpectedTotalRequests(/*browser=*/1, /*renderer=*/1), |
| ExpectedStartedRequests(/*browser=*/1, /*renderer=*/1), |
| ExpectedSucceededRequests(/*browser=*/1, /*renderer=*/0), |
| ExpectedFailedRequests(/*browser=*/0, /*renderer=*/0)); |
| } |
| |
| // Delays response to a keepalive ping until after the page making the keepalive |
| // ping is put into BackForwardCache. The response should be processed by the |
| // renderer after the page is restored from BackForwardCache. |
| IN_PROC_BROWSER_TEST_P(KeepAliveURLBrowserTest, |
| ReceiveResponseInBackForwardCache) { |
| const std::string method = GetParam(); |
| auto request_handler = |
| std::move(RegisterRequestHandlers({kKeepAliveEndpoint})[0]); |
| ASSERT_TRUE(server()->Start()); |
| |
| ASSERT_TRUE(NavigateToURL(web_contents(), GetKeepAlivePageURL(method))); |
| RenderFrameHostImplWrapper rfh_1(current_frame_host()); |
| // Ensure the keepalive request is sent before leaving the current page. |
| request_handler->WaitForRequest(); |
| ASSERT_EQ(loader_service()->NumLoadersForTesting(), 1u); |
| // Collects any potential histogram before the process is gone. |
| FetchHistogramsFromChildProcesses(); |
| |
| // Navigate to cross-origin page. |
| ASSERT_TRUE(NavigateToURL(web_contents(), GetCrossOriginPageURL())); |
| // Ensure the previous page has been put into BackForwardCache. |
| ASSERT_EQ(rfh_1->GetLifecycleState(), |
| RenderFrameHost::LifecycleState::kInBackForwardCache); |
| // The loader is still pending to receive response. |
| ASSERT_EQ(loader_service()->NumLoadersForTesting(), 1u); |
| ASSERT_EQ(loader_service()->NumDisconnectedLoadersForTesting(), 0u); |
| |
| // Send back response. |
| request_handler->Send(k200TextResponse); |
| // The response is immediately forwarded to the in-BackForwardCache renderer. |
| loaders_observer().WaitForTotalOnReceiveResponseForwarded(1); |
| // Go back to `rfh_1`. |
| ASSERT_TRUE(HistoryGoBack(web_contents())); |
| |
| // The response should be processed in renderer. Hence resolving Promise. |
| TitleWatcher watcher(web_contents(), kPromiseResolvedPageTitle); |
| EXPECT_EQ(watcher.WaitAndGetTitle(), kPromiseResolvedPageTitle); |
| request_handler->Done(); |
| loaders_observer().WaitForTotalOnCompleteForwarded({net::OK}); |
| EXPECT_EQ(loader_service()->NumLoadersForTesting(), 0u); |
| ExpectFetchKeepAliveHistogram( |
| FetchKeepAliveRequestMetricType::kFetch, |
| ExpectedTotalRequests(/*browser=*/1, /*renderer=*/1), |
| ExpectedStartedRequests(/*browser=*/1, /*renderer=*/1), |
| ExpectedSucceededRequests(/*browser=*/1, /*renderer=*/1), |
| ExpectedFailedRequests(/*browser=*/0, /*renderer=*/0)); |
| } |
| |
| // Tests fetch(..., {keepalive: true}) with a cross-origin & CORS-safelisted |
| // request that causes a redirect chain of 4 URLs. |
| // |
| // As the mode is set to "no-cors" for CORS-safelisted requests, the redirect is |
| // processed without an error while the request is cross-origin. |
| IN_PROC_BROWSER_TEST_P(KeepAliveURLBrowserTest, MultipleRedirectsRequest) { |
| const auto beacon_endpoint = |
| base::StringPrintf("%s?id=%s", kKeepAliveEndpoint, kBeaconId); |
| auto request_handler = |
| std::move(RegisterRequestHandlers({beacon_endpoint})[0]); |
| ASSERT_TRUE(server()->Start()); |
| |
| // Set up a cross-origin (kSecondaryHost) URL with CORS-safelisted |
| // payload that causes multiple redirects and eventually points to a |
| // cross-origin `target_url`: |
| // |
| // http://b.test:<port>/no-cors-server-redirect-307?... |
| // --> http://b.test:<port>/server-redirect-307?... |
| // --> http://b.test:<port>/no-cors-server-redirect-307?... |
| // --> `target_url |
| const auto target_url = server()->GetURL(kSecondaryHost, beacon_endpoint); |
| const auto beacon_url = GetCrossOriginMultipleRedirectsURL(target_url); |
| |
| // Navigate to a page that calls fetch() API and verify its response. |
| ASSERT_TRUE(NavigateToURL(web_contents(), |
| server()->GetURL(kPrimaryHost, "/title1.html"))); |
| |
| ASSERT_TRUE(ExecJs(web_contents(), |
| JsReplace(R"( |
| fetch($1, {keepalive: true, mode: 'no-cors'}); |
| )", |
| beacon_url), |
| content::EXECUTE_SCRIPT_NO_RESOLVE_PROMISES)); |
| ASSERT_EQ(loader_service()->NumLoadersForTesting(), 1u); |
| |
| // The in-browser logic should handle all redirects in browser first. |
| loaders_observer().WaitForTotalOnReceiveRedirectProcessed(3); |
| // After in-browser processing, the loader should remain alive to support |
| // forwarding stored redirects/response to renderer. |
| ASSERT_EQ(loader_service()->NumDisconnectedLoadersForTesting(), 0u); |
| EXPECT_EQ(loader_service()->NumLoadersForTesting(), 1u); |
| |
| // Ensure the fetch request is sent. |
| request_handler->WaitForRequest(); |
| // Send back response to terminate in-browser request handling. |
| request_handler->Send(k200TextResponse); |
| request_handler->Done(); |
| |
| // All redirects and the response should be forwarded to renderer. |
| loaders_observer().WaitForTotalOnReceiveRedirectForwarded(3); |
| loaders_observer().WaitForTotalOnReceiveResponseForwarded(1); |
| loaders_observer().WaitForTotalOnCompleteForwarded({net::OK}); |
| // After forwarding, the loader should all be gone. |
| EXPECT_EQ(loader_service()->NumDisconnectedLoadersForTesting(), 0u); |
| EXPECT_EQ(loader_service()->NumLoadersForTesting(), 0u); |
| ExpectFetchKeepAliveHistogram( |
| FetchKeepAliveRequestMetricType::kFetch, |
| ExpectedTotalRequests(/*browser=*/1, /*renderer=*/1), |
| ExpectedStartedRequests(/*browser=*/1, /*renderer=*/1), |
| ExpectedSucceededRequests(/*browser=*/1, /*renderer=*/1), |
| ExpectedFailedRequests(/*browser=*/0, /*renderer=*/0)); |
| } |
| |
| // Tests fetch(..., {keepalive: true}) with a cross-origin & CORS-safelisted |
| // request that causes a redirect chain of 3 URLs, where the cross-origin URLs |
| // are the 2nd URL & the 3rd URL in the chain. |
| // |
| // As the mode is set to "cors" for CORS-safelisted requests, the redirect will |
| // fail at the first cross-origin URL. |
| IN_PROC_BROWSER_TEST_P(KeepAliveURLBrowserTest, |
| MultipleRedirectsAndFailInBetweenRequest) { |
| const auto beacon_endpoint = |
| base::StringPrintf("%s?id=%s", kKeepAliveEndpoint, kBeaconId); |
| ASSERT_TRUE(server()->Start()); |
| |
| // Set up a same-origin URL with CORS-safelisted payload that causes multiple |
| // redirects and eventually points to a cross-origin `target_url`: |
| // |
| // http://a.test:<port>/server-redirect-307?... |
| // --> http://b.test:<port>/no-cors-server-redirect-307?... => should fail |
| // --> `target_url => should not reach here |
| const auto target_url = server()->GetURL(kSecondaryHost, beacon_endpoint); |
| const auto beacon_url = GetSameAndCrossOriginRedirectsURL(target_url); |
| |
| // Navigate to a page that calls fetch() API and verify its response. |
| ASSERT_TRUE(NavigateToURL(web_contents(), |
| server()->GetURL(kPrimaryHost, "/title1.html"))); |
| ASSERT_TRUE(ExecJs(web_contents(), |
| JsReplace(R"( |
| fetch($1, {keepalive: true, mode: 'cors'}); |
| )", |
| beacon_url), |
| content::EXECUTE_SCRIPT_NO_RESOLVE_PROMISES)); |
| // TODO(crbug.com/40236167): Figure out how to reliably wait for # loaders. |
| |
| // The in-browser logic should handle all redirects in browser first. |
| loaders_observer().WaitForTotalOnReceiveRedirectProcessed(1); |
| // After in-browser processing, the loader should remain alive to support |
| // forwarding stored redirects/response to renderer. |
| ASSERT_EQ(loader_service()->NumDisconnectedLoadersForTesting(), 0u); |
| EXPECT_EQ(loader_service()->NumLoadersForTesting(), 1u); |
| |
| // No request will be sent to kKeepAliveEndpoint, as it fails at the 2nd URL. |
| |
| // All redirects should be forwarded to renderer. |
| loaders_observer().WaitForTotalOnReceiveRedirectForwarded(1); |
| loaders_observer().WaitForTotalOnCompleteForwarded({net::ERR_FAILED}); |
| // After forwarding, the loader should all be gone. |
| EXPECT_EQ(loader_service()->NumDisconnectedLoadersForTesting(), 0u); |
| EXPECT_EQ(loader_service()->NumLoadersForTesting(), 0u); |
| ExpectFetchKeepAliveHistogram( |
| FetchKeepAliveRequestMetricType::kFetch, |
| ExpectedTotalRequests(/*browser=*/1, /*renderer=*/1), |
| ExpectedStartedRequests(/*browser=*/1, /*renderer=*/1), |
| ExpectedSucceededRequests(/*browser=*/0, /*renderer=*/0), |
| ExpectedFailedRequests(/*browser=*/1, /*renderer=*/1)); |
| } |
| |
| // Tests fetch(..., {keepalive: true}) with a cross-origin & CORS-safelisted |
| // request that causes a redirect chain of 3 URLs, where the cross-origin URL |
| // is the target URL (3rd URL in the chain). |
| // |
| // As the mode is set to "cors" for CORS-safelisted requests, the redirect will |
| // fail at the first cross-origin URL. |
| IN_PROC_BROWSER_TEST_P(KeepAliveURLBrowserTest, |
| MultipleRedirectsAndFailAtLastRequest) { |
| const auto beacon_endpoint = |
| base::StringPrintf("%s?id=%s", kKeepAliveEndpoint, kBeaconId); |
| auto request_handler = |
| std::move(RegisterRequestHandlers({beacon_endpoint})[0]); |
| ASSERT_TRUE(server()->Start()); |
| |
| // Set up a same-origin URL with CORS-safelisted payload that causes multiple |
| // redirects and eventually points to a cross-origin `target_url`: |
| // |
| // http://a.test:<port>/server-redirect-307?... |
| // --> http://a.test:<port>/no-cors-server-redirect-307?... |
| // --> `target_url => should fail to get response |
| const auto target_url = server()->GetURL(kSecondaryHost, beacon_endpoint); |
| const auto beacon_url = GetSameOriginMultipleRedirectsURL(target_url); |
| |
| // Navigate to a page that calls fetch() API and verify its response. |
| ASSERT_TRUE(NavigateToURL(web_contents(), |
| server()->GetURL(kPrimaryHost, "/title1.html"))); |
| ASSERT_TRUE(ExecJs(web_contents(), |
| JsReplace(R"( |
| fetch($1, {keepalive: true, mode: 'cors'}); |
| )", |
| beacon_url), |
| content::EXECUTE_SCRIPT_NO_RESOLVE_PROMISES)); |
| ASSERT_EQ(loader_service()->NumLoadersForTesting(), 1u); |
| |
| // The in-browser logic should handle all redirects in browser first. |
| loaders_observer().WaitForTotalOnReceiveRedirectProcessed(2); |
| // After in-browser processing, the loader should remain alive to support |
| // forwarding stored redirects/response to renderer. |
| ASSERT_EQ(loader_service()->NumDisconnectedLoadersForTesting(), 0u); |
| EXPECT_EQ(loader_service()->NumLoadersForTesting(), 1u); |
| |
| // No request will be sent to kKeepAliveEndpoint, as it fails at the 2nd URL. |
| // The redirect request should be processed in browser and gets sent. |
| request_handler->WaitForRequest(); |
| // End the keepalive request by sending back final response. |
| request_handler->Send(k200TextResponse); |
| request_handler->Done(); |
| |
| // All redirects should be forwarded to renderer. |
| loaders_observer().WaitForTotalOnReceiveRedirectForwarded(2); |
| loaders_observer().WaitForTotalOnCompleteForwarded({net::ERR_FAILED}); |
| // After forwarding, the loader should all be gone. |
| EXPECT_EQ(loader_service()->NumDisconnectedLoadersForTesting(), 0u); |
| EXPECT_EQ(loader_service()->NumLoadersForTesting(), 0u); |
| ExpectFetchKeepAliveHistogram( |
| FetchKeepAliveRequestMetricType::kFetch, |
| ExpectedTotalRequests(/*browser=*/1, /*renderer=*/1), |
| ExpectedStartedRequests(/*browser=*/1, /*renderer=*/1), |
| ExpectedSucceededRequests(/*browser=*/0, /*renderer=*/0), |
| ExpectedFailedRequests(/*browser=*/1, /*renderer=*/1)); |
| } |
| |
| // Delays handling redirect for a keepalive ping until after the page making the |
| // keepalive ping has been unloaded. The browser must ensure the redirect is |
| // verified and properly processed by the browser. |
| IN_PROC_BROWSER_TEST_P(KeepAliveURLBrowserTest, |
| ReceiveRedirectAfterPageUnload) { |
| const std::string method = GetParam(); |
| const char redirect_target[] = "/beacon-redirected"; |
| auto request_handlers = |
| RegisterRequestHandlers({kKeepAliveEndpoint, redirect_target}); |
| ASSERT_TRUE(server()->Start()); |
| |
| // Sets up redirects according to the following redirect chain: |
| // fetch("http://a.test:<port>/beacon", keepalive: true) |
| // --> http://a.test:<port>/beacon-redirected |
| ASSERT_NO_FATAL_FAILURE( |
| LoadPageWithKeepAliveRequestAndSendResponseAfterUnload( |
| GetKeepAlivePageURL(method), request_handlers[0].get(), |
| base::StringPrintf(k301Response, redirect_target))); |
| |
| // The in-browser logic should process the redirect. |
| loaders_observer().WaitForTotalOnReceiveRedirectProcessed(1); |
| |
| // The redirect request should be processed in browser and gets sent. |
| request_handlers[1]->WaitForRequest(); |
| // End the keepalive request by sending back final response. |
| request_handlers[1]->Send(k200TextResponse); |
| request_handlers[1]->Done(); |
| |
| // The response should be processed in browser. |
| loaders_observer().WaitForTotalOnReceiveResponseProcessed(1); |
| // `KeepAliveURLLoader::OnComplete` will not be called but the loader must |
| // still be terminated, as renderer is dead. |
| EXPECT_EQ(loader_service()->NumDisconnectedLoadersForTesting(), 0u); |
| ExpectFetchKeepAliveHistogram( |
| FetchKeepAliveRequestMetricType::kFetch, |
| ExpectedTotalRequests(/*browser=*/1, /*renderer=*/1), |
| ExpectedStartedRequests(/*browser=*/1, /*renderer=*/1), |
| ExpectedSucceededRequests(/*browser=*/1, /*renderer=*/0), |
| ExpectedFailedRequests(/*browser=*/0, /*renderer=*/0)); |
| } |
| |
| // Delays handling an unsafe redirect for a keepalive ping until after the page |
| // making the keepalive ping has been unloaded. |
| // The browser must ensure the unsafe redirect is not followed. |
| IN_PROC_BROWSER_TEST_P(KeepAliveURLBrowserTest, |
| ReceiveUnSafeRedirectAfterPageUnload) { |
| const std::string method = GetParam(); |
| const char unsafe_redirect_target[] = "chrome://settings"; |
| auto request_handler = |
| std::move(RegisterRequestHandlers({kKeepAliveEndpoint})[0]); |
| ASSERT_TRUE(server()->Start()); |
| |
| // Set up redirects according to the following redirect chain: |
| // fetch("http://a.test:<port>/beacon", keepalive: true) |
| // --> chrome://settings |
| ASSERT_NO_FATAL_FAILURE( |
| LoadPageWithKeepAliveRequestAndSendResponseAfterUnload( |
| GetKeepAlivePageURL(method), request_handler.get(), |
| base::StringPrintf(k301Response, unsafe_redirect_target))); |
| |
| // The redirect is unsafe, so the loader is terminated. |
| // While the 1st page is unloaded, the disconnection may not propagate to |
| // browser process in time, such that calling |
| // `WaitforTotalCompleteProcessed()` here might be flaky. |
| loaders_observer().WaitForTotalOnComplete({net::ERR_UNSAFE_REDIRECT}); |
| EXPECT_EQ(loader_service()->NumLoadersForTesting(), 0u); |
| ExpectFetchKeepAliveHistogram( |
| FetchKeepAliveRequestMetricType::kFetch, |
| ExpectedTotalRequests(/*browser=*/1, /*renderer=*/1), |
| ExpectedStartedRequests(/*browser=*/1, /*renderer=*/1), |
| ExpectedSucceededRequests(/*browser=*/0, /*renderer=*/0), |
| ExpectedFailedRequests(/*browser=*/1, /*renderer=*/0)); |
| } |
| |
| // Delays handling an violating CSP redirect for a keepalive ping until after |
| // the page making the keepalive ping has been unloaded. |
| // The browser must ensure the redirect is not followed. |
| IN_PROC_BROWSER_TEST_P(KeepAliveURLBrowserTest, |
| ReceiveViolatingCSPRedirectAfterPageUnload) { |
| const std::string method = GetParam(); |
| const char violating_csp_redirect_target[] = "http://b.com/beacon-redirected"; |
| auto request_handler = |
| std::move(RegisterRequestHandlers({kKeepAliveEndpoint})[0]); |
| ASSERT_TRUE(server()->Start()); |
| const GURL allowed_csp_url = server()->GetURL(kAllowedCspHost, "/"); |
| |
| // Set up redirects according to the following redirect chain: |
| // fetch("http://a.test:<port>/beacon", keepalive: true) |
| // --> http://b.test/beacon-redirected |
| ASSERT_NO_FATAL_FAILURE( |
| LoadPageWithKeepAliveRequestAndSendResponseAfterUnload( |
| GetKeepAlivePageURL( |
| method, /*num_requests=*/1, |
| GetConnectSrcCSPHeader(url::Origin::Create(allowed_csp_url))), |
| request_handler.get(), |
| base::StringPrintf(k301Response, violating_csp_redirect_target))); |
| |
| // The redirect doesn't match CSP source from the 1st page, so the loader is |
| // terminated. |
| // While the 1st page is unloaded, the disconnection may not propagate to |
| // browser process in time, such that calling |
| // `WaitforTotalCompleteProcessed()` here might be flaky. |
| loaders_observer().WaitForTotalOnComplete({net::ERR_BLOCKED_BY_CSP}); |
| EXPECT_EQ(loader_service()->NumLoadersForTesting(), 0u); |
| ExpectFetchKeepAliveHistogram( |
| FetchKeepAliveRequestMetricType::kFetch, |
| ExpectedTotalRequests(/*browser=*/1, /*renderer=*/1), |
| ExpectedStartedRequests(/*browser=*/1, /*renderer=*/1), |
| ExpectedSucceededRequests(/*browser=*/0, /*renderer=*/0), |
| ExpectedFailedRequests(/*browser=*/1, /*renderer=*/0)); |
| } |
| |
| // Verifies a redirect to mixed content target URL is not loaded. |
| IN_PROC_BROWSER_TEST_P(KeepAliveURLBrowserTest, ReceiveMixedContentRedirect) { |
| SetUseHttps(); |
| const std::string method = GetParam(); |
| auto request_handler = |
| std::move(RegisterRequestHandlers({kKeepAliveEndpoint})[0]); |
| ASSERT_TRUE(server()->Start()); |
| // Sets up a target URL that only has different scheme. |
| // https://a.test:<port>/beacon-redirected |
| std::string same_content_target = |
| server()->GetURL(kPrimaryHost, "/beacon-redirected").spec(); |
| // http://a.test:<port>/beacon-redirected |
| std::string mixed_content_target = same_content_target; |
| base::ReplaceSubstringsAfterOffset(&mixed_content_target, 0, "https", "http"); |
| |
| // Sets up redirects according to the following redirect chain: |
| // fetch("https://a.test:<port>/beacon", keepalive: true) |
| // --> http://a.test:<port>/beacon-redirected => blocked by mixed content |
| // Although it's also a CORS request, it will be blocked by mixed content |
| // before reaching network service. |
| ASSERT_NO_FATAL_FAILURE(LoadPageWithKeepAliveRequestAndSendResponse( |
| GetKeepAlivePageURL(method), request_handler.get(), |
| base::StringPrintf(k301Response, mixed_content_target.c_str()))); |
| |
| // The redirect is mixed content, so the redirect is aborted. |
| loaders_observer().WaitForTotalOnReceiveRedirectForwarded(1); |
| loaders_observer().WaitForTotalOnReceiveResponseProcessed(0); |
| // Note that the renderer terminates without waiting for error forwarded from |
| // browser as it also calculates the error by itself. |
| loaders_observer().WaitForTotalOnCompleteForwarded({}); |
| // The loader in browser is only terminated after renderer terminates its |
| // loader. There is no way to wait for such disconnection mojo message |
| // forwarded to browser at this moment. |
| |
| ExpectFetchKeepAliveHistogram( |
| FetchKeepAliveRequestMetricType::kFetch, |
| ExpectedTotalRequests(/*browser=*/1, /*renderer=*/1), |
| ExpectedStartedRequests(/*browser=*/1, /*renderer=*/1), |
| ExpectedSucceededRequests(/*browser=*/0, /*renderer=*/0), |
| // TODO(crbug.com/40109064): Figure out why UMA from renderer cannot be |
| // fetched by FetchHistogramsFromChildProcesses(). |
| ExpectedFailedRequests(/*browser=*/1)); |
| } |
| |
| // Verifies a redirect to mixed content target URL is allowed by |
| // KeepAliveURLLoader if the page making the fetch keepalive request has been |
| // unloaded, the same as pre-migration approach https://crrev.com/c/518743. |
| // |
| // Note that the current implementation in Blink & content cannot handle mixed |
| // content checking without the RFHI of the page that loads the request. |
| IN_PROC_BROWSER_TEST_P(KeepAliveURLBrowserTest, |
| ReceiveMixedContentRedirectAfterUnload) { |
| SetUseHttps(); |
| const std::string method = GetParam(); |
| auto request_handler = |
| std::move(RegisterRequestHandlers({kKeepAliveEndpoint})[0]); |
| auto redirected_request_handler = |
| std::make_unique<net::test_server::ControllableHttpResponse>( |
| embedded_test_server(), "/beacon-redirected"); |
| ASSERT_TRUE(server()->Start()); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| // Sets up a mixed content target URL that only has different scheme. |
| // http://a.test:<port>/beacon-redirected |
| std::string mixed_content_target = |
| embedded_test_server()->GetURL(kPrimaryHost, "/beacon-redirected").spec(); |
| |
| // Sets up redirects according to the following redirect chain: |
| // fetch("https://a.test:<port>/beacon", keepalive: true) |
| // --> http://a.test:<port>/beacon-redirected |
| ASSERT_NO_FATAL_FAILURE( |
| LoadPageWithKeepAliveRequestAndSendResponseAfterUnload( |
| GetKeepAlivePageURL(method), request_handler.get(), |
| base::StringPrintf(k301Response, mixed_content_target.c_str()))); |
| |
| redirected_request_handler->WaitForRequest(); |
| redirected_request_handler->Send( |
| "HTTP/1.1 200 OK\r\n" |
| "Content-Type: text/html; charset=utf-8\r\n" |
| // Necessary as this is a response to cross-origin request. |
| "Access-Control-Allow-Origin: *\r\n" |
| "\r\n" |
| "Acked!"); |
| redirected_request_handler->Done(); |
| |
| // The in-browser logic should process the redirect & response, as there is no |
| // mixed content checking after unload. |
| // TODO(crbug.com/40941240): Revisit the checks after the bug is fixed. |
| loaders_observer().WaitForTotalOnReceiveRedirectProcessed(1); |
| loaders_observer().WaitForTotalOnReceiveResponseProcessed(1); |
| EXPECT_EQ(loader_service()->NumDisconnectedLoadersForTesting(), 0u); |
| EXPECT_EQ(loader_service()->NumLoadersForTesting(), 0u); |
| ExpectFetchKeepAliveHistogram( |
| FetchKeepAliveRequestMetricType::kFetch, |
| ExpectedTotalRequests(/*browser=*/1, /*renderer=*/1), |
| ExpectedStartedRequests(/*browser=*/1, /*renderer=*/1), |
| ExpectedSucceededRequests(/*browser=*/1, /*renderer=*/0), |
| ExpectedFailedRequests(/*browser=*/0, /*renderer=*/0)); |
| } |
| |
| // TODO(crbug.com/40236167): Re-enable this test on Mac. |
| #if BUILDFLAG(IS_MAC) |
| #define MAYBE_ReceiveViolatingCSPRedirectInChildFrame \ |
| DISABLED_ReceiveViolatingCSPRedirectInChildFrame |
| #else |
| #define MAYBE_ReceiveViolatingCSPRedirectInChildFrame \ |
| ReceiveViolatingCSPRedirectInChildFrame |
| #endif |
| // Ensures that a keepalive request in a child frame use its RFH's data instead |
| // of its parent frame's: |
| // The main frame CSP allows `kAllowedCspHost`, while the child frame CSP does |
| // not. See also https://w3c.github.io/webappsec-csp/#security-inherit-csp. |
| IN_PROC_BROWSER_TEST_P(KeepAliveURLBrowserTest, |
| MAYBE_ReceiveViolatingCSPRedirectInChildFrame) { |
| const std::string method = GetParam(); |
| auto request_handler = |
| std::move(RegisterRequestHandlers({GetKeepAliveEndpoint("main")})[0]); |
| ASSERT_TRUE(server()->Start()); |
| const GURL main_target_url = |
| server()->GetURL(kAllowedCspHost, GetKeepAliveEndpoint("main")); |
| const GURL child_target_url = |
| server()->GetURL(kAllowedCspHost, GetKeepAliveEndpoint("child")); |
| const GURL main_beacon_url = GetSameOriginRedirectURL(main_target_url); |
| const GURL child_beacon_url = GetSameOriginRedirectURL(child_target_url); |
| |
| // Main Page: |
| // Prepares the main page that sends out a keepalive request. |
| ASSERT_TRUE(NavigateToURL( |
| web_contents(), |
| server()->GetURL( |
| kPrimaryHost, |
| "/set-header-with-file/content/test/data/title1.html?" + |
| GetConnectSrcCSPHeader(url::Origin::Create(main_target_url))))); |
| ASSERT_TRUE(ExecJs(web_contents(), |
| JsReplace(R"( |
| fetch($1, {keepalive: true, mode: 'cors'}); |
| |
| let childLoaded; |
| let childThrown; |
| var childLoadedPromise = new Promise(resolve => childLoaded = resolve); |
| var childThrownPromise = new Promise(resolve => childThrown = resolve); |
| window.addEventListener('message', e => { |
| if (e.data === 'loaded') { |
| childLoaded(true); |
| } else { |
| childThrown(e.data); |
| } |
| }); |
| |
| // Child Frame (Same-Origin): |
| // Prepares the child page that also sends out a keepalive request. |
| const iframe = document.createElement('iframe'); |
| iframe.srcdoc = ` |
| <meta http-equiv="Content-Security-Policy" content="connect-src 'self';"> |
| <script> |
| fetch($2, {keepalive: true, mode: 'cors'}).catch(e => |
| window.parent.postMessage(e.message, "*") |
| ); |
| window.parent.postMessage('loaded', "*"); |
| </script> |
| `; |
| document.body.appendChild(iframe); |
| )", |
| main_beacon_url, child_beacon_url), |
| content::EXECUTE_SCRIPT_NO_RESOLVE_PROMISES)); |
| ASSERT_EQ(current_frame_host()->child_count(), 1u); |
| EXPECT_EQ(true, EvalJs(web_contents(), "childLoadedPromise")); |
| // Redirects the keepalive request from child page to an allowed target. |
| // http://a.test:<port>/server-redirect-307?... |
| // --> http://csp.test:<port>/beacon?id=child => disallowed by CSP |
| EXPECT_EQ("Failed to fetch", EvalJs(web_contents(), "childThrownPromise")); |
| |
| // Only the main page is expected to sent out its keepalive request. |
| request_handler->WaitForRequest(); |
| |
| // Redirects the keepalive request from main page to a disallowed target. |
| // http://a.test:<port>/server-redirect-307?... |
| // --> http://csp.test:<port>/beacon?id=main => allowed by CSP |
| request_handler->Send( |
| "HTTP/1.1 200 OK\r\n" |
| "Content-Type: text/html; charset=utf-8\r\n" |
| // Necessary as this is a response to cross-origin request. |
| "Access-Control-Allow-Origin: *\r\n" |
| "\r\n" |
| "Acked!"); |
| request_handler->Done(); |
| |
| // Only 1 redirect is expected to reach response. |
| loaders_observer().WaitForTotalOnReceiveRedirectProcessed(1); |
| loaders_observer().WaitForTotalOnReceiveResponse(1); |
| // TODO(crbug.com/40236167): the order of calls to OnComplete is not stable. |
| // Update KeepAliveURLLoadersTestObserver::WaitForTotalOnComplete to |
| // accommodate this situation before asserting net::ERR_BLOCKED_BY_CSP. |
| |
| // Total 2 requests, and of them 1 fails. |
| ExpectFetchKeepAliveHistogram( |
| FetchKeepAliveRequestMetricType::kFetch, |
| ExpectedTotalRequests(/*browser=*/2, /*renderer=*/2), |
| ExpectedStartedRequests(/*browser=*/2, /*renderer=*/2), |
| ExpectedSucceededRequests(/*browser=*/1, /*renderer=*/1), |
| ExpectedFailedRequests(/*browser=*/1, /*renderer=*/1)); |
| } |
| |
| // Contains the browser tests for loading fetch(url, {keepalive: true}) |
| // requests **without** routing to browser process. |
| class FetchKeepAlivePreMigrationBrowserTest |
| : public FetchKeepAliveCommonTestBase, |
| public ::testing::WithParamInterface<std::string> { |
| protected: |
| const FeaturesType& GetEnabledFeatures() override { |
| static const FeaturesType enabled_features = |
| content::GetDefaultEnabledBackForwardCacheFeaturesForTesting( |
| {{features::kBackForwardCache, {}}}); |
| return enabled_features; |
| } |
| const DisabledFeaturesType& GetDisabledFeatures() override { |
| static const DisabledFeaturesType disabled_features = |
| GetDefaultDisabledBackForwardCacheFeaturesForTesting( |
| {blink::features::kKeepAliveInBrowserMigration, |
| blink::features::kFetchLaterAPI}); |
| return disabled_features; |
| } |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P( |
| All, |
| FetchKeepAlivePreMigrationBrowserTest, |
| ::testing::Values(net::HttpRequestHeaders::kGetMethod, |
| net::HttpRequestHeaders::kPostMethod), |
| [](const testing::TestParamInfo< |
| FetchKeepAlivePreMigrationBrowserTest::ParamType>& info) { |
| return info.param; |
| }); |
| |
| IN_PROC_BROWSER_TEST_P(FetchKeepAlivePreMigrationBrowserTest, OneRequest) { |
| const std::string method = GetParam(); |
| auto request_handler = |
| std::move(RegisterRequestHandlers({kKeepAliveEndpoint})[0]); |
| ASSERT_TRUE(server()->Start()); |
| |
| ASSERT_TRUE(NavigateToURL(web_contents(), GetKeepAlivePageURL(method))); |
| // Ensure the keepalive request is sent, but delay response. |
| request_handler->WaitForRequest(); |
| |
| // End the keepalive request by sending back response. |
| request_handler->Send(k200TextResponse); |
| request_handler->Done(); |
| |
| TitleWatcher watcher(web_contents(), kPromiseResolvedPageTitle); |
| EXPECT_EQ(watcher.WaitAndGetTitle(), kPromiseResolvedPageTitle); |
| ExpectFetchKeepAliveHistogram( |
| FetchKeepAliveRequestMetricType::kFetch, |
| ExpectedTotalRequests(/*browser=*/0, /*renderer=*/1), |
| ExpectedStartedRequests(/*browser=*/0, /*renderer=*/1), |
| ExpectedSucceededRequests(/*browser=*/0, /*renderer=*/1), |
| ExpectedFailedRequests(/*browser=*/0, /*renderer=*/0)); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(FetchKeepAlivePreMigrationBrowserTest, |
| ReceiveResponseAfterPageUnload) { |
| const std::string method = GetParam(); |
| auto request_handler = |
| std::move(RegisterRequestHandlers({kKeepAliveEndpoint})[0]); |
| ASSERT_TRUE(server()->Start()); |
| |
| ASSERT_NO_FATAL_FAILURE( |
| LoadPageWithKeepAliveRequestAndSendResponseAfterUnload( |
| GetKeepAlivePageURL(method), request_handler.get(), |
| k200TextResponse)); |
| |
| // The response should be processed in renderer. |
| ExpectFetchKeepAliveHistogram( |
| FetchKeepAliveRequestMetricType::kFetch, |
| ExpectedTotalRequests(/*browser=*/0, /*renderer=*/1), |
| ExpectedStartedRequests(/*browser=*/0, /*renderer=*/1), |
| // Due to https://crbug.com/40109064, succeeded_count and failed_count |
| // logging from renderer are flaky. |
| ExpectedSucceededRequests(/*browser=*/0), |
| // The pre-migration implementation in |
| // `blink::ResourceLoader::DidResponseResponseInternal()` triggers error |
| // handling if a document is detached even if the renderer has received |
| // the response. |
| ExpectedFailedRequests(/*browser=*/0)); |
| } |
| |
| class SendBeaconBrowserTestBase : public KeepAliveURLBrowserTestBase { |
| protected: |
| virtual std::string beacon_payload_type() const = 0; |
| |
| const FeaturesType& GetEnabledFeatures() override { |
| static const FeaturesType enabled_features = |
| GetDefaultEnabledBackForwardCacheFeaturesForTesting( |
| {{blink::features::kKeepAliveInBrowserMigration, {}}}); |
| return enabled_features; |
| } |
| |
| GURL GetBeaconPageURL( |
| const GURL& beacon_url, |
| bool with_non_cors_safelisted_content, |
| std::optional<int> delay_iframe_removal_ms = std::nullopt) const { |
| std::vector<std::string> queries = { |
| "/send-beacon-in-iframe.html?url=" + EncodeURL(beacon_url), |
| "&payload_type=" + beacon_payload_type()}; |
| if (with_non_cors_safelisted_content) { |
| // Setting the payload's content type to `application/octet-stream`, as |
| // only `application/x-www-form-urlencoded`, `multipart/form-data`, and |
| // `text/plain` MIME types are allowed for CORS-safelisted `content-type` |
| // request header. |
| // https://fetch.spec.whatwg.org/#cors-safelisted-request-header |
| queries.push_back("&payload_content_type=application/octet-stream"); |
| } |
| if (delay_iframe_removal_ms.has_value()) { |
| queries.push_back(base::StringPrintf("&delay_iframe_removal_ms=%d", |
| delay_iframe_removal_ms.value())); |
| } |
| |
| return server()->GetURL(kPrimaryHost, base::StrCat(queries)); |
| } |
| |
| // Navigates to a page that calls `navigator.sendBeacon(beacon_url)` from a |
| // programmatically created iframe. The iframe will then be removed after the |
| // JS call after an optional `delay_iframe_removal_ms` interval. |
| // `request_handler` must handle the final URL of the sendBeacon request. |
| void LoadPageWithIframeAndSendBeacon( |
| const GURL& beacon_url, |
| net::test_server::ControllableHttpResponse* request_handler, |
| const std::string& response, |
| int expect_total_redirects, |
| std::optional<int> delay_iframe_removal_ms = std::nullopt) { |
| // Navigate to the page that calls sendBeacon with `beacon_url` from an |
| // appended iframe. |
| ASSERT_TRUE(NavigateToURL( |
| web_contents(), |
| GetBeaconPageURL(beacon_url, |
| /*with_non_cors_safelisted_content=*/false, |
| delay_iframe_removal_ms))); |
| ASSERT_EQ(loader_service()->NumLoadersForTesting(), 1u); |
| |
| // All redirects, if exist, should be processed in browser first. |
| loaders_observer().WaitForTotalOnReceiveRedirectProcessed( |
| expect_total_redirects); |
| // After in-browser processing, the loader should remain alive to support |
| // forwarding stored redirects/response to renderer. But it may or may not |
| // connect to a renderer. |
| EXPECT_EQ(loader_service()->NumLoadersForTesting(), 1u); |
| |
| // Ensure the sendBeacon request is sent. |
| request_handler->WaitForRequest(); |
| // Send back final response to terminate in-browser request handling. |
| request_handler->Send(response); |
| request_handler->Done(); |
| |
| // After in-browser redirect/response processing, the in-browser logic may |
| // or may not forward redirect/response to renderer process, depending on |
| // whether the renderer is still alive. |
| loaders_observer().WaitForTotalOnReceiveResponse(1); |
| // OnComplete may not be called if the renderer dies too early in before |
| // receiving response. |
| |
| // The loader should all be gone. |
| EXPECT_EQ(loader_service()->NumLoadersForTesting(), 0u); |
| } |
| }; |
| |
| class SendBeaconBrowserTest |
| : public SendBeaconBrowserTestBase, |
| public ::testing::WithParamInterface<std::string> { |
| protected: |
| std::string beacon_payload_type() const override { return GetParam(); } |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P( |
| All, |
| SendBeaconBrowserTest, |
| ::testing::Values("string", "arraybuffer", "form", "blob"), |
| [](const testing::TestParamInfo<KeepAliveURLBrowserTest::ParamType>& info) { |
| return info.param; |
| }); |
| |
| // TODO(crbug.com/40931297): Re-enable this test on Mac. |
| #if BUILDFLAG(IS_MAC) |
| #define MAYBE_MultipleRedirectsRequestWithIframeRemoval \ |
| DISABLED_MultipleRedirectsRequestWithIframeRemoval |
| #else |
| #define MAYBE_MultipleRedirectsRequestWithIframeRemoval \ |
| MultipleRedirectsRequestWithIframeRemoval |
| #endif |
| // Tests navigator.sendBeacon() with a cross-origin & CORS-safelisted request |
| // that causes a redirect chain of 4 URLs. |
| // |
| // The JS call happens in an iframe that is removed right after the sendBeacon() |
| // call, so the chain of redirects & response handling must survive the iframe |
| // unload. |
| IN_PROC_BROWSER_TEST_P(SendBeaconBrowserTest, |
| MAYBE_MultipleRedirectsRequestWithIframeRemoval) { |
| const auto beacon_endpoint = |
| base::StringPrintf("%s?id=%s", kKeepAliveEndpoint, kBeaconId); |
| auto request_handler = |
| std::move(RegisterRequestHandlers({beacon_endpoint})[0]); |
| ASSERT_TRUE(server()->Start()); |
| |
| // Set up a cross-origin (kSecondaryHost) URL with CORS-safelisted |
| // payload that causes multiple redirects. |
| const auto target_url = server()->GetURL(kSecondaryHost, beacon_endpoint); |
| const auto beacon_url = GetCrossOriginMultipleRedirectsURL(target_url); |
| |
| LoadPageWithIframeAndSendBeacon(beacon_url, request_handler.get(), |
| k200TextResponse, |
| /*expect_total_redirects=*/3); |
| } |
| |
| // Tests navigator.sendBeacon() with a cross-origin & CORS-safelisted request |
| // that causes a redirect chain of 4 URLs. |
| // |
| // Unlike the `MultipleRedirectsRequestWithIframeRemoval` test case above, the |
| // request here is fired within an iframe that will be removed shortly |
| // (delayed by 0ms, roughly in the JS next event cycle). |
| // This is to mimic the following scenario: |
| // |
| // 1. The server returns a redirect. |
| // 2. In the browser process KeepAliveURLLoader::OnReceiveRedirect(), |
| // forwarding_client_ is not null (as renderer/iframe still exists), so it |
| // calls forwarding_client_->OnReceiveRedirect() IPC to forward to renderer. |
| // 3. The renderer process is somehow shut down before its |
| // URLLoaderClient::OnReceiveRedirect() is finished, so the redirect chain is |
| // incompleted. |
| // 4. KeepAliveURLLoader::OnRendererConnectionError() is triggered, and only |
| // aware of forwarding_client_'s disconnection. It should take over redirect |
| // chain handling. |
| // |
| // Without delaying iframe removal, renderer disconnection may happen in between |
| // (2) and (3). |
| IN_PROC_BROWSER_TEST_P(SendBeaconBrowserTest, |
| MultipleRedirectsRequestWithDelayedIframeRemoval) { |
| const auto beacon_endpoint = |
| base::StringPrintf("%s?id=%s", kKeepAliveEndpoint, kBeaconId); |
| auto request_handler = |
| std::move(RegisterRequestHandlers({beacon_endpoint})[0]); |
| ASSERT_TRUE(server()->Start()); |
| |
| // Set up a cross-origin (kSecondaryHost) URL with CORS-safelisted |
| // payload that causes multiple redirects. |
| const auto target_url = server()->GetURL(kSecondaryHost, beacon_endpoint); |
| const auto beacon_url = GetCrossOriginMultipleRedirectsURL(target_url); |
| |
| LoadPageWithIframeAndSendBeacon(beacon_url, request_handler.get(), |
| k200TextResponse, |
| /*expect_total_redirects=*/3, |
| /*delay_iframe_removal_ms=*/0); |
| } |
| |
| // Tests navigator.sendBeacon() with a cross-origin & CORS-safelisted request |
| // that redirects from url1 to url2. The redirect is handled by a server |
| // endpoint (/no-cors-server-redirect-307) which does not support CORS. |
| // As navigator.sendBeacon() marks its request with `no-cors`, the redirect |
| // should succeed. |
| // TODO(crbug.com/40282448): Flaky on Android and Mac. |
| #if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_ANDROID) |
| #define MAYBE_CrossOriginAndCORSSafelistedRedirectRequest \ |
| DISABLED_CrossOriginAndCORSSafelistedRedirectRequest |
| #else |
| #define MAYBE_CrossOriginAndCORSSafelistedRedirectRequest \ |
| CrossOriginAndCORSSafelistedRedirectRequest |
| #endif |
| IN_PROC_BROWSER_TEST_P(SendBeaconBrowserTest, |
| MAYBE_CrossOriginAndCORSSafelistedRedirectRequest) { |
| const auto beacon_endpoint = |
| base::StringPrintf("%s?id=%s", kKeepAliveEndpoint, kBeaconId); |
| auto request_handler = |
| std::move(RegisterRequestHandlers({beacon_endpoint})[0]); |
| ASSERT_TRUE(server()->Start()); |
| |
| // Set up a cross-origin (kSecondaryHost) redirect with CORS-safelisted |
| // payload according to the following redirect chain: |
| // navigator.sendBeacon( |
| // "http://b.test:<port>/no-cors-server-redirect-307?...", |
| // <CORS-safelisted payload>) |
| // --> http://b.test:<port>/beacon?id=beacon01 |
| const auto target_url = server()->GetURL(kSecondaryHost, beacon_endpoint); |
| const auto beacon_url = server()->GetURL( |
| kSecondaryHost, base::StringPrintf("/no-cors-server-redirect-307?%s", |
| EncodeURL(target_url).c_str())); |
| |
| LoadPageWithIframeAndSendBeacon(beacon_url, request_handler.get(), |
| k200TextResponse, |
| /*expect_total_redirects=*/1); |
| } |
| |
| class SendBeaconBlobBrowserTest : public SendBeaconBrowserTestBase { |
| protected: |
| std::string beacon_payload_type() const override { return "blob"; } |
| }; |
| |
| // Tests navigator.sendBeacon() with a cross-origin & non-CORS-safelisted |
| // request that redirects from url1 to url2. The redirect is handled by a server |
| // endpoint (/no-cors-server-redirect-307) which does not support CORS. |
| // As navigator.sendBeacon() marks its request with `no-cors`, the redirect |
| // should fail. |
| IN_PROC_BROWSER_TEST_F(SendBeaconBlobBrowserTest, |
| CrossOriginAndNonCORSSafelistedRedirectRequest) { |
| const auto beacon_endpoint = |
| base::StringPrintf("%s?id=%s", kKeepAliveEndpoint, kBeaconId); |
| auto request_handler = |
| std::move(RegisterRequestHandlers({beacon_endpoint})[0]); |
| ASSERT_TRUE(server()->Start()); |
| |
| // Set up a cross-origin (kSecondaryHost) redirect with non-CORS-safelisted |
| // payload according to the following redirect chain: |
| // navigator.sendBeacon( |
| // "http://b.test:<port>/no-cors-server-redirect-307?...", |
| // <non-CORS-safelisted payload>) => should fail here |
| // --> http://b.test:<port>/beacon?id=beacon01 |
| const auto target_url = server()->GetURL(kSecondaryHost, beacon_endpoint); |
| const auto beacon_url = server()->GetURL( |
| kSecondaryHost, base::StringPrintf("/no-cors-server-redirect-307?%s", |
| EncodeURL(target_url).c_str())); |
| // Navigate to the page that calls sendBeacon with `beacon_url` from an |
| // appended iframe, which will be removed shortly after calling sendBeacon(). |
| ASSERT_TRUE(NavigateToURL( |
| web_contents(), |
| GetBeaconPageURL(beacon_url, /*with_non_cors_safelisted_content=*/true))); |
| |
| // The redirect is rejected in-browser during redirect (with |
| // non-CORS-safelisted payload) handling because /no-cors-server-redirect-xxx |
| // doesn't support CORS. Thus, KeepAliveURLLoader::OnReceiveRedirect() is not |
| // called but KeepAliveURLLoader::OnComplete(). |
| // Note that renderer can be gone at any point before or after the first URL |
| // is loaded. So OnComplete() may or may not be forwarded. |
| loaders_observer().WaitForTotalOnComplete({net::ERR_FAILED}); |
| EXPECT_FALSE(request_handler->has_received_request()); |
| // After in-browser processing, the loader should all be gone. |
| EXPECT_EQ(loader_service()->NumDisconnectedLoadersForTesting(), 0u); |
| EXPECT_EQ(loader_service()->NumLoadersForTesting(), 0u); |
| ExpectFetchKeepAliveHistogram( |
| FetchKeepAliveRequestMetricType::kBeacon, |
| ExpectedTotalRequests(/*browser=*/1, /*renderer=*/1), |
| ExpectedStartedRequests(/*browser=*/1, /*renderer=*/1), |
| ExpectedSucceededRequests(/*browser=*/0, /*renderer=*/0), |
| ExpectedFailedRequests(/*browser=*/1, /*renderer=*/1)); |
| } |
| |
| // A base class to help testing JS fetchLater() API behaviors. |
| class FetchLaterBrowserTestBase : public KeepAliveURLBrowserTestBase { |
| protected: |
| void SetUp() override { |
| // fetchLater() API only supports HTTPS requests. |
| SetUseHttps(); |
| KeepAliveURLBrowserTestBase::SetUp(); |
| } |
| |
| bool NavigateToURL(const GURL& url) { |
| previous_document_ = |
| std::make_unique<RenderFrameHostImplWrapper>(current_frame_host()); |
| bool ret = content::NavigateToURL(web_contents(), url); |
| current_document_ = |
| std::make_unique<RenderFrameHostImplWrapper>(current_frame_host()); |
| return ret; |
| } |
| bool WaitUntilPreviousDocumentDeleted() { |
| CHECK(previous_document_); |
| // `previous_document_` might already be destroyed here. |
| return previous_document_->WaitUntilRenderFrameDeleted(); |
| } |
| // Caution: the returned document might already be killed if BFCache is not |
| // working. |
| RenderFrameHostImplWrapper& previous_document() { |
| CHECK(previous_document_); |
| CHECK(!previous_document_->IsDestroyed()); |
| return *previous_document_; |
| } |
| RenderFrameHostImplWrapper& current_document() { |
| CHECK(previous_document_); |
| return *current_document_; |
| } |
| |
| // Navigates to an empty page, and executes `script` on it. |
| void RunScript(const std::string& script) { |
| ASSERT_TRUE(NavigateToURL(server()->GetURL(kPrimaryHost, "/title1.html"))); |
| ASSERT_TRUE(ExecJs(web_contents(), script)); |
| ASSERT_TRUE(WaitForLoadStop(web_contents())); |
| } |
| |
| // Navigates to a page that executes `script`, and navigates to another page. |
| void RunScriptAndNavigateAway(const std::string& script) { |
| RunScript(script); |
| |
| // Navigate to cross-origin page to ensure the 1st page can be unloaded if |
| // BackForwardCache is disabled. |
| ASSERT_TRUE( |
| NavigateToURL(server()->GetURL(kSecondaryHost, "/title2.html"))); |
| ASSERT_TRUE(WaitForLoadStop(web_contents())); |
| } |
| |
| // Expects `total` number of FetchLater requests to be sent. |
| // `total` must equal to the size of `request_handlers`. |
| // `requester_handlers` are to wait for the FetchLater requests and to |
| // respond. |
| void ExpectFetchLaterRequests( |
| size_t total, |
| std::vector<std::unique_ptr<net::test_server::ControllableHttpResponse>>& |
| request_handlers) { |
| SCOPED_TRACE( |
| base::StringPrintf("ExpectFetchLaterRequests: %zu requests", total)); |
| ASSERT_EQ(total, request_handlers.size()); |
| EXPECT_EQ(loader_service()->NumLoadersForTesting(), total); |
| |
| for (const auto& handler : request_handlers) { |
| // Waits for a FetchLater request. |
| handler->WaitForRequest(); |
| // Sends back final response to terminate in-browser request handling. |
| handler->Send(k200TextResponse); |
| // Triggers OnComplete. |
| handler->Done(); |
| } |
| |
| loaders_observer().WaitForTotalOnReceiveResponse(total); |
| // TODO(crbug.com/40236167): Check NumLoadersForTesting==0 after migrating |
| // to in-browser ThrottlingURLLoader. Current implementation cannot ensure |
| // receiving renderer disconnection. Also need to wait for TotalOnComplete |
| // by `total`, not by states. |
| } |
| |
| GURL GetFetchLaterPageURL(const std::string& host, |
| const std::string& method) const { |
| std::string url = base::StrCat( |
| {"/set-header-with-file/content/test/data/fetch_later.html?" |
| "method=", |
| method}); |
| return server()->GetURL(host, url); |
| } |
| |
| private: |
| std::unique_ptr<RenderFrameHostImplWrapper> current_document_ = nullptr; |
| std::unique_ptr<RenderFrameHostImplWrapper> previous_document_ = nullptr; |
| }; |
| |
| class FetchLaterBasicBrowserTest : public FetchLaterBrowserTestBase { |
| protected: |
| const FeaturesType& GetEnabledFeatures() override { |
| static const FeaturesType enabled_features = { |
| {blink::features::kFetchLaterAPI, {{}}}}; |
| return enabled_features; |
| } |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(FetchLaterBasicBrowserTest, CallInMainDocument) { |
| const std::string target_url = kFetchLaterEndpoint; |
| ASSERT_TRUE(server()->Start()); |
| |
| RunScript(JsReplace(R"( |
| fetchLater($1); |
| )", |
| target_url)); |
| ASSERT_FALSE(current_document().IsDestroyed()); |
| |
| // The loader should still be connected as the page exists. |
| EXPECT_EQ(loader_service()->NumDisconnectedLoadersForTesting(), 0u); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(FetchLaterBasicBrowserTest, CallInSameOriginChild) { |
| ASSERT_TRUE(server()->Start()); |
| |
| RunScript(JsReplace( |
| R"( |
| var childPromise = new Promise((resolve, reject) => { |
| window.addEventListener('message', e => { |
| if (e.data.type === 'fetchLater.done') { |
| resolve(e.data.type); |
| } else { |
| reject(e.data.type); |
| } |
| }); |
| }); |
| |
| const iframe = document.createElement("iframe"); |
| iframe.src = $1; |
| document.body.appendChild(iframe); |
| )", |
| GetFetchLaterPageURL(kPrimaryHost, net::HttpRequestHeaders::kGetMethod))); |
| ASSERT_FALSE(current_document().IsDestroyed()); |
| |
| EXPECT_EQ("fetchLater.done", EvalJs(web_contents(), "childPromise")); |
| // The loader should still be connected as the page exists. |
| EXPECT_EQ(loader_service()->NumDisconnectedLoadersForTesting(), 0u); |
| } |
| |
| // By default of `deferred-fetch-minimal` policy, `fetchLater()` should be |
| // allowed in first X cross-origin child iframes. |
| IN_PROC_BROWSER_TEST_F(FetchLaterBasicBrowserTest, CallInCrossOriginChild) { |
| const std::string target_url = kFetchLaterEndpoint; |
| ASSERT_TRUE(server()->Start()); |
| |
| RunScript(JsReplace( |
| R"( |
| var childPromise = new Promise((resolve, reject) => { |
| window.addEventListener('message', e => { |
| if (e.data.type === 'fetchLater.done') { |
| resolve(e.data.type); |
| } else { |
| reject(e.data.type + ': ' + e.data.error); |
| } |
| }); |
| }); |
| |
| const iframe = document.createElement("iframe"); |
| iframe.src = $1; |
| document.body.appendChild(iframe); |
| )", |
| GetFetchLaterPageURL(kSecondaryHost, |
| net::HttpRequestHeaders::kGetMethod))); |
| ASSERT_FALSE(current_document().IsDestroyed()); |
| |
| EXPECT_EQ("fetchLater.done", EvalJs(web_contents(), "childPromise")); |
| // The loader should still exist as the page exists. |
| EXPECT_EQ(loader_service()->NumDisconnectedLoadersForTesting(), 0u); |
| } |
| |
| // A type to support parameterized testing for timeout-related tests. |
| struct TestTimeoutType { |
| std::string test_case_name; |
| int32_t timeout; |
| }; |
| |
| // Tests to cover FetchLater's behaviors when BackForwardCache is off. |
| // |
| // Disables BackForwardCache such that a page is discarded right away on user |
| // navigating to another page. |
| class FetchLaterNoBackForwardCacheBrowserTest |
| : public FetchLaterBrowserTestBase, |
| public testing::WithParamInterface<TestTimeoutType> { |
| protected: |
| const FeaturesType& GetEnabledFeatures() override { |
| static const FeaturesType enabled_features = { |
| {blink::features::kFetchLaterAPI, {{}}}}; |
| return enabled_features; |
| } |
| const DisabledFeaturesType& GetDisabledFeatures() override { |
| static const DisabledFeaturesType disabled_features = { |
| features::kBackForwardCache}; |
| return disabled_features; |
| } |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P( |
| All, |
| FetchLaterNoBackForwardCacheBrowserTest, |
| testing::ValuesIn<std::vector<TestTimeoutType>>({ |
| {"LongTimeout", 600000}, // 10 minutes |
| {"OneMinuteTimeout", 60000}, // 1 minute |
| }), |
| [](const testing::TestParamInfo<TestTimeoutType>& info) { |
| return info.param.test_case_name; |
| }); |
| |
| // All pending FetchLater requests should be sent after the initiator page is |
| // gone, no matter how much time their activateAfter has left. |
| // Disables BackForwardCache such that a page is discarded right away on user |
| // navigating to another page. |
| IN_PROC_BROWSER_TEST_P(FetchLaterNoBackForwardCacheBrowserTest, |
| SendOnPageDiscardBeforeActivationTimeout) { |
| const std::string target_url = kFetchLaterEndpoint; |
| auto request_handlers = RegisterRequestHandlers({target_url, target_url}); |
| ASSERT_TRUE(server()->Start()); |
| |
| // Creates two FetchLater requests with various long activateAfter, which |
| // should all be sent on page discard. |
| RunScriptAndNavigateAway(JsReplace(R"( |
| fetchLater($1, {activateAfter: $2}); |
| fetchLater($1, {activateAfter: $2}); |
| )", |
| target_url, GetParam().timeout)); |
| // Ensure the 1st page has been unloaded. |
| ASSERT_TRUE(WaitUntilPreviousDocumentDeleted()); |
| |
| // Loaders are disconnected after the 1st page is gone. |
| EXPECT_EQ(loader_service()->NumDisconnectedLoadersForTesting(), 2u); |
| // The FetchLater requests should've been sent after the 1st page is gone. |
| ExpectFetchLaterRequests(2, request_handlers); |
| } |
| |
| class FetchLaterWithBackForwardCacheMetricsBrowserTestBase |
| : public FetchLaterBrowserTestBase, |
| public BackForwardCacheMetricsTestMatcher { |
| protected: |
| void SetUpOnMainThread() override { |
| // TestAutoSetUkmRecorder's constructor requires a sequenced context. |
| ukm_recorder_ = std::make_unique<ukm::TestAutoSetUkmRecorder>(); |
| histogram_tester_ = std::make_unique<base::HistogramTester>(); |
| FetchLaterBrowserTestBase::SetUpOnMainThread(); |
| } |
| |
| void TearDownOnMainThread() override { |
| ukm_recorder_.reset(); |
| histogram_tester_.reset(); |
| FetchLaterBrowserTestBase::TearDownOnMainThread(); |
| } |
| |
| // `BackForwardCacheMetricsTestMatcher` implementation. |
| const ukm::TestAutoSetUkmRecorder& ukm_recorder() override { |
| return *ukm_recorder_; |
| } |
| const base::HistogramTester& histogram_tester() override { |
| return *histogram_tester_; |
| } |
| |
| private: |
| std::unique_ptr<ukm::TestAutoSetUkmRecorder> ukm_recorder_; |
| std::unique_ptr<base::HistogramTester> histogram_tester_; |
| }; |
| |
| // Tests to cover FetchLater's behaviors when BackForwardCache is on but does |
| // not come into play. |
| // |
| // Setting long `BackForwardCache TTL (1min)` so that FetchLater sending cannot |
| // be caused by page eviction out of BackForwardCache. |
| class FetchLaterNoActivationTimeoutBrowserTest |
| : public FetchLaterWithBackForwardCacheMetricsBrowserTestBase { |
| protected: |
| const FeaturesType& GetEnabledFeatures() override { |
| static const FeaturesType enabled_features = { |
| {blink::features::kFetchLaterAPI, {}}, |
| {features::kBackForwardCache, {{}}}, |
| {features::kBackForwardCacheTimeToLiveControl, |
| {{"time_to_live_seconds", "60"}}}, |
| // Forces BackForwardCache to work in low memory device. |
| {features::kBackForwardCacheMemoryControls, |
| {{"memory_threshold_for_back_forward_cache_in_mb", "0"}}}}; |
| return enabled_features; |
| } |
| }; |
| |
| // A pending FetchLater request with default options should be sent after the |
| // initiator page is gone. |
| // Similar to SendOnPageDiscardBeforeActivationTimeout. |
| IN_PROC_BROWSER_TEST_F(FetchLaterNoActivationTimeoutBrowserTest, |
| SendOnPageDeletion) { |
| const std::string target_url = kFetchLaterEndpoint; |
| auto request_handlers = RegisterRequestHandlers({target_url}); |
| ASSERT_TRUE(server()->Start()); |
| |
| // Creates a FetchLater request in an iframe, which is removed after loaded. |
| ASSERT_TRUE(NavigateToURL( |
| server()->GetURL(kPrimaryHost, "/page_with_blank_iframe.html"))); |
| ASSERT_TRUE(ExecJs(web_contents(), R"( |
| var promise = new Promise(resolve => { |
| window.addEventListener('message', e => { |
| const iframe = document.getElementById('test_iframe'); |
| iframe.remove(); |
| resolve(e.data); |
| }); |
| }); |
| )")); |
| auto* iframe = |
| static_cast<RenderFrameHostImpl*>(ChildFrameAt(web_contents(), 0)); |
| EXPECT_TRUE(ExecJs(iframe, JsReplace(R"( |
| fetchLater($1); |
| window.parent.postMessage(true, "*"); |
| )", |
| target_url))); |
| // `iframe` is removed after it calls fetchLater(). |
| EXPECT_EQ(true, EvalJs(web_contents(), "promise")); |
| |
| // The loader is disconnected after the 1st page is gone. |
| EXPECT_EQ(loader_service()->NumDisconnectedLoadersForTesting(), 1u); |
| // The FetchLater requests should've been sent after the 1st page is gone. |
| ExpectFetchLaterRequests(1, request_handlers); |
| } |
| |
| // A pending FetchLater request should have been sent after its page gets |
| // restored from BackForwardCache before getting evicted. It is because, by |
| // default, pending requests are all flushed on BFCache no matter |
| // BackgroundSync is on or not. See http://b/310541607#comment28. |
| IN_PROC_BROWSER_TEST_F( |
| FetchLaterNoActivationTimeoutBrowserTest, |
| FlushedWhenPageIsRestoredBeforeBeingEvictedFromBackForwardCache) { |
| const std::string target_url = kFetchLaterEndpoint; |
| auto request_handlers = RegisterRequestHandlers({target_url}); |
| ASSERT_TRUE(server()->Start()); |
| |
| RunScriptAndNavigateAway(JsReplace(R"( |
| fetchLater($1); |
| )", |
| target_url)); |
| ASSERT_TRUE(previous_document()->IsInBackForwardCache()); |
| // Navigate back to the 1st page. |
| ASSERT_TRUE(HistoryGoBack(web_contents())); |
| |
| // The same page is still alive. |
| ExpectRestored(FROM_HERE); |
| // The FetchLater requests should've been sent. |
| ExpectFetchLaterRequests(1, request_handlers); |
| } |
| |
| // Without an activateAfter set, a pending FetchLater request should not be |
| // sent out during its page frozen state. |
| // Similar to ResetActivationTimeoutTimerOnPageResume. |
| IN_PROC_BROWSER_TEST_F(FetchLaterNoActivationTimeoutBrowserTest, |
| NotSendWhenPageIsResumedAfterBeingFrozen) { |
| const std::string target_url = kFetchLaterEndpoint; |
| ASSERT_TRUE(server()->Start()); |
| |
| // Creates a FetchLater request with NO activateAfter. |
| // It should be impossible to send out during page frozen. |
| ASSERT_TRUE(NavigateToURL(server()->GetURL(kPrimaryHost, "/title1.html"))); |
| ASSERT_TRUE(ExecJs(web_contents(), JsReplace(R"( |
| fetchLater($1); |
| )", |
| target_url))); |
| ASSERT_TRUE(WaitForLoadStop(web_contents())); |
| |
| // Forces to freeze the current page. |
| web_contents()->WasHidden(); |
| web_contents()->SetPageFrozen(true); |
| |
| // The FetchLater request should not be sent. |
| EXPECT_EQ(loader_service()->NumLoadersForTesting(), 1u); |
| EXPECT_EQ(loader_service()->NumDisconnectedLoadersForTesting(), 0u); |
| |
| // Forces to wake up the current page. |
| web_contents()->WasHidden(); |
| web_contents()->SetPageFrozen(false); |
| // The FetchLater request should not be sent. |
| // TODO(crbug.com/40276121): Verify FetchLaterResult once |
| // https://crrev.com/c/4820528 is submitted. |
| EXPECT_EQ(loader_service()->NumLoadersForTesting(), 1u); |
| EXPECT_EQ(loader_service()->NumDisconnectedLoadersForTesting(), 0u); |
| } |
| |
| // Tests to cover FetchLater's activateAfter behaviors when BackForwardCache |
| // is on and may come into play. |
| // |
| // BackForwardCache eviction is simulated by calling |
| // `DisableBFCacheForRFHForTesting(previous_document())` instead of relying on |
| // its TTL. |
| class FetchLaterActivationTimeoutBrowserTest |
| : public FetchLaterWithBackForwardCacheMetricsBrowserTestBase { |
| protected: |
| const FeaturesType& GetEnabledFeatures() override { |
| static const FeaturesType enabled_features = { |
| {blink::features::kFetchLaterAPI, {}}, |
| {features::kBackForwardCache, {{}}}, |
| // Sets to a long timeout, as tests below should not rely on it. |
| {features::kBackForwardCacheTimeToLiveControl, |
| {{"time_to_live_seconds", "60"}}}, |
| // Forces BackForwardCache to work in low memory device. |
| {features::kBackForwardCacheMemoryControls, |
| {{"memory_threshold_for_back_forward_cache_in_mb", "0"}}}}; |
| return enabled_features; |
| } |
| }; |
| |
| // When setting activateAfter=0, a pending FetchLater request should be sent |
| // "roughly" immediately. |
| IN_PROC_BROWSER_TEST_F(FetchLaterActivationTimeoutBrowserTest, |
| SendOnZeroActivationTimeout) { |
| const std::string target_url = kFetchLaterEndpoint; |
| auto request_handlers = RegisterRequestHandlers({target_url}); |
| ASSERT_TRUE(server()->Start()); |
| |
| // Creates a FetchLater request with activateAfter=0s. |
| RunScript(JsReplace(R"( |
| fetchLater($1, {activateAfter: 0}); |
| )", |
| target_url)); |
| ASSERT_FALSE(current_document().IsDestroyed()); |
| |
| // The loader should still exist as the page exists. |
| EXPECT_EQ(loader_service()->NumDisconnectedLoadersForTesting(), 0u); |
| // The FetchLater request should be sent, triggered by its activateAfter. |
| ExpectFetchLaterRequests(1, request_handlers); |
| } |
| |
| // When setting activateAfter>0, a pending FetchLater request should be sent |
| // after around the specified time, if no navigation happens. |
| IN_PROC_BROWSER_TEST_F(FetchLaterActivationTimeoutBrowserTest, |
| SendOnActivationTimeout) { |
| const std::string target_url = kFetchLaterEndpoint; |
| auto request_handlers = RegisterRequestHandlers({target_url}); |
| ASSERT_TRUE(server()->Start()); |
| |
| // Creates a FetchLater request with activateAfter=2s. |
| // It should be sent out after 2s. |
| RunScript(JsReplace(R"( |
| fetchLater($1, {activateAfter: 2000}); |
| )", |
| target_url)); |
| ASSERT_FALSE(current_document().IsDestroyed()); |
| |
| // The loader should still exist as the page exists. |
| EXPECT_EQ(loader_service()->NumDisconnectedLoadersForTesting(), 0u); |
| // The FetchLater request should be sent, triggered by its activateAfter. |
| ExpectFetchLaterRequests(1, request_handlers); |
| } |
| |
| // A pending FetchLater request should be sent when its page is evicted out of |
| // BackForwardCache. |
| IN_PROC_BROWSER_TEST_F(FetchLaterActivationTimeoutBrowserTest, |
| SendOnBackForwardCachedEviction) { |
| const std::string target_url = kFetchLaterEndpoint; |
| auto request_handlers = RegisterRequestHandlers({target_url}); |
| ASSERT_TRUE(server()->Start()); |
| |
| // Creates a FetchLater request with long activateAfter (3min) |
| RunScriptAndNavigateAway(JsReplace(R"( |
| fetchLater($1, {activateAfter: 180000}); |
| )", |
| target_url)); |
| ASSERT_TRUE(previous_document()->IsInBackForwardCache()); |
| // Forces evicting previous page. This will also post a task that destroys it. |
| DisableBFCacheForRFHForTesting(previous_document()->GetGlobalId()); |
| ASSERT_TRUE(previous_document()->is_evicted_from_back_forward_cache()); |
| // Eviction happens immediately, but RFH deletion may be delayed. |
| ASSERT_TRUE(previous_document().WaitUntilRenderFrameDeleted()); |
| |
| // The loader is disconnected after the page is evicted by browser process to |
| // start loading the request. However, it may happen earlier or later, so it's |
| // difficult to assert the existence of the disconnected loader. |
| |
| // At the end, the FetchLater request should be sent, and the loader is |
| // expected to process the response. |
| ExpectFetchLaterRequests(1, request_handlers); |
| } |
| |
| // All other send-on-BFCache behaviors are covered in |
| // send-on-deactivate.tentative.https.window.js |
| |
| class KeepAliveURLAttributionReportingBrowserTest |
| : public KeepAliveURLBrowserTest { |
| protected: |
| void SetUp() override { |
| // Attribution Reporting API only supports HTTPS requests. |
| SetUseHttps(); |
| KeepAliveURLBrowserTest::SetUp(); |
| } |
| |
| void SetUpOnMainThread() override { |
| auto mock_manager = std::make_unique<MockAttributionManager>(); |
| auto mock_data_host_manager = |
| std::make_unique<MockAttributionDataHostManager>(); |
| mock_manager->SetDataHostManager(std::move(mock_data_host_manager)); |
| static_cast<StoragePartitionImpl*>( |
| web_contents()->GetBrowserContext()->GetDefaultStoragePartition()) |
| ->OverrideAttributionManagerForTesting(std::move(mock_manager)); |
| |
| KeepAliveURLBrowserTest::SetUpOnMainThread(); |
| } |
| |
| const FeaturesType& GetEnabledFeatures() override { |
| static const FeaturesType enabled_features = |
| GetDefaultEnabledBackForwardCacheFeaturesForTesting( |
| {{blink::features::kKeepAliveInBrowserMigration, {}}, |
| {blink::features::kAttributionReportingInBrowserMigration, {}}}); |
| return enabled_features; |
| } |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P( |
| All, |
| KeepAliveURLAttributionReportingBrowserTest, |
| ::testing::Values(net::HttpRequestHeaders::kGetMethod, |
| net::HttpRequestHeaders::kPostMethod), |
| [](const testing::TestParamInfo<KeepAliveURLBrowserTest::ParamType>& info) { |
| return info.param; |
| }); |
| |
| IN_PROC_BROWSER_TEST_P(KeepAliveURLAttributionReportingBrowserTest, |
| ReceiveViolatingCSPRedirect_NotForwarded) { |
| const std::string method = GetParam(); |
| const char violating_csp_redirect_target[] = |
| "http://b.test/beacon-redirected"; |
| auto request_handler = |
| std::move(RegisterRequestHandlers({kKeepAliveEndpoint})[0]); |
| ASSERT_TRUE(server()->Start()); |
| const GURL allowed_csp_url = server()->GetURL(kAllowedCspHost, "/"); |
| |
| auto* data_host_manager = static_cast<MockAttributionDataHostManager*>( |
| AttributionManager::FromWebContents(web_contents()) |
| ->GetDataHostManager()); |
| EXPECT_CALL(*data_host_manager, NotifyBackgroundRegistrationStarted).Times(1); |
| EXPECT_CALL(*data_host_manager, NotifyBackgroundRegistrationData).Times(0); |
| EXPECT_CALL(*data_host_manager, NotifyBackgroundRegistrationCompleted) |
| .Times(1); |
| |
| // Set up redirects according to the following redirect chain: |
| // fetch("http://a.test:<port>/beacon", keepalive: true) |
| // --> http://b.test/beacon-redirected |
| ASSERT_NO_FATAL_FAILURE( |
| LoadPageWithKeepAliveRequestAndSendResponseAfterUnload( |
| GetKeepAlivePageURL( |
| method, /*num_requests=*/1, |
| GetConnectSrcCSPHeader(url::Origin::Create(allowed_csp_url))), |
| request_handler.get(), |
| base::StringPrintf(k301Response, violating_csp_redirect_target))); |
| |
| // The redirect doesn't match CSP source from the 1st page, so the loader is |
| // terminated. |
| // While the 1st page is unloaded, the disconnection may not propagate to |
| // browser process in time, such that calling |
| // `WaitforTotalCompleteProcessed()` here might be flaky. |
| loaders_observer().WaitForTotalOnComplete({net::ERR_BLOCKED_BY_CSP}); |
| } |
| |
| } // namespace content |