// 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 "content/browser/loader/keep_alive_url_loader.h"

#include <memory>
#include <utility>
#include <vector>

#include "base/check_is_test.h"
#include "base/feature_list.h"
#include "base/features.h"
#include "base/functional/bind.h"
#include "base/memory/scoped_refptr.h"
#include "base/metrics/histogram.h"
#include "base/metrics/histogram_base.h"
#include "base/metrics/histogram_functions.h"
#include "base/no_destructor.h"
#include "base/strings/strcat.h"
#include "base/trace_event/trace_event.h"
#include "base/trace_event/typed_macros.h"
#include "base/unguessable_token.h"
#include "content/browser/devtools/devtools_instrumentation.h"
#include "content/browser/loader/keep_alive_attribution_request_helper.h"
#include "content/browser/renderer_host/mixed_content_checker.h"
#include "content/browser/renderer_host/policy_container_host.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/keep_alive_request_tracker.h"
#include "content/public/common/content_client.h"
#include "content/public/common/url_utils.h"
#include "net/base/load_flags.h"
#include "net/http/http_request_headers.h"
#include "net/http/http_response_headers.h"
#include "net/url_request/redirect_util.h"
#include "services/network/public/cpp/content_security_policy/content_security_policy.h"
#include "services/network/public/cpp/content_security_policy/csp_context.h"
#include "services/network/public/cpp/devtools_observer_util.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/url_loader_completion_status.h"
#include "services/network/public/mojom/content_security_policy.mojom.h"
#include "services/network/public/mojom/early_hints.mojom.h"
#include "services/network/public/mojom/fetch_api.mojom-shared.h"
#include "services/network/public/mojom/url_request.mojom.h"
#include "third_party/abseil-cpp/absl/container/flat_hash_map.h"
#include "third_party/blink/public/common/features.h"

namespace features {

// See third_party/blink/renderer/core/fetch/retry_options.idl for details of
// each parameter.
// Note that we allow script to set their own policies for retry, but these
// browser-configured limits act as both lower/upper bounds and also default
// values if not set by script.

const base::FeatureParam<size_t> kMaxRetryCount{&blink::features::kFetchRetry,
                                                "max_retry_count", 10};
const base::FeatureParam<base::TimeDelta> kMinRetryDelta{
    &blink::features::kFetchRetry, "min_retry_delta", base::Milliseconds(500)};
const base::FeatureParam<double> kMinRetryBackoffFactor{
    &blink::features::kFetchRetry, "min_retry_backoff", 1.0};
const base::FeatureParam<base::TimeDelta> kMaxRetryAge{
    &blink::features::kFetchRetry, "max_retry_age", base::Days(1)};
// TODO(crbug.com/417930271): This should be reviewed beyond OT.
const base::FeatureParam<bool> kAddRetryHeader{&blink::features::kFetchRetry,
                                               "add_retry_header", true};

}  // namespace features

namespace content {
namespace {

// Very simple ThreadChecker to use as a static variable. Used because using
// `base::ThreadChecker` directly is not permitted and the static keyword cannot
// be applied to THREAD_CHECKER. When not under DCHECK this class will be empty
// and do nothing.
class WrappedThreadChecker {
 public:
  void Check() { DCHECK_CALLED_ON_VALID_THREAD(thread_checker); }

 private:
  THREAD_CHECKER(thread_checker);
};

constexpr net::NetworkTrafficAnnotationTag kKeepAliveRetryAnnotationTag =
    net::DefineNetworkTrafficAnnotation("keepalive_fetch_retry", R"(
  semantics {
    sender: "KeepAlive Fetch Retry Mechanism"
    description:
      "This request is an automated retry of a fetch request that was "
      "originally initiated by a website with the `keepalive: true` flag "
      "and `retryOptions` specifying that retries should occur. This "
      "retry is triggered by the browser process if the initial attempt or "
      "a subsequent retry fails, and can occur even if the originating "
      "renderer process has been closed. The purpose is to increase the "
      "likelihood of successful data delivery for critical beacons, logs, "
      "or other data the website intended to send reliably."
    trigger:
      "An initial keepalive fetch request (or a previous retry of it) "
      "failed due to a network error or a specific HTTP 5xx server error "
      "(if configured in RetryOptions). The RetryOptions associated with "
      "the original request dictated that a retry should be attempted. "
      "This specific request is triggered by an automated timer within the "
      "browser process's KeepAliveURLLoader after a calculated backoff period."
    data:
      "The same HTTP method, URL, headers, and body as the original "
      "keepalive request that is being retried. This data is determined "
      "by the website that initiated the original fetch request."
    destination: WEBSITE
    internal {
        contacts {
          email: "chrome-loading@google.com"
        }
      }
    user_data {
      type: ARBITRARY_DATA
      type: SENSITIVE_URL
    }
    last_reviewed: "2025-05-27"

  }
  policy {
    cookies_allowed: YES
    cookies_store: "Same as the original request. Governed by the "
                   "credentials mode ('omit', 'same-origin', 'include') "
                   "of the original fetch request."
    setting:
      "This retry mechanism is a sub-feature of the Fetch API's "
      "`keepalive` and the proposed `retryOptions`. Websites enable this "
      "by constructing fetch requests with these options. Users can "
      "disable JavaScript or clear/block cookies for sites, which would "
      "affect the original request and consequently prevent retries. "
      "There isn't a separate browser setting to disable only the retry "
      "aspect of keepalive fetches."
    policy_exception_justification:
      "This is an automated follow-up to a user-initiated (via website "
      "JavaScript) keepalive request."
  }
)");

// Internally enforces a limit to allow a loader outlive its renderer after
// receiving disconnection notification from the renderer.
//
// Defaults to 30s, the same as pre-migration's timeout.
constexpr base::TimeDelta kDefaultDisconnectedKeepAliveURLLoaderTimeout =
    base::Seconds(30);

base::TimeDelta GetDisconnectedKeepAliveURLLoaderTimeout() {
  return base::Seconds(GetFieldTrialParamByFeatureAsInt(
      blink::features::kKeepAliveInBrowserMigration,
      "disconnected_loader_timeout_seconds",
      base::checked_cast<int32_t>(
          kDefaultDisconnectedKeepAliveURLLoaderTimeout.InSeconds())));
}

// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
//
// Must remain in sync with FetchLaterBrowserMetricType in
// tools/metrics/histograms/enums.xml.
enum class FetchLaterBrowserMetricType {
  kAbortedByInitiator = 0,
  kStartedAfterInitiatorDisconnected = 1,
  kStartedByInitiator = 2,
  kCancelledAfterTimeLimit = 3,
  kStartedWhenShutdown = 4,
  kMaxValue = kStartedWhenShutdown,
};

void LogFetchLaterMetric(const FetchLaterBrowserMetricType& type) {
  base::UmaHistogramEnumeration("FetchLater.Browser.Metrics", type);
}

// A ContentSecurityPolicy context for KeepAliveURLLoader.
class KeepAliveURLLoaderCSPContext final : public network::CSPContext {
 public:
  // network::CSPContext override:
  void ReportContentSecurityPolicyViolation(
      network::mojom::CSPViolationPtr violation_params) final {
    // TODO(crbug.com/40236167): Support reporting violation w/o renderer.
  }
  void SanitizeDataForUseInCspViolation(
      network::mojom::CSPDirectiveName directive,
      GURL* blocked_url,
      network::mojom::SourceLocation* source_location) const final {
    // TODO(crbug.com/40236167): Support reporting violation w/o renderer.
  }
};

// Checks if `url` is allowed by the set of Content-Security-Policy `policies`.
// Violation will not be reported back to renderer, as this function must be
// called after renderer is gone.
// TODO(crbug.com/40263403): Isolated world's CSP is not handled.
bool IsRedirectAllowedByCSP(
    const std::vector<network::mojom::ContentSecurityPolicyPtr>& policies,
    const GURL& url,
    const GURL& url_before_redirects,
    bool has_followed_redirect) {
  // Sets the CSP Directive for fetch() requests. See
  // https://w3c.github.io/webappsec-csp/#directive-connect-src
  // https://fetch.spec.whatwg.org/#destination-table
  auto directive = network::mojom::CSPDirectiveName::ConnectSrc;
  // Sets empty as source location is only used when reporting back to renderer.
  auto empty_source_location = network::mojom::SourceLocation::New();
  auto disposition = network::CSPContext::CheckCSPDisposition::CHECK_ALL_CSP;

  // When reaching here, renderer should have be gone, or at least
  // `KeepAliveURLLoader::forwarding_client_` is disconnected.
  return KeepAliveURLLoaderCSPContext()
      .IsAllowedByCsp(policies, directive, url, url_before_redirects,
                      has_followed_redirect, empty_source_location, disposition,
                      /*is_form_submission=*/false)
      .IsAllowed();
}

bool IsNetErrorEligibleForRetry(int net_error) {
  // Check if the error we encountered is likely transient / can succeed with
  // another attempt.
  return  // Generic transient errors.
      net_error == net::ERR_TIMED_OUT ||
      net_error == net::ERR_CONNECTION_TIMED_OUT ||
      net_error == net::ERR_CONNECTION_CLOSED ||
      net_error == net::ERR_CONNECTION_REFUSED ||
      net_error == net::ERR_CONNECTION_RESET ||
      net_error == net::ERR_CONNECTION_FAILED ||
      net_error == net::ERR_ADDRESS_UNREACHABLE ||
      net_error == net::ERR_NETWORK_CHANGED ||
      // Proxy/tunnel-specific connection issues.
      net_error == net::ERR_TUNNEL_CONNECTION_FAILED ||
      net_error == net::ERR_PROXY_CONNECTION_FAILED ||
      net_error == net::ERR_SOCKS_CONNECTION_FAILED ||
      net_error == net::ERR_HTTP2_PING_FAILED ||
      net_error == net::ERR_HTTP2_PROTOCOL_ERROR ||
      net_error == net::ERR_QUIC_PROTOCOL_ERROR ||
      // DNS failures.
      net_error == net::ERR_NAME_NOT_RESOLVED ||
      net_error == net::ERR_INTERNET_DISCONNECTED ||
      net_error == net::ERR_NAME_RESOLUTION_FAILED;
}

bool IsServerGuaranteedToBeNotReachedYet(int net_error) {
  return net_error == net::ERR_CONNECTION_REFUSED ||
         net_error == net::ERR_ADDRESS_UNREACHABLE ||
         net_error == net::ERR_TUNNEL_CONNECTION_FAILED ||
         net_error == net::ERR_PROXY_CONNECTION_FAILED ||
         net_error == net::ERR_SOCKS_CONNECTION_FAILED ||
         net_error == net::ERR_NAME_NOT_RESOLVED ||
         net_error == net::ERR_NAME_RESOLUTION_FAILED;
}

}  // namespace

// A wrapper class around the target URLLoaderClient connected to Renderer,
// where the owning KeepAliveURLLoader forwards the network loading results to.
class KeepAliveURLLoader::ForwardingClient final
    : public network::mojom::URLLoaderClient {
 public:
  ForwardingClient(
      KeepAliveURLLoader* loader,
      mojo::PendingRemote<network::mojom::URLLoaderClient> forwarding_client)
      : keep_alive_url_loader_(std::move(loader)),
        target_(std::move(forwarding_client)) {
    CHECK(keep_alive_url_loader_);
    // For FetchLater requests, `target_` is not bound to renderer.
    if (target_) {
      target_.set_disconnect_handler(base::BindOnce(
          &ForwardingClient::OnDisconnected, base::Unretained(this)));
    }
  }
  // Not copyable.
  ForwardingClient(const ForwardingClient&) = delete;
  ForwardingClient& operator=(const ForwardingClient&) = delete;

  int32_t request_id() const { return keep_alive_url_loader_->request_id_; }
  bool IsConnected() const { return !!target_; }
  void OnDisconnected();

  // network::mojom::URLLoaderClient overrides:
  void OnReceiveEarlyHints(network::mojom::EarlyHintsPtr early_hints) override {
    DCHECK_CURRENTLY_ON(BrowserThread::UI);
    TRACE_EVENT("loading",
                "KeepAliveURLLoader::ForwardingClient::OnReceiveEarlyHints",
                "request_id", request_id());

    if (IsConnected()) {
      // The renderer is alive, forwards the action.
      target_->OnReceiveEarlyHints(std::move(early_hints));
      return;
    }
  }

  void OnUploadProgress(int64_t current_position,
                        int64_t total_size,
                        base::OnceCallback<void()> callback) override {
    DCHECK_CURRENTLY_ON(BrowserThread::UI);
    TRACE_EVENT("loading",
                "KeepAliveURLLoader::ForwardingClient::OnUploadProgress",
                "request_id", request_id());

    if (IsConnected()) {
      // The renderer is alive, forwards the action.
      target_->OnUploadProgress(current_position, total_size,
                                std::move(callback));
      return;
    }
  }

  void OnTransferSizeUpdated(int32_t transfer_size_diff) override {
    DCHECK_CURRENTLY_ON(BrowserThread::UI);
    TRACE_EVENT("loading",
                "KeepAliveURLLoader::ForwardingClient::OnTransferSizeUpdated",
                "request_id", request_id());

    if (IsConnected()) {
      // The renderer is alive, forwards the action.
      target_->OnTransferSizeUpdated(transfer_size_diff);
      return;
    }
  }

#if BUILDFLAG(ARKWEB_RESOURCE_INTERCEPTION)
    void OnTransferDataWithSharedMemory(base::ReadOnlySharedMemoryRegion region, uint64_t buffer_size) override {}
#endif

  void OnReceiveRedirect(const net::RedirectInfo& redirect_info,
                         network::mojom::URLResponseHeadPtr head) override {
    DCHECK_CURRENTLY_ON(BrowserThread::UI);
    CHECK(IsConnected());
    target_->OnReceiveRedirect(redirect_info, std::move(head));
  }

  void OnReceiveResponse(
      network::mojom::URLResponseHeadPtr head,
      mojo::ScopedDataPipeConsumerHandle body,
      std::optional<mojo_base::BigBuffer> cached_metadata) override {
    DCHECK_CURRENTLY_ON(BrowserThread::UI);
    CHECK(IsConnected());
    target_->OnReceiveResponse(std::move(head), std::move(body),
                               std::move(cached_metadata));
  }

  void OnComplete(
      const network::URLLoaderCompletionStatus& completion_status) override {
    DCHECK_CURRENTLY_ON(BrowserThread::UI);
    CHECK(IsConnected());
    target_->OnComplete(completion_status);
  }

 private:
  // Cannot be nullptr as it owns `this`.
  const raw_ptr<KeepAliveURLLoader> keep_alive_url_loader_;
  // The target where overridden the `network::mojom::URLLoaderClient` methods
  // should forward to.
  // Its receiver resides in the Renderer.
  mojo::Remote<network::mojom::URLLoaderClient> target_;
};

// Stores the chain of redriects, response, and completion status, such that
// they can be forwarded to renderer after handled in browser first.
// See also `ForwardURLLoad()`.
struct KeepAliveURLLoader::StoredURLLoad {
  StoredURLLoad() = default;

  // Not copyable.
  StoredURLLoad(const StoredURLLoad&) = delete;
  StoredURLLoad& operator=(const StoredURLLoad&) = delete;

  // Stores data for a redirect received from `OnReceiveRedirect()`.
  struct RedirectData {
    RedirectData(const net::RedirectInfo& redirect_info,
                 network::mojom::URLResponseHeadPtr response_head)
        : info(redirect_info), head(std::move(response_head)) {}
    // Not copyable.
    RedirectData(const RedirectData&) = delete;
    RedirectData& operator=(const RedirectData&) = delete;

    // A copy of the RedirectInfo.
    net::RedirectInfo info;
    // The original URLResponseHead not yet passed to renderer.
    network::mojom::URLResponseHeadPtr head;
  };

  // Stores data for a response received from `OnReceiveResponse()`.
  struct ResponseData {
    ResponseData(network::mojom::URLResponseHeadPtr response_head,
                 mojo::ScopedDataPipeConsumerHandle body_handle,
                 std::optional<mojo_base::BigBuffer> cached_metadata)
        : head(std::move(response_head)),
          body(std::move(body_handle)),
          metadata(std::move(cached_metadata)) {}
    // Not copyable.
    ResponseData(const ResponseData&) = delete;
    ResponseData& operator=(const ResponseData&) = delete;

    // The original URLResponseHead not yet passed to renderer.
    network::mojom::URLResponseHeadPtr head;
    // The original body handle not yet passed to renderer.
    mojo::ScopedDataPipeConsumerHandle body;
    // The original cached metadata not yet passed to renderer.
    std::optional<mojo_base::BigBuffer> metadata;
  };

  // Stores all intermediate redirect data received from `OnReceiveRedirect()`.
  std::queue<std::unique_ptr<RedirectData>> redirects;
  // Stores the response data received from `OnReceiveResponse()` for later use
  // in renderer.
  std::unique_ptr<ResponseData> response = nullptr;
  // Stores the completion status received from `OnComplete()` for later use in
  // renderer.
  std::optional<network::URLLoaderCompletionStatus> completion_status =
      std::nullopt;
  // Tells whether any of the above field has been used (forwarded to renderer).
  bool forwarding = false;
};

KeepAliveURLLoader::KeepAliveURLLoader(
    int32_t request_id,
    uint32_t options,
    const network::ResourceRequest& resource_request,
    mojo::PendingRemote<network::mojom::URLLoaderClient> forwarding_client,
    const net::MutableNetworkTrafficAnnotationTag& traffic_annotation,
    scoped_refptr<network::SharedURLLoaderFactory> network_loader_factory,
    scoped_refptr<PolicyContainerHost> policy_container_host,
    WeakDocumentPtr weak_document_ptr,
    net::NetworkIsolationKey network_isolation_key,
    std::optional<ukm::SourceId> ukm_source_id,
    StoragePartitionImpl* storage_partition,
    URLLoaderThrottlesGetter throttles_getter,
    base::PassKey<KeepAliveURLLoaderService>,
    std::unique_ptr<KeepAliveAttributionRequestHelper>
        attribution_request_helper)
    : request_id_(request_id),
      devtools_request_id_(base::UnguessableToken::Create().ToString()),
      options_(options),
      resource_request_(resource_request),
      forwarding_client_(
          std::make_unique<ForwardingClient>(this,
                                             std::move(forwarding_client))),
      traffic_annotation_(net::NetworkTrafficAnnotationTag(traffic_annotation)),
      network_loader_factory_(std::move(network_loader_factory)),
      stored_url_load_(std::make_unique<StoredURLLoad>()),
      policy_container_host_(std::move(policy_container_host)),
      weak_document_ptr_(std::move(weak_document_ptr)),
      network_isolation_key_(network_isolation_key),
      ukm_source_id_(ukm_source_id),
      request_tracker_(
          GetContentClient()->browser()->MaybeCreateKeepAliveRequestTracker(
              resource_request,
              ukm_source_id,
              base::BindRepeating(&KeepAliveURLLoader::IsContextDetached,
                                  // `this` owns `request_tracker_`, so it is
                                  // safe to use.
                                  base::Unretained(this)))),
      storage_partition_(storage_partition),
      initial_url_(resource_request.url),
      last_url_(resource_request.url),
      throttles_getter_(throttles_getter),
      attribution_request_helper_(std::move(attribution_request_helper)) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  CHECK(network_loader_factory_);
  CHECK(policy_container_host_);
  CHECK(!resource_request.trusted_params);
  CHECK(storage_partition_);
  TRACE_EVENT("loading", "KeepAliveURLLoader::KeepAliveURLLoader", "request_id",
              request_id_, "url", last_url_);
  TRACE_EVENT_BEGIN("loading", "KeepAliveURLLoader",
                    perfetto::Track(request_id_), "url", last_url_);

  original_resource_request_ = resource_request_;

  LogFetchKeepAliveRequestMetric("Total");
  if (IsFetchLater()) {
    base::UmaHistogramBoolean("FetchLater.Browser.Total", true);
  }
}

void KeepAliveURLLoader::Start() {
  StartInternal(/*is_retry=*/false);
}

void KeepAliveURLLoader::StartInternal(bool is_retry) {
  CHECK(!is_started_ || is_retry);
  TRACE_EVENT("loading", "KeepAliveURLLoader::Start", "request_id",
              request_id_);
  is_started_ = true;
  if (!is_retry) {
    first_request_start_time_ = base::TimeTicks::Now();
  }

  LogFetchKeepAliveRequestMetric(is_retry ? "Retried" : "Started");
  if (request_tracker_) {
    request_tracker_->AdvanceToNextStage(
        KeepAliveRequestTracker::RequestStageType::kRequestStarted);
  }
  if (IsFetchLater()) {
    if (is_retry) {
      base::UmaHistogramBoolean("FetchLater.Browser.Total.Retried", true);
    } else {
      base::UmaHistogramBoolean("FetchLater.Browser.Total.Started", true);
    }
    // Logs to DevTools only if the initiator is still alive.
    if (RenderFrameHostImpl* rfh = GetInitiator(); rfh) {
      devtools_instrumentation::OnFetchKeepAliveRequestWillBeSent(
          rfh->frame_tree_node(), devtools_request_id_, resource_request_);
    }
  }

  GetContentClient()->browser()->OnKeepaliveRequestStarted(
      storage_partition_->browser_context());

  // Asks the network service to create a URL loader with passed in params.
  url_loader_ = blink::ThrottlingURLLoader::CreateLoaderAndStart(
      network_loader_factory_, throttles_getter_.Run(), request_id_, options_,
      &resource_request_, forwarding_client_.get(),
      is_retry ? kKeepAliveRetryAnnotationTag : traffic_annotation_,
      base::SingleThreadTaskRunner::GetCurrentDefault(),
      /*cors_exempt_header_list=*/std::nullopt,
      // `this`'s lifetime is at least the same as `url_loader_`.
      /*client_receiver_delegate=*/this);

  // `url_loader_` now re-runs a subset of throttles that have been run
  // in renderer, which is necessary to handle in-browser redirects.
  // There is already a similar use case that also runs throttles in browser in
  // `SearchPrefetchRequest::StartPrefetchRequest()`. The review discussion in
  // https://crrev.com/c/2552723/3 suggests that running them again in browser
  // is fine.
}

KeepAliveURLLoader::~KeepAliveURLLoader() {
  TRACE_EVENT("loading", "KeepAliveURLLoader::~KeepAliveURLLoader",
              "request_id", request_id_);
  // End "KeepAliveURLLoader" trace event.
  TRACE_EVENT_END("loading", perfetto::Track(request_id_));

  // Allows logging to start as early as possible.
  request_tracker_.reset();
  disconnected_loader_timer_.Stop();
  if (IsStarted()) {
    GetContentClient()->browser()->OnKeepaliveRequestFinished();
  }
}

void KeepAliveURLLoader::set_on_delete_callback(
    OnDeleteCallback on_delete_callback) {
  on_delete_callback_ = std::move(on_delete_callback);
}

void KeepAliveURLLoader::set_check_retry_eligibility_callback(
    CheckRetryEligibilityCallback check_retry_eligibility_callback) {
  check_retry_eligibility_callback_ =
      std::move(check_retry_eligibility_callback);
}

void KeepAliveURLLoader::set_on_retry_scheduled_callback(
    OnRetryScheduledCallback on_retry_scheduled_callback) {
  on_retry_scheduled_callback_ = std::move(on_retry_scheduled_callback);
}

base::WeakPtr<KeepAliveURLLoader> KeepAliveURLLoader::GetWeakPtr() {
  return weak_ptr_factory_.GetWeakPtr();
}

bool KeepAliveURLLoader::IsStarted() const {
  return is_started_;
}

bool KeepAliveURLLoader::IsAttemptingRetry(bool include_failed_retry) const {
  return retry_state_ != RetryState::kNotAttemptingRetry &&
         (include_failed_retry || retry_state_ != RetryState::kRetryFailed);
}

RenderFrameHostImpl* KeepAliveURLLoader::GetInitiator() const {
  return static_cast<RenderFrameHostImpl*>(
      weak_document_ptr_.AsRenderFrameHostIfValid());
}

bool KeepAliveURLLoader::IsContextDetached() const {
  return !GetInitiator();
}

void KeepAliveURLLoader::FollowRedirect(
    const std::vector<std::string>& removed_headers,
    const net::HttpRequestHeaders& modified_headers,
    const net::HttpRequestHeaders& modified_cors_exempt_headers,
    const std::optional<GURL>& new_url) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  TRACE_EVENT("loading", "KeepAliveURLLoader::FollowRedirect", "request_id",
              request_id_, "url", new_url);

  if (new_url != std::nullopt) {
    mojo::ReportBadMessage(
        "Unexpected `new_url` in KeepAliveURLLoader::FollowRedirect(): "
        "must be null");
    return;
  }

  if (IsRendererConnected()) {
    // Continue forwarding the stored data to renderer.
    // Note: we may or may not have response at this point.
    ForwardURLLoad();
    // DO NOT touch any members after this line. `this` may be already deleted
    // if `OnComplete()` has been triggered.
    return;
  }
  // No need to forward anymore as the target renderer is gone.
  DeleteSelf();
  // DO NOT touch any members after this line. `this` is deleted.
}

void KeepAliveURLLoader::SetPriority(net::RequestPriority priority,
                                     int intra_priority_value) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  TRACE_EVENT("loading", "KeepAliveURLLoader::SetPriority", "request_id",
              request_id_);

  // Let `url_loader_` handles how to forward the action to the network service.
  url_loader_->SetPriority(priority, intra_priority_value);
}

void KeepAliveURLLoader::EndReceiveRedirect(
    const net::RedirectInfo& redirect_info,
    network::mojom::URLResponseHeadPtr head) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  did_encounter_redirect_ = true;
  CHECK_GT(redirect_limit_, 0u);
  if (--redirect_limit_ == 0) {
    // Don't process the redirect if we've reached our limit.
    OnComplete(network::URLLoaderCompletionStatus(net::ERR_TOO_MANY_REDIRECTS));
    return;
  }

  TRACE_EVENT("loading", "KeepAliveURLLoader::EndReceiveRedirect", "request_id",
              request_id_);
  if (request_tracker_) {
    // Note: redirect may have been cancelled by throttle before reaching here.
    request_tracker_->AdvanceToNextStage(
        request_tracker_->GetNextRedirectStageType());
  }

  // Throttles from content-embedder has already been run for this redirect.
  // See also the call sequence from renderer:
  // https://docs.google.com/document/d/1ZzxMMBvpqn8VZBZKnb7Go8TWjnrGcXuLS_USwVVRUvY/edit?pli=1#heading=h.d006i46pmq9

  if (IsFetchLater()) {
    // Logs to DevTools only if the initiator is still alive.
    if (auto* rfh = GetInitiator(); rfh) {
      auto redirect_head_info = network::ExtractDevToolsInfo(*head);
      std::pair<const GURL&, const network::mojom::URLResponseHeadDevToolsInfo&>
          redirect_info_for_devtools{last_url_, *redirect_head_info};
      devtools_instrumentation::OnFetchKeepAliveRequestWillBeSent(
          rfh->frame_tree_node(), devtools_request_id_, resource_request_,
          redirect_info_for_devtools);
    }
  }

  scoped_refptr<net::HttpResponseHeaders> headers = head->headers;

  // Stores the redirect data for later use by renderer.
  stored_url_load_->redirects.emplace(
      std::make_unique<StoredURLLoad::RedirectData>(redirect_info,
                                                    std::move(head)));

  // Runs additional redirect checks before proceeding.
  if (net::Error err = WillFollowRedirect(redirect_info); err != net::OK) {
    OnComplete(network::URLLoaderCompletionStatus(err));
    return;
  }

  if (attribution_request_helper_) {
    attribution_request_helper_->OnReceiveRedirect(headers,
                                                   redirect_info.new_url);
  }

  // TODO(crbug.com/40236167): Figure out how to deal with lost
  // ResourceFetcher's counter & dev console logging (renderer is dead).

  resource_request_.url = redirect_info.new_url;
  resource_request_.site_for_cookies = redirect_info.new_site_for_cookies;
  resource_request_.referrer = GURL(redirect_info.new_referrer);
  resource_request_.referrer_policy = redirect_info.new_referrer_policy;
  // Ask the network service to follow the redirect.
  last_url_ = GURL(redirect_info.new_url);
  // TODO(crbug.com/40880984): Remove Authorization header upon cross-origin
  // redirect.
  if (observer_for_testing_) {
    CHECK_IS_TEST();
    observer_for_testing_->OnReceiveRedirectProcessed(this);
  }

  // Asks `url_loader_` to directly forward the action to the network service.
  // The modified headers are stored there, if exists.
  //
  // Note: there may be throttles running in IO thread, which may send signals
  // in between `FollowRedirect()` and the next `OnReceiveRedirect()` or
  // `OnReceiveResponse()`.
  url_loader_->FollowRedirect(
      /*removed_headers=*/{}, /*modified_headers=*/{},
      /*modified_cors_exempt_headers=*/{});
}

void KeepAliveURLLoader::OnReceiveResponse(
    network::mojom::URLResponseHeadPtr response,
    mojo::ScopedDataPipeConsumerHandle body,
    std::optional<mojo_base::BigBuffer> cached_metadata) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  TRACE_EVENT("loading", "KeepAliveURLLoader::OnReceiveResponse", "request_id",
              request_id_, "url", last_url_);

  LogFetchKeepAliveRequestMetric("Succeeded");
  if (request_tracker_) {
    request_tracker_->AdvanceToNextStage(
        KeepAliveRequestTracker::RequestStageType::kResponseReceived);
  }

  if (observer_for_testing_) {
    CHECK_IS_TEST();
    observer_for_testing_->OnReceiveResponse(this);
  }

  if (IsFetchLater()) {
    // Logs to DevTools only if the initiator is still alive.
    if (auto* rfh = GetInitiator(); rfh) {
      devtools_instrumentation::OnFetchKeepAliveResponseReceived(
          rfh->frame_tree_node(), devtools_request_id_, last_url_, *response);
    }
  }

  if (attribution_request_helper_) {
    attribution_request_helper_->OnReceiveResponse(response->headers.get());
    attribution_request_helper_.reset();
  }

  // In case the renderer is alive, the stored response data will be forwarded
  // at the end of `ForwardURLLoad()`.
  stored_url_load_->response = std::make_unique<StoredURLLoad::ResponseData>(
      std::move(response), std::move(body), std::move(cached_metadata));

  if (IsRendererConnected()) {
    // Starts to forward the stored redirects/response to renderer.
    // Note that `OnComplete()` may be triggered in between the forwarding.
    ForwardURLLoad();
    // DO NOT touch any members after this line. `this` may be already deleted
    // if `OnComplete()` has been triggered.
    return;
  }

  if (observer_for_testing_) {
    CHECK_IS_TEST();
    observer_for_testing_->OnReceiveResponseProcessed(this);
  }

  // When reaching here, the loader counterpart in renderer is already
  // disconnected before receiving response. However, the renderer may still be
  // alive, and waiting for DevTools logger to finish with `OnComplete()`. For
  // examples:
  // 1. Calling fetchLater(url, {activateAfter: 0}) will immediately dispose
  //    renderer loader, which is independent of renderer lifetime and requires
  //    OnComplete().
  // 2. Calling fetch(url, {keepalive: true}) in unload may lead to disconnected
  //    renderer loader + dead renderer, which requires no OnComplete().
  //
  // Only for case 1, we want to wait for a limited time to allow `OnComplete()`
  // to trigger, but also prevents this loader from staying alive indefinitely.
  if (IsFetchLater() && !disconnected_loader_timer_.IsRunning()) {
    disconnected_loader_timer_.Start(
        FROM_HERE, GetDisconnectedKeepAliveURLLoaderTimeout(),
        base::BindOnce(&KeepAliveURLLoader::OnDisconnectedLoaderTimerFired,
                       // `this` owns `disconnected_loader_timer_`.
                       base::Unretained(this)));
    return;
  }
  // Otherwise, no need to wait for `OnComplete()`.
  // This loader should be deleted immediately to avoid hanged requests taking
  // up resources.
  DeleteSelf();
  // DO NOT touch any members after this line. `this` is already deleted.
}

void KeepAliveURLLoader::NotifyOnCompleteForTestAndDevTools(
    const network::URLLoaderCompletionStatus& completion_status) {
  if (observer_for_testing_) {
    CHECK_IS_TEST();
    observer_for_testing_->OnComplete(this, completion_status);
  }

  if (IsFetchLater()) {
    // Logs to DevTools only if the initiator is still alive.
    if (RenderFrameHostImpl* rfh = GetInitiator(); rfh) {
      devtools_instrumentation::OnFetchKeepAliveRequestComplete(
          rfh->frame_tree_node(), devtools_request_id_, completion_status);
    }
  }
}

void KeepAliveURLLoader::OnComplete(
    const network::URLLoaderCompletionStatus& completion_status) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  TRACE_EVENT("loading", "KeepAliveURLLoader::OnComplete", "request_id",
              request_id_);
  if (IsAttemptingRetry(/*include_failed_retry=*/false)) {
    retry_state_ = RetryState::kRetryFailed;
  }

  if (request_tracker_) {
    request_tracker_->AdvanceToNextStage(
        KeepAliveRequestTracker::RequestStageType::kLoaderCompleted,
        completion_status);
  }

  if (completion_status.error_code != net::OK) {
    // If the request succeeds, it should've been logged in `OnReceiveResponse`.
    LogFetchKeepAliveRequestMetric("Failed");

    if (RetryOrDelayErrorIfNeeded(
            completion_status,
            base::BindOnce(&KeepAliveURLLoader::OnCompleteInternal,
                           // `this` owns `max_age_handler_timer_`.
                           base::Unretained(this), completion_status))) {
      // Retry or delayed error processing is scheduled. Don't process the
      // cancellation at this time, but still notify test observers and
      // devtools.
      NotifyOnCompleteForTestAndDevTools(completion_status);
      return;
    }
  }

  NotifyOnCompleteForTestAndDevTools(completion_status);
  OnCompleteInternal(completion_status);
}

void KeepAliveURLLoader::OnCompleteInternal(
    const network::URLLoaderCompletionStatus& completion_status) {
  // Note that we don't need to reset the attribution helper if we retry.
  if (completion_status.error_code != net::OK) {
    if (attribution_request_helper_) {
      attribution_request_helper_->OnError();
      attribution_request_helper_.reset();
    }
  }

  // In case the renderer is alive, the stored status will be forwarded
  // at the end of `ForwardURLLoad()`.
  stored_url_load_->completion_status = completion_status;

  if (IsRendererConnected()) {
    if (HasReceivedResponse()) {
      // Do nothing. `completion_status` will be forwarded at the end of
      // `ForwardURLLoad()`.
      return;
    }

    // Either (1) an error happens in between redirect handling in browser, or
    // (2) the redirects and response have all been forwarded.
    // Starts forwarding stored redirects and the completion status to renderer.
    ForwardURLLoad();
    // DO NOT touch any members after this line. `this` is already deleted.
    return;
  }

  // TODO(crbug.com/40236167): Handle in the browser process.
  if (observer_for_testing_) {
    CHECK_IS_TEST();
    observer_for_testing_->OnCompleteProcessed(this, completion_status);
  }

  DeleteSelf();
  // DO NOT touch any members after this line. `this` is already deleted.
}

size_t KeepAliveURLLoader::GetMaxAttemptsForRetry() const {
  CHECK(resource_request_.fetch_retry_options.has_value());
  return std::min(
      features::kMaxRetryCount.Get(),
      static_cast<size_t>(resource_request_.fetch_retry_options->max_attempts));
}

base::TimeDelta KeepAliveURLLoader::GetMaxAgeForRetry() const {
  CHECK(resource_request_.fetch_retry_options.has_value());
  if (!resource_request_.fetch_retry_options->max_age.has_value()) {
    return features::kMaxRetryAge.Get();
  }

  return std::min(features::kMaxRetryAge.Get(),
                  resource_request_.fetch_retry_options->max_age.value());
}

base::TimeDelta KeepAliveURLLoader::GetInitialTimeDeltaForRetry() const {
  CHECK(resource_request_.fetch_retry_options.has_value());
  if (!resource_request_.fetch_retry_options->initial_delay.has_value()) {
    return features::kMinRetryDelta.Get();
  }
  return std::max(features::kMinRetryDelta.Get(),
                  resource_request_.fetch_retry_options->initial_delay.value());
}

double KeepAliveURLLoader::GetBackoffFactorForRetry() const {
  CHECK(resource_request_.fetch_retry_options.has_value());
  if (!resource_request_.fetch_retry_options->backoff_factor.has_value()) {
    return features::kMinRetryBackoffFactor.Get();
  }
  return std::max(
      features::kMinRetryBackoffFactor.Get(),
      resource_request_.fetch_retry_options->backoff_factor.value());
}

base::TimeDelta KeepAliveURLLoader::UpdateNextRetryDelay() {
  if (last_retry_delay_ == base::TimeDelta()) {
    last_retry_delay_ = GetInitialTimeDeltaForRetry();
  } else {
    last_retry_delay_ *= GetBackoffFactorForRetry();
  }
  return last_retry_delay_;
}

bool KeepAliveURLLoader::IsEligibleForRetry(
    std::optional<network::URLLoaderCompletionStatus> completion_status) const {
  auto retry_options = resource_request_.fetch_retry_options;
  if (!retry_options.has_value()) {
    // The fetch must opt-in to retry.
    return false;
  }

  // Don't retry if it's not HTTPs, there's already a retry scheduled, or we're
  // over the limit.
  if (!resource_request_.url.SchemeIs(url::kHttpsScheme) ||
      retry_state_ == RetryState::kRetryScheduled ||
      retry_state_ == RetryState::kWaitingForSameNetworkIsolationKeyDocument ||
      retry_count_ >= GetMaxAttemptsForRetry() ||
      (first_request_start_time_ != base::TimeTicks() &&
       base::TimeTicks::Now() - first_request_start_time_ >
           GetMaxAgeForRetry())) {
    return false;
  }

  if (!retry_options->retry_non_idempotent &&
      !net::HttpUtil::IsMethodIdempotent(resource_request_.method)) {
    // Don't retry non-idempotent method unless the fetch explicitly opted-in to
    // do so.
    return false;
  }

  if (!retry_options->retry_after_unload && IsContextDetached()) {
    return false;
  }

  CHECK(!check_retry_eligibility_callback_.is_null());
  if (!check_retry_eligibility_callback_.Run()) {
    return false;
  }

  if (HasReceivedResponse()) {
    // If we've previously received and stored a response from the server, we
    // can't hold on to the response pipe for too long because it might congest
    // the network. So, just give up from retrying in this case.
    return false;
  }

  if (!completion_status.has_value()) {
    // No completion status. This can only happen when we hit the renderer
    // disconnect timeout before getting any results. The request should be
    // eligible to retry, except if explicitly opting in to retry only if the
    // server is guaranteed to be not reached yet. We can't guarantee that in
    // this case, because we don't know if the server has been reached yet or
    // not.
    return !retry_options->retry_only_if_server_unreached;
  }

  if (completion_status->resolve_error_info.is_secure_network_error) {
    // Don't retry if the error was a secure DNS network error,
    // since the retry may interfere with the captive portal probe state.
    // TODO(crbug.com/40104002): Explore how to allow retries for secure
    // DNS network errors without interfering with the captive portal
    // probe state.
    return false;
  }

  if (retry_options->retry_only_if_server_unreached) {
    // Only retry in this case if we've never encountered redirect yet (since if
    // we've been redirected, we must have reached the redirector server
    // before), and the error indicates that the server is not reached yet.
    return !did_encounter_redirect_ &&
           IsServerGuaranteedToBeNotReachedYet(completion_status->error_code);
  }

  return IsNetErrorEligibleForRetry(completion_status->error_code);
}

bool KeepAliveURLLoader::MaybeScheduleRetry(
    std::optional<network::URLLoaderCompletionStatus> completion_status) {
  if (!IsEligibleForRetry(completion_status)) {
    return false;
  }

  // We can retry. Reset the URLLoader to avoid scheduling another retry if we
  // received another error signal after this (e.g. OnComplete with error
  // happened, then the disconnection triggers CancelWithStatus).
  url_loader_.reset();

  // Set a timer to delete self when the max age has been reached. Note that
  // we check if the timer is already set here, because it could've been set
  // already by a previous retry attempt (so there's no use to set it again),
  // or it was already set to process an error we encountered in a past attempt
  // (which should still be kept, in case this retry attempt went past max
  // age, at which point we should still send that latest error).
  if (!max_age_handler_timer_.IsRunning()) {
    base::TimeDelta current_age =
        (base::TimeTicks::Now() - first_request_start_time_);
    max_age_handler_timer_.Start(
        FROM_HERE, GetMaxAgeForRetry() - current_age,
        base::BindOnce(&KeepAliveURLLoader::DeleteSelf,
                       // `this` owns `max_age_handler_timer_`.
                       base::Unretained(this)));
  }

  // Update the retry-tracking states. Note that there's no need to reset any
  // of the actual request-related state, since the retry is attempted from the
  // last request attempt, and no state has been updated in response of the
  // failed result yet. All states relating to previous attempts (e.g. stored
  // loads storing previous redirects) only contain results from successful
  // redirects/responses so there's no need to reset.
  retry_count_++;
  CHECK_LE(retry_count_, GetMaxAttemptsForRetry());
  retry_state_ = RetryState::kRetryScheduled;

  // Schedule the retry.
  retry_timer_.Start(FROM_HERE, UpdateNextRetryDelay(),
                     base::BindOnce(&KeepAliveURLLoader::AttemptRetryIfAllowed,
                                    base::Unretained(this)));

  CHECK(!on_retry_scheduled_callback_.is_null());
  on_retry_scheduled_callback_.Run();

  return true;
}

void KeepAliveURLLoader::AttemptRetryIfAllowed() {
  if (retry_state_ == RetryState::kRetryFailed) {
    return;
  }
  CHECK(retry_state_ == RetryState::kRetryScheduled ||
        retry_state_ == RetryState::kWaitingForSameNetworkIsolationKeyDocument);
  // Don't retry when there's no active document with a same network isolation
  // key as the initiator of the load, to avoid privacy concerns of revealing
  // information about the user (that their browser is up, and their current
  // IP address) to the destination origin, while there is no active document.
  if (!storage_partition_->GetActiveDocumentCount(network_isolation_key_)) {
    // No active document with the same NetworkIsolationKey exists. Wait until
    // we see such a document, or delete ourselves when we can't attempt a retry
    // anymore (we reached the max age of retries, which will run self-deletion
    // when we first scheduled the retry attempt).
    retry_state_ = RetryState::kWaitingForSameNetworkIsolationKeyDocument;
    return;
  }

  // TODO(crbug.com/417930271): Check if we're offline, and if so, wait until
  // we're online before attempting.
  retry_state_ = RetryState::kRetryInProgress;
  // Reset the request IDs.
  request_id_ = GlobalRequestID::MakeBrowserInitiated().request_id;
  devtools_request_id_ = base::UnguessableToken::Create().ToString();

  // Retry using the original request, even if the failure happens after
  // redirects.
  resource_request_ = original_resource_request_;
  if (features::kAddRetryHeader.Get()) {
    // Add retry information in the header.
    resource_request_.headers.SetHeader(kRetryAttemptsHeader,
                                        base::NumberToString(retry_count_));
  }

  // TODO(crbug.com/417930271): Track the retry as a state in the
  // KeepAliveRequestTracker too.
  request_tracker_ =
      GetContentClient()->browser()->MaybeCreateKeepAliveRequestTracker(
          resource_request_, ukm_source_id_,
          base::BindRepeating(&KeepAliveURLLoader::IsContextDetached,
                              // `this` owns `request_tracker_`, so it is
                              // safe to use.
                              base::Unretained(this)));

  StartInternal(/*is_retry=*/true);
}

void KeepAliveURLLoader::DidObserveNewlyActiveDocumentWithNIK(
    const net::NetworkIsolationKey& nik) {
  if (nik == network_isolation_key_ &&
      retry_state_ == RetryState::kWaitingForSameNetworkIsolationKeyDocument) {
    // We previously wanted to retry but couldn't due to there being no active
    // document with the same Network Isolation Key. Now that we observe such a
    // document, we can attempt the retry.
    retry_state_ = RetryState::kRetryScheduled;
    GetUIThreadTaskRunner({})->PostTask(
        FROM_HERE, base::BindOnce(&KeepAliveURLLoader::AttemptRetryIfAllowed,
                                  base::Unretained(this)));
  }
}

bool KeepAliveURLLoader::HasReceivedResponse() const {
  return stored_url_load_ && stored_url_load_->response != nullptr;
}

void KeepAliveURLLoader::ForwardURLLoad() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  CHECK(IsRendererConnected());
  CHECK(stored_url_load_);

  // Don't delete self if we need to forward response to the renderer. This is
  // ok since it's either not an error, or we've already reached max age and are
  // forwarding the errors.
  max_age_handler_timer_.Stop();

  // Forwards the redirects/response/completion in the exact sequence.
  stored_url_load_->forwarding = true;

  if (!stored_url_load_->redirects.empty()) {
    // All redirects have been handled in the browser. However, redirects must
    // also be processed by the renderer so that it knows what URL the
    // response come from when parsing the response.
    //
    // Note: The renderer might get shut down before
    // `forwarding_client_->OnReceiveRedirect()` finish all redirect handling.
    // In such case, the handling will be taken over by browser from
    // `OnRendererConnectionError()`.
    forwarding_client_->OnReceiveRedirect(
        stored_url_load_->redirects.front()->info,
        std::move(stored_url_load_->redirects.front()->head));
    stored_url_load_->redirects.pop();

    if (observer_for_testing_) {
      CHECK_IS_TEST();
      observer_for_testing_->OnReceiveRedirectForwarded(this);
    }
    // The rest of `stored_url_load_->redirects` will be forwarded in the next
    // visit to this method when `FollowRedirect()` is called by the renderer.
    return;
  }

  if (stored_url_load_->response) {
    // Note: The receiver may fail to finish reading the entire
    // `stored_url_load_->response`response`, so response caching is not
    // guaranteed.
    // Note: The renderer might get shut down before
    // `forwarding_client_->OnReceiveResponse()` finish response handling.
    // In such case, the attributionsrc handling cannot be dropped and should be
    // taken over by browser in `OnRendererConnectionError().
    forwarding_client_->OnReceiveResponse(
        std::move(stored_url_load_->response->head),
        std::move(stored_url_load_->response->body),
        std::move(stored_url_load_->response->metadata));
    stored_url_load_->response = nullptr;

    if (observer_for_testing_) {
      CHECK_IS_TEST();
      observer_for_testing_->OnReceiveResponseForwarded(this);
    }
  }

  if (stored_url_load_->completion_status.has_value()) {
    forwarding_client_->OnComplete(*(stored_url_load_->completion_status));
    if (observer_for_testing_) {
      CHECK_IS_TEST();
      observer_for_testing_->OnCompleteForwarded(
          this, *(stored_url_load_->completion_status));
    }
    stored_url_load_ = nullptr;

    DeleteSelf();
    // DO NOT touch any members after this line. `this` is already deleted.
  }
}

bool KeepAliveURLLoader::IsForwardURLLoadStarted() const {
  return stored_url_load_ && stored_url_load_->forwarding;
}

bool KeepAliveURLLoader::IsRendererConnected() const {
  CHECK(forwarding_client_);
  return forwarding_client_->IsConnected();
}

net::Error KeepAliveURLLoader::WillFollowRedirect(
    const net::RedirectInfo& redirect_info) const {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  // TODO(crbug.com/40236167): Add logic to handle redirecting to extensions
  // from `ChromeContentRendererClient::IsSafeRedirectTarget()`.
  if (!IsSafeRedirectTarget(last_url_, redirect_info.new_url)) {
    return net::ERR_UNSAFE_REDIRECT;
  }

  if (resource_request_.redirect_mode == network::mojom::RedirectMode::kError) {
    return net::ERR_FAILED;
  }

  if (resource_request_.redirect_mode !=
      network::mojom::RedirectMode::kManual) {
    // Checks if redirecting to `redirect_info.new_url` is allowed by
    // ContentSecurityPolicy from the request initiator document.
    if (!IsRedirectAllowedByCSP(
            policy_container_host_->policies().content_security_policies,
            redirect_info.new_url, initial_url_, last_url_ != initial_url_)) {
      return net::ERR_BLOCKED_BY_CSP;
    }

    // Checks if redirecting to `redirect_info.new_url` is allowed by
    // MixedContent checker.
    // TODO(crbug.com/40941240): Figure out how to check without a frame.
    if (auto* rfh = GetInitiator();
        rfh && MixedContentChecker::ShouldBlockFetchKeepAlive(
                   rfh, redirect_info.new_url,
                   /*for_redirect=*/true)) {
      return net::ERR_FAILED;
    }
  }

  return net::OK;
}

bool KeepAliveURLLoader::RetryOrDelayErrorIfNeeded(
    const network::URLLoaderCompletionStatus& status,
    base::OnceClosure closure) {
  auto retry_options = resource_request_.fetch_retry_options;
  if (!retry_options.has_value()) {
    // Ignore fetches that do not opt-in to retry.
    return false;
  }

  // Schedule retry if needed.
  if (MaybeScheduleRetry(status)) {
    return true;
  }

  base::TimeDelta current_age =
      (base::TimeTicks::Now() - first_request_start_time_);
  if (IsRendererConnected() && current_age < GetMaxAgeForRetry()) {
    // A retry is not attempted, but we can only notify the renderer about the
    // error when we reach the max age, to avoid exposing information about the
    // error through timing. Note that we only do this when the renderer is
    // still connected and waiting for the error info. If the renderer is
    // already disconnected, we can just continue processing the error and free
    // up resources by deleting ourself. Note also that this will replace the
    // previous action set in the timer (which is either to send the error from
    // a previous attempt, or to delete self, which should not take precedent
    // over this).
    max_age_handler_timer_.Start(FROM_HERE, GetMaxAgeForRetry() - current_age,
                                 std::move(closure));

    // Reset the URLLoader to avoid receiving another error signal after this
    // (e.g. OnComplete with error happened, then the disconnection triggers
    // CancelWithStatus).
    url_loader_.reset();
    return true;
  }

  return false;
}

void KeepAliveURLLoader::CancelWithStatus(
    const network::URLLoaderCompletionStatus& status) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  TRACE_EVENT("loading", "KeepAliveURLLoader::CancelWithStatus", "request_id",
              request_id_);
  if (IsAttemptingRetry(/*include_failed_retry=*/false)) {
    retry_state_ = RetryState::kRetryFailed;
  }

  if (!stored_url_load_->completion_status.has_value()) {
    // Only logs if there is no error logged by `OnComplete()` yet.
    LogFetchKeepAliveRequestMetric("Failed");
  }

  if (request_tracker_) {
    request_tracker_->AdvanceToNextStage(
        KeepAliveRequestTracker::RequestStageType::kRequestFailed, status);
  }

  if (RetryOrDelayErrorIfNeeded(
          status, base::BindOnce(&KeepAliveURLLoader::CancelWithStatusInternal,
                                 // `this` owns `max_age_handler_timer_`.
                                 base::Unretained(this), status))) {
    // Retry or delayed error processing is scheduled. Don't process the
    // cancellation at this time.
    return;
  }

  CancelWithStatusInternal(status);
}

void KeepAliveURLLoader::CancelWithStatusInternal(
    const network::URLLoaderCompletionStatus& status) {
  // This method can be triggered when one of the followings happen:
  // 1. Network -> `url_loader_` gets disconnected.
  // 2. `url_loader_` gets cancelled by throttles.
  // 3. `url_loader_` terminates itself.

  if (IsRendererConnected()) {
    if (!IsForwardURLLoadStarted()) {
      // The loader is cancelled before this loader forwards anything to
      // renderer. It should make an ateempt to forward any previous loads.
      ForwardURLLoad();
      // DO NOT touch any members after this line. `this` may be deleted.
      return;
    }
    // Otherwise, let `ForwardURLLoad()` continue forwarding the rest.
    return;
  }

  // We should let the renderer know it's closed by deleting `this`.
  DeleteSelf();
  // DO NOT touch any members after this line. `this` is already deleted.
}

// Browser -> Renderer connection
void KeepAliveURLLoader::ForwardingClient::OnDisconnected() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  TRACE_EVENT("loading", "KeepAliveURLLoader::ForwardingClient::OnDisconnected",
              "request_id", request_id());

  // Dropping the client as renderer is gone.
  target_.reset();

  if (!keep_alive_url_loader_->IsForwardURLLoadStarted() &&
      !keep_alive_url_loader_->HasReceivedResponse()) {
    // The renderer disconnects before this loader forwards anything to it.
    // But the in-browser request processing may not complete yet.

    // TODO(crbug.com/40259706): Ensure that attributionsrc response handling is
    // taken over by browser.
    return;
  }

  // Renderer disconnects in-between forwarding, no need to call
  // `ForwardURLLoad()` anymore.
  keep_alive_url_loader_->DeleteSelf();
  // DO NOT touch any members after this line. `this` is already deleted.
}

// Browser <- Renderer connection.
void KeepAliveURLLoader::OnURLLoaderDisconnected() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  TRACE_EVENT("loading", "KeepAliveURLLoader::OnURLLoaderDisconnected",
              "request_id", request_id_);
  if (request_tracker_) {
    request_tracker_->AdvanceToNextStage(
        KeepAliveRequestTracker::RequestStageType::
            kLoaderDisconnectedFromRenderer);
  }
  if (!IsStarted()) {
    // May be the last chance to start a deferred loader.
    LogFetchLaterMetric(
        FetchLaterBrowserMetricType::kStartedAfterInitiatorDisconnected);
    Start();
  }
  // For other types of keepalive requests, this loader does not care about
  // whether messages can be received from renderer or not.

  // Prevents this loader from staying alive indefinitely.
  if (!disconnected_loader_timer_.IsRunning()) {
    disconnected_loader_timer_.Start(
        FROM_HERE, GetDisconnectedKeepAliveURLLoaderTimeout(),
        base::BindOnce(&KeepAliveURLLoader::OnDisconnectedLoaderTimerFired,
                       // `this` owns `disconnected_loader_timer_`.
                       base::Unretained(this)));
  }
}

void KeepAliveURLLoader::OnDisconnectedLoaderTimerFired() {
  if (IsAttemptingRetry(/*include_failed_retry=*/false)) {
    retry_state_ = RetryState::kRetryFailed;
  }
  if (resource_request_.fetch_retry_options.has_value() &&
      resource_request_.fetch_retry_options->retry_after_unload &&
      (IsAttemptingRetry(/*include_failed_retry=*/false) ||
       MaybeScheduleRetry(/*completion_status=*/std::nullopt))) {
    // A retry is already pending or we just scheduled a retry. Don't delete
    // the loader, and instead keep it around for the retry.
    return;
  }

  if (request_tracker_) {
    request_tracker_->AdvanceToNextStage(
        KeepAliveRequestTracker::RequestStageType::
            kRequestCancelledAfterTimeLimit);
  }
  if (IsFetchLater()) {
    LogFetchLaterMetric(FetchLaterBrowserMetricType::kCancelledAfterTimeLimit);
  }
  DeleteSelf();
}

void KeepAliveURLLoader::Shutdown() {
  base::UmaHistogramBoolean(
      "FetchKeepAlive.Requests2.Shutdown.IsStarted.Browser", IsStarted());
  if (request_tracker_) {
    request_tracker_->AdvanceToNextStage(
        KeepAliveRequestTracker::RequestStageType::kBrowserShutdown);
  }
  if (!IsStarted()) {
    CHECK(IsFetchLater());
    LogFetchLaterMetric(FetchLaterBrowserMetricType::kStartedWhenShutdown);
    // At this point, browser is shutting down, and renderer termination has not
    // reached browser. It is the last chance to start loading the request from
    // here.
    Start();
  }
}

bool KeepAliveURLLoader::IsFetchLater() const {
  return base::FeatureList::IsEnabled(blink::features::kFetchLaterAPI) &&
         resource_request_.is_fetch_later_api;
}

void KeepAliveURLLoader::SendNow() {
  if (!IsFetchLater()) {
    mojo::ReportBadMessage("Unexpected call to KeepAliveURLLoader::SendNow()");
    return;
  }
  LogFetchLaterMetric(FetchLaterBrowserMetricType::kStartedByInitiator);
  if (!IsStarted()) {
    Start();
  }
}

void KeepAliveURLLoader::Cancel() {
  if (!IsFetchLater()) {
    mojo::ReportBadMessage("Unexpected call to KeepAliveURLLoader::Cancel()");
    return;
  }
  if (request_tracker_) {
    request_tracker_->AdvanceToNextStage(
        KeepAliveRequestTracker::RequestStageType::kRequestCancelledByRenderer);
  }
  LogFetchLaterMetric(FetchLaterBrowserMetricType::kAbortedByInitiator);
  DeleteSelf();
}

void KeepAliveURLLoader::DeleteSelf() {
  CHECK(on_delete_callback_);
  std::move(on_delete_callback_).Run();
}

void KeepAliveURLLoader::SetObserverForTesting(
    scoped_refptr<TestObserver> observer) {
  observer_for_testing_ = observer;
}

void KeepAliveURLLoader::LogFetchKeepAliveRequestMetric(
    std::string_view request_state_name) {
  if (IsFetchLater()) {
    return;
  }

  auto resource_type =
      static_cast<blink::mojom::ResourceType>(resource_request_.resource_type);
  FetchKeepAliveRequestMetricType sample_type;
  // See also blink::PopulateResourceRequest().
  switch (resource_type) {
    case blink::mojom::ResourceType::kXhr:
      sample_type = FetchKeepAliveRequestMetricType::kFetch;
      break;
    // Includes BEACON/PING/ATTRIBUTION_SRC types
    case blink::mojom::ResourceType::kPing:
      sample_type = FetchKeepAliveRequestMetricType::kPing;
      break;
    case blink::mojom::ResourceType::kCspReport:
      sample_type = FetchKeepAliveRequestMetricType::kReporting;
      break;
    case blink::mojom::ResourceType::kImage:
      sample_type = FetchKeepAliveRequestMetricType::kBackgroundFetchIcon;
      break;
    case blink::mojom::ResourceType::kMainFrame:
    case blink::mojom::ResourceType::kSubFrame:
    case blink::mojom::ResourceType::kStylesheet:
    case blink::mojom::ResourceType::kScript:
    case blink::mojom::ResourceType::kFontResource:
    case blink::mojom::ResourceType::kSubResource:
    case blink::mojom::ResourceType::kObject:
    case blink::mojom::ResourceType::kMedia:
    case blink::mojom::ResourceType::kWorker:
    case blink::mojom::ResourceType::kSharedWorker:
    case blink::mojom::ResourceType::kPrefetch:
    case blink::mojom::ResourceType::kFavicon:
    case blink::mojom::ResourceType::kServiceWorker:
    case blink::mojom::ResourceType::kPluginResource:
    case blink::mojom::ResourceType::kNavigationPreloadMainFrame:
    case blink::mojom::ResourceType::kNavigationPreloadSubFrame:
    case blink::mojom::ResourceType::kJson:
      NOTREACHED();
  }

  CHECK(request_state_name == "Total" || request_state_name == "Started" ||
        request_state_name == "Retried" || request_state_name == "Succeeded" ||
        request_state_name == "Failed");

  const std::string histogram_name = base::StrCat(
      {"FetchKeepAlive.Requests2.", request_state_name, ".Browser"});

  // When under the experiment keep a local cache of resolved histograms to
  // avoid contention on the global lock that is taken by histogram functions.
  if (base::features::IsReducePPMsEnabled()) {
    static base::NoDestructor<
        absl::flat_hash_map<std::string, base::HistogramBase*>>
        histograms;

    // Verify that `histograms` is not read/modified by more than one thread.
    // Since it's static it will be used by any code that calls into the
    // function.
    static WrappedThreadChecker* thread_checker = new WrappedThreadChecker;
    thread_checker->Check();

    auto it = histograms->find(histogram_name);
    if (it != histograms->end()) {
      it->second->Add(static_cast<int32_t>(sample_type));
    } else {
      // TODO(crbug.com/424432184): This is messy and leaks information from
      // LinearHistogram. If the experiment succeeds implement
      // GetUmaHistogramEnumerationFactory before cleaning up the flag.
      int32_t max_value =
          static_cast<int32_t>(FetchKeepAliveRequestMetricType::kMaxValue);
      base::HistogramBase* histo = base::LinearHistogram::FactoryGet(
          histogram_name, /*minimum=*/1,
          /*maximum=*/max_value + 1,
          /*bucket_count=*/max_value + 2,
          base::HistogramBase::kUmaTargetedHistogramFlag);
      histo->Add(static_cast<int32_t>(sample_type));

      (*histograms)[histogram_name] = histo;
    }
  } else {
    base::UmaHistogramEnumeration(histogram_name, sample_type);
  }

  if (bool is_context_detached = !GetInitiator();
      request_state_name == "Started" || request_state_name == "Succeeded") {
    base::UmaHistogramBoolean(
        base::StrCat({"FetchKeepAlive.Requests2.", request_state_name,
                      ".IsContextDetached.Browser"}),
        is_context_detached);
  }
}

}  // namespace content