910e62b5创建于 1月15日历史提交
// Copyright 2025 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 <string>
#include <vector>

#include "base/strings/stringprintf.h"
#include "content/browser/loader/keep_alive_request_browsertest_util.h"
#include "content/browser/loader/keep_alive_url_loader_service.h"
#include "content/browser/web_contents/web_contents_impl.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/keep_alive_url_loader_utils.h"
#include "net/test/embedded_test_server/controllable_http_response.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/features.h"
#include "url/gurl.h"

namespace content {

class SendBeaconBrowserTestBase : public KeepAliveRequestBrowserTestBase {
 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<SendBeaconBrowserTest::ParamType>& info) {
      return info.param;
    });

// 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.
// TODO(crbug.com/412499381): Re-enable this test.
IN_PROC_BROWSER_TEST_P(SendBeaconBrowserTest,
                       DISABLED_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).
// TODO(crbug.com/412499381): Re-enable this test.
IN_PROC_BROWSER_TEST_P(
    SendBeaconBrowserTest,
    DISABLED_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/412499381): Re-enable this test.
IN_PROC_BROWSER_TEST_P(SendBeaconBrowserTest,
                       DISABLED_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));
}

}  // namespace content