Introduce WebNavigationBodyLoader

WebNavigationBodyLoader interface (and two implementations)
will be used for loading navigation response body, instead of
going through WebURLLoader. This patch only contains the
implementation, but does not include the usage just yet.

WIP usage can be seen here:
https://chromium-review.googlesource.com/c/chromium/src/+/1388212.

The advantages of a separate code path for navigations:
- Possible to commit navigations synchronously, instead
  of waiting for RawResourceClient callbacks from the loader.
  This is a primary motivation.
- Makes it easier to Onion Soupify navigation code separately
  from loading code.
- All navigation-related special cases can be handled directly
  in DocumentLoader now.
- We don't need a "replay fake request" mode in loader anymore.
- We stop abusing ResourceRequest/ResourceResponse for navigation
  information (e.g. RequestorOrigin, OriginPolicy and many more).

Disadvantages:
- Two URLLoaderClient and body pipe consumer implementations
  to maintain.
- Future loading features might have to touch navigation body
  loader as well. Although most of the navigation-related features
  currently have to touch navigation url request in the browser,
  there is a small chance that NavigationBodyLoader will be
  affected as well.

Larger cleanups possible after this:
- Remove ResourceType::kMainResource and any code related to it.
- Remove ResourceRequest::GetFrameType and any code related to it.

Ideally, NavigationBodyLoader could be implemented directly in Blink,
but there are some dependencies on resource load statistics in content.
Also, Blink is not quite ready to talk to Network Service directly.

This patch corresponds to step 6.g of the doc linked in the bug.

Bug: 855189
Change-Id: I3ac7b33db60e7b50e583e2006dd194c4bf843921
Reviewed-on: https://chromium-review.googlesource.com/c/1399052
Commit-Queue: Dmitry Gozman <dgozman@chromium.org>
Reviewed-by: Kinuko Yasuda <kinuko@chromium.org>
Reviewed-by: Daniel Cheng <dcheng@chromium.org>
Reviewed-by: Arthur Sonzogni <arthursonzogni@chromium.org>
Reviewed-by: Yutaka Hirano <yhirano@chromium.org>
Reviewed-by: Mythri Alle <mythria@chromium.org>
Cr-Commit-Position: refs/heads/master@{#622147}
diff --git a/content/renderer/BUILD.gn b/content/renderer/BUILD.gn
index d86ac48..733ea0d8 100644
--- a/content/renderer/BUILD.gn
+++ b/content/renderer/BUILD.gn
@@ -167,6 +167,8 @@
     "loader/code_cache_loader_impl.h",
     "loader/frame_request_blocker.cc",
     "loader/frame_request_blocker.h",
+    "loader/navigation_body_loader.cc",
+    "loader/navigation_body_loader.h",
     "loader/navigation_response_override_parameters.cc",
     "loader/navigation_response_override_parameters.h",
     "loader/request_extra_data.cc",
diff --git a/content/renderer/loader/code_cache_loader_impl.h b/content/renderer/loader/code_cache_loader_impl.h
index fc4084b6..3366129 100644
--- a/content/renderer/loader/code_cache_loader_impl.h
+++ b/content/renderer/loader/code_cache_loader_impl.h
@@ -13,6 +13,10 @@
 
 namespace content {
 
+// This class is loading V8 compilation code cache for scripts
+// (either separate script resources, or inline scripts in html file).
+// It is talking to the browser process and uses per-site isolated
+// cache backend to avoid cross-origin contamination.
 class CodeCacheLoaderImpl : public blink::CodeCacheLoader {
  public:
   CodeCacheLoaderImpl();
diff --git a/content/renderer/loader/navigation_body_loader.cc b/content/renderer/loader/navigation_body_loader.cc
new file mode 100644
index 0000000..14cc344f
--- /dev/null
+++ b/content/renderer/loader/navigation_body_loader.cc
@@ -0,0 +1,243 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "content/renderer/loader/navigation_body_loader.h"
+
+#include "base/bind.h"
+#include "base/macros.h"
+#include "content/renderer/loader/code_cache_loader_impl.h"
+#include "content/renderer/loader/resource_load_stats.h"
+#include "content/renderer/loader/url_response_body_consumer.h"
+#include "content/renderer/loader/web_url_loader_impl.h"
+#include "services/network/public/cpp/url_loader_completion_status.h"
+
+namespace content {
+
+NavigationBodyLoader::NavigationBodyLoader(
+    mojom::ResourceLoadInfoPtr resource_load_info,
+    const network::ResourceResponseHead& head,
+    network::mojom::URLLoaderClientEndpointsPtr endpoints,
+    scoped_refptr<base::SingleThreadTaskRunner> task_runner,
+    int render_frame_id)
+    : resource_load_info_(std::move(resource_load_info)),
+      head_(head),
+      endpoints_(std::move(endpoints)),
+      task_runner_(task_runner),
+      render_frame_id_(render_frame_id),
+      url_loader_client_binding_(this),
+      handle_watcher_(FROM_HERE,
+                      mojo::SimpleWatcher::ArmingPolicy::MANUAL,
+                      task_runner),
+      weak_factory_(this) {}
+
+NavigationBodyLoader::~NavigationBodyLoader() {
+  if (!has_received_completion_ || !has_seen_end_of_data_) {
+    NotifyResourceLoadCanceled(
+        render_frame_id_, resource_load_info_->request_id,
+        resource_load_info_->url, resource_load_info_->resource_type,
+        net::ERR_ABORTED);
+  }
+}
+
+void NavigationBodyLoader::OnReceiveResponse(
+    const network::ResourceResponseHead& head) {
+  // This has already happened in the browser process.
+  NOTREACHED();
+}
+
+void NavigationBodyLoader::OnReceiveRedirect(
+    const net::RedirectInfo& redirect_info,
+    const network::ResourceResponseHead& head) {
+  // This has already happened in the browser process.
+  NOTREACHED();
+}
+
+void NavigationBodyLoader::OnUploadProgress(int64_t current_position,
+                                            int64_t total_size,
+                                            OnUploadProgressCallback callback) {
+  // This has already happened in the browser process.
+  NOTREACHED();
+}
+
+void NavigationBodyLoader::OnReceiveCachedMetadata(
+    const std::vector<uint8_t>& data) {
+  client_->BodyCodeCacheReceived(data);
+}
+
+void NavigationBodyLoader::OnTransferSizeUpdated(int32_t transfer_size_diff) {
+  NotifyResourceTransferSizeUpdated(
+      render_frame_id_, resource_load_info_->request_id, transfer_size_diff);
+}
+
+void NavigationBodyLoader::OnStartLoadingResponseBody(
+    mojo::ScopedDataPipeConsumerHandle handle) {
+  DCHECK(!has_received_body_handle_);
+  has_received_body_handle_ = true;
+  has_seen_end_of_data_ = false;
+  handle_ = std::move(handle);
+  DCHECK(handle_.is_valid());
+  handle_watcher_.Watch(handle_.get(), MOJO_HANDLE_SIGNAL_READABLE,
+                        base::BindRepeating(&NavigationBodyLoader::OnReadable,
+                                            base::Unretained(this)));
+  OnReadable(MOJO_RESULT_OK);
+}
+
+void NavigationBodyLoader::OnComplete(
+    const network::URLLoaderCompletionStatus& status) {
+  // Except for errors, there must always be a response's body.
+  DCHECK(has_received_body_handle_ || status.error_code != net::OK);
+  has_received_completion_ = true;
+  status_ = status;
+  NotifyCompletionIfAppropriate();
+}
+
+void NavigationBodyLoader::SetDefersLoading(bool defers) {
+  if (is_deferred_ == defers)
+    return;
+  is_deferred_ = defers;
+  OnReadable(MOJO_RESULT_OK);
+}
+
+void NavigationBodyLoader::StartLoadingBody(
+    WebNavigationBodyLoader::Client* client,
+    bool use_isolated_code_cache) {
+  client_ = client;
+
+  NotifyResourceLoadStarted(render_frame_id_, resource_load_info_->request_id,
+                            resource_load_info_->url, head_,
+                            resource_load_info_->resource_type);
+
+  if (use_isolated_code_cache) {
+    code_cache_loader_ = std::make_unique<CodeCacheLoaderImpl>();
+    code_cache_loader_->FetchFromCodeCache(
+        blink::mojom::CodeCacheType::kJavascript, resource_load_info_->url,
+        base::BindOnce(&NavigationBodyLoader::CodeCacheReceived,
+                       weak_factory_.GetWeakPtr()));
+  } else {
+    BindURLLoaderAndContinue();
+  }
+}
+
+void NavigationBodyLoader::CodeCacheReceived(const base::Time& response_time,
+                                             const std::vector<uint8_t>& data) {
+  if (head_.response_time == response_time && client_) {
+    base::WeakPtr<NavigationBodyLoader> weak_self = weak_factory_.GetWeakPtr();
+    client_->BodyCodeCacheReceived(data);
+    if (!weak_self)
+      return;
+  }
+  code_cache_loader_.reset();
+  // TODO(dgozman): we should explore retrieveing code cache in parallel with
+  // receiving response or reading the first data chunk.
+  BindURLLoaderAndContinue();
+}
+
+void NavigationBodyLoader::BindURLLoaderAndContinue() {
+  url_loader_.Bind(std::move(endpoints_->url_loader), task_runner_);
+  url_loader_client_binding_.Bind(std::move(endpoints_->url_loader_client),
+                                  task_runner_);
+  url_loader_client_binding_.set_connection_error_handler(base::BindOnce(
+      &NavigationBodyLoader::OnConnectionClosed, base::Unretained(this)));
+}
+
+void NavigationBodyLoader::OnConnectionClosed() {
+  // If the connection aborts before the load completes, mark it as failed.
+  if (!has_received_completion_)
+    OnComplete(network::URLLoaderCompletionStatus(net::ERR_FAILED));
+}
+
+void NavigationBodyLoader::OnReadable(MojoResult unused) {
+  if (has_seen_end_of_data_ || is_deferred_ || is_in_on_readable_)
+    return;
+  // Protect against reentrancy:
+  // - when the client calls SetDefersLoading;
+  // - when nested message loop starts from BodyDataReceived
+  //   and we get notified by the watcher.
+  // Note: we cannot use AutoReset here since |this| may be deleted
+  // before reset.
+  is_in_on_readable_ = true;
+  base::WeakPtr<NavigationBodyLoader> weak_self = weak_factory_.GetWeakPtr();
+  ReadFromDataPipe();
+  if (!weak_self)
+    return;
+  is_in_on_readable_ = false;
+}
+
+void NavigationBodyLoader::ReadFromDataPipe() {
+  uint32_t num_bytes_consumed = 0;
+  while (!is_deferred_) {
+    const void* buffer = nullptr;
+    uint32_t available = 0;
+    MojoResult result =
+        handle_->BeginReadData(&buffer, &available, MOJO_READ_DATA_FLAG_NONE);
+    if (result == MOJO_RESULT_SHOULD_WAIT) {
+      handle_watcher_.ArmOrNotify();
+      return;
+    }
+    if (result == MOJO_RESULT_FAILED_PRECONDITION) {
+      has_seen_end_of_data_ = true;
+      NotifyCompletionIfAppropriate();
+      return;
+    }
+    if (result != MOJO_RESULT_OK) {
+      status_.error_code = net::ERR_FAILED;
+      has_seen_end_of_data_ = true;
+      has_received_completion_ = true;
+      NotifyCompletionIfAppropriate();
+      return;
+    }
+    DCHECK_LE(num_bytes_consumed,
+              URLResponseBodyConsumer::kMaxNumConsumedBytesInTask);
+    available = std::min(available,
+                         URLResponseBodyConsumer::kMaxNumConsumedBytesInTask -
+                             num_bytes_consumed);
+    if (available == 0) {
+      // We've already consumed many bytes in this task. Defer the remaining
+      // to the next task.
+      result = handle_->EndReadData(0);
+      DCHECK_EQ(result, MOJO_RESULT_OK);
+      handle_watcher_.ArmOrNotify();
+      return;
+    }
+    num_bytes_consumed += available;
+    base::WeakPtr<NavigationBodyLoader> weak_self = weak_factory_.GetWeakPtr();
+    client_->BodyDataReceived(
+        base::make_span(static_cast<const char*>(buffer), available));
+    if (!weak_self)
+      return;
+    result = handle_->EndReadData(available);
+    DCHECK_EQ(MOJO_RESULT_OK, result);
+  }
+}
+
+void NavigationBodyLoader::NotifyCompletionIfAppropriate() {
+  if (!has_received_completion_ || !has_seen_end_of_data_)
+    return;
+
+  handle_watcher_.Cancel();
+
+  GURL url = resource_load_info_->url;
+  resource_load_info_->was_cached = status_.exists_in_cache;
+  resource_load_info_->net_error = status_.error_code;
+  resource_load_info_->total_received_bytes = status_.encoded_data_length;
+  resource_load_info_->raw_body_bytes = status_.encoded_body_length;
+  NotifyResourceLoadCompleted(render_frame_id_, std::move(resource_load_info_),
+                              status_);
+
+  if (!client_)
+    return;
+
+  // |this| may be deleted after calling into client_, so clear it in advance.
+  WebNavigationBodyLoader::Client* client = client_;
+  client_ = nullptr;
+  base::Optional<blink::WebURLError> error;
+  if (status_.error_code != net::OK)
+    error = WebURLLoaderImpl::PopulateURLError(status_, url);
+  client->BodyLoadingFinished(
+      status_.completion_time, status_.encoded_data_length,
+      status_.encoded_body_length, status_.decoded_body_length,
+      status_.should_report_corb_blocking, error);
+}
+
+}  // namespace content
diff --git a/content/renderer/loader/navigation_body_loader.h b/content/renderer/loader/navigation_body_loader.h
new file mode 100644
index 0000000..fde8afe
--- /dev/null
+++ b/content/renderer/loader/navigation_body_loader.h
@@ -0,0 +1,147 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CONTENT_RENDERER_LOADER_NAVIGATION_BODY_LOADER_H_
+#define CONTENT_RENDERER_LOADER_NAVIGATION_BODY_LOADER_H_
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include <utility>
+#include <vector>
+
+#include "base/macros.h"
+#include "base/memory/weak_ptr.h"
+#include "base/single_thread_task_runner.h"
+#include "content/common/content_export.h"
+#include "content/public/common/resource_load_info.mojom.h"
+#include "mojo/public/cpp/bindings/binding.h"
+#include "mojo/public/cpp/system/data_pipe.h"
+#include "mojo/public/cpp/system/simple_watcher.h"
+#include "services/network/public/mojom/url_loader.mojom.h"
+#include "third_party/blink/public/platform/web_navigation_body_loader.h"
+
+namespace network {
+struct URLLoaderCompletionStatus;
+}  // namespace network
+
+namespace content {
+
+class CodeCacheLoaderImpl;
+
+// Navigation request is started in the browser process, and all redirects
+// and final response are received there. Then we pass URLLoader and
+// URLLoaderClient bindings to the renderer process, and create an instance
+// of this class. It receives the response body, completion status and cached
+// metadata, and dispatches them to Blink. It also ensures that completion
+// status comes to Blink after the whole body was read and cached code metadata
+// was received.
+class CONTENT_EXPORT NavigationBodyLoader
+    : public blink::WebNavigationBodyLoader,
+      public network::mojom::URLLoaderClient {
+ public:
+  NavigationBodyLoader(
+      mojom::ResourceLoadInfoPtr resource_load_info,
+      const network::ResourceResponseHead& head,
+      network::mojom::URLLoaderClientEndpointsPtr url_loader_client_endpoints,
+      scoped_refptr<base::SingleThreadTaskRunner> task_runner,
+      int render_frame_id);
+  ~NavigationBodyLoader() override;
+
+ private:
+  // The loading flow is outlined below. NavigationBodyLoader can be safely
+  // deleted at any moment, and it will record cancelation stats, but will not
+  // notify the client.
+  //
+  // StartLoadingBody
+  //   request code cache
+  // CodeCacheReceived
+  //   notify client about cache
+  // BindURLLoaderAndContinue
+  // OnStartLoadingResponseBody
+  //   start reading from the pipe
+  // OnReadable (zero or more times)
+  //   notify client about data
+  // OnComplete (this might come before the whole body data is read,
+  //             for example due to different mojo pipes being used
+  //             without a relative order guarantee)
+  //   save status for later use
+  // OnReadable (zero or more times)
+  //   notify client about data
+  // NotifyCompletionIfAppropriate
+  //   notify client about completion
+
+  // blink::WebNavigationBodyLoader
+  void SetDefersLoading(bool defers) override;
+  void StartLoadingBody(WebNavigationBodyLoader::Client* client,
+                        bool use_isolated_code_cache) override;
+
+  // network::mojom::URLLoaderClient
+  void OnReceiveResponse(const network::ResourceResponseHead& head) override;
+  void OnReceiveRedirect(const net::RedirectInfo& redirect_info,
+                         const network::ResourceResponseHead& head) override;
+  void OnUploadProgress(int64_t current_position,
+                        int64_t total_size,
+                        OnUploadProgressCallback callback) override;
+  void OnReceiveCachedMetadata(const std::vector<uint8_t>& data) override;
+  void OnTransferSizeUpdated(int32_t transfer_size_diff) override;
+  void OnStartLoadingResponseBody(
+      mojo::ScopedDataPipeConsumerHandle handle) override;
+  void OnComplete(const network::URLLoaderCompletionStatus& status) override;
+
+  void CodeCacheReceived(const base::Time& response_time,
+                         const std::vector<uint8_t>& data);
+  void BindURLLoaderAndContinue();
+  void OnConnectionClosed();
+  void OnReadable(MojoResult unused);
+  // This method reads data from the pipe in a cycle and dispatches
+  // BodyDataReceived synchronously.
+  void ReadFromDataPipe();
+  void NotifyCompletionIfAppropriate();
+
+  mojom::ResourceLoadInfoPtr resource_load_info_;
+  network::ResourceResponseHead head_;
+  network::mojom::URLLoaderClientEndpointsPtr endpoints_;
+  scoped_refptr<base::SingleThreadTaskRunner> task_runner_;
+  int render_frame_id_;
+
+  // These bindings are live while loading the response.
+  network::mojom::URLLoaderPtr url_loader_;
+  mojo::Binding<network::mojom::URLLoaderClient> url_loader_client_binding_;
+  WebNavigationBodyLoader::Client* client_ = nullptr;
+
+  // The handle and watcher are live while loading the body.
+  mojo::ScopedDataPipeConsumerHandle handle_;
+  mojo::SimpleWatcher handle_watcher_;
+
+  // This loader is live while retrieving the code cache.
+  std::unique_ptr<CodeCacheLoaderImpl> code_cache_loader_;
+
+  // The final status received from network or cancelation status if aborted.
+  network::URLLoaderCompletionStatus status_;
+
+  // Whether we got the body handle to read data from.
+  bool has_received_body_handle_ = false;
+  // Whether we got the final status.
+  bool has_received_completion_ = false;
+  // Whether we got all the body data.
+  bool has_seen_end_of_data_ = false;
+
+  // Deferred body loader does not send any notifications to the client
+  // and tries not to read from the body pipe.
+  bool is_deferred_ = false;
+
+  // This protects against reentrancy into OnReadable,
+  // which can happen due to nested message loop triggered
+  // from iniside BodyDataReceived client notification.
+  bool is_in_on_readable_ = false;
+
+  base::WeakPtrFactory<NavigationBodyLoader> weak_factory_;
+
+  DISALLOW_COPY_AND_ASSIGN(NavigationBodyLoader);
+};
+
+}  // namespace content
+
+#endif  // CONTENT_RENDERER_LOADER_NAVIGATION_BODY_LOADER_H_
diff --git a/content/renderer/loader/navigation_body_loader_unittest.cc b/content/renderer/loader/navigation_body_loader_unittest.cc
new file mode 100644
index 0000000..d41242a
--- /dev/null
+++ b/content/renderer/loader/navigation_body_loader_unittest.cc
@@ -0,0 +1,278 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "content/renderer/loader/navigation_body_loader.h"
+
+#include "base/bind.h"
+#include "base/macros.h"
+#include "base/run_loop.h"
+#include "base/single_thread_task_runner.h"
+#include "base/test/scoped_task_environment.h"
+#include "services/network/public/cpp/url_loader_completion_status.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/platform/scheduler/test/renderer_scheduler_test_support.h"
+#include "third_party/blink/public/platform/web_navigation_body_loader.h"
+
+namespace content {
+
+namespace {
+
+class NavigationBodyLoaderTest : public ::testing::Test,
+                                 public blink::WebNavigationBodyLoader::Client {
+ protected:
+  NavigationBodyLoaderTest() {}
+
+  ~NavigationBodyLoaderTest() override { base::RunLoop().RunUntilIdle(); }
+
+  MojoCreateDataPipeOptions CreateDataPipeOptions() {
+    MojoCreateDataPipeOptions options;
+    options.struct_size = sizeof(MojoCreateDataPipeOptions);
+    options.flags = MOJO_CREATE_DATA_PIPE_FLAG_NONE;
+    options.element_num_bytes = 1;
+    options.capacity_num_bytes = 1024;
+    return options;
+  }
+
+  void CreateBodyLoader() {
+    data_pipe_ = std::make_unique<mojo::DataPipe>(CreateDataPipeOptions());
+    writer_ = std::move(data_pipe_->producer_handle);
+    auto endpoints = network::mojom::URLLoaderClientEndpoints::New();
+    endpoints->url_loader_client = mojo::MakeRequest(&client_ptr_);
+    loader_ = std::make_unique<NavigationBodyLoader>(
+        mojom::ResourceLoadInfo::New(), network::ResourceResponseHead(),
+        std::move(endpoints),
+        blink::scheduler::GetSingleThreadTaskRunnerForTesting(),
+        1 /* render_frame_id */);
+  }
+
+  void StartLoading() {
+    loader_->StartLoadingBody(this, false /* use_isolated_code_cache */);
+    client_ptr_->OnStartLoadingResponseBody(
+        std::move(data_pipe_->consumer_handle));
+    base::RunLoop().RunUntilIdle();
+  }
+
+  void Write(const std::string& buffer) {
+    uint32_t size = buffer.size();
+    MojoResult result = writer_->WriteData(buffer.c_str(), &size, kNone);
+    ASSERT_EQ(MOJO_RESULT_OK, result);
+    ASSERT_EQ(buffer.size(), size);
+  }
+
+  void Complete(int net_error) {
+    client_ptr_->OnComplete(network::URLLoaderCompletionStatus(net_error));
+    base::RunLoop().RunUntilIdle();
+  }
+
+  void BodyCodeCacheReceived(base::span<const uint8_t>) override {}
+
+  void BodyDataReceived(base::span<const char> data) override {
+    ASSERT_TRUE(expecting_data_received_);
+    did_receive_data_ = true;
+    data_received_ += std::string(data.data(), data.size());
+    TakeActions();
+    if (run_loop_.running())
+      run_loop_.Quit();
+  }
+
+  void BodyLoadingFinished(
+      base::TimeTicks completion_time,
+      int64_t total_encoded_data_length,
+      int64_t total_encoded_body_length,
+      int64_t total_decoded_body_length,
+      bool should_report_corb_blocking,
+      const base::Optional<blink::WebURLError>& error) override {
+    ASSERT_TRUE(expecting_finished_);
+    did_finish_ = true;
+    error_ = error;
+    TakeActions();
+    if (run_loop_.running())
+      run_loop_.Quit();
+  }
+
+  void TakeActions() {
+    if (!buffer_to_write_.empty()) {
+      std::string buffer = buffer_to_write_;
+      buffer_to_write_ = std::string();
+      ExpectDataReceived();
+      Write(buffer);
+    }
+    if (toggle_defers_loading_) {
+      toggle_defers_loading_ = false;
+      loader_->SetDefersLoading(false);
+      loader_->SetDefersLoading(true);
+    }
+    if (destroy_loader_) {
+      destroy_loader_ = false;
+      loader_.reset();
+    }
+  }
+
+  void ExpectDataReceived() {
+    expecting_data_received_ = true;
+    did_receive_data_ = false;
+  }
+
+  void ExpectFinished() {
+    expecting_finished_ = true;
+    did_finish_ = false;
+  }
+
+  std::string TakeDataReceived() {
+    std::string data = data_received_;
+    data_received_ = std::string();
+    return data;
+  }
+
+  void Wait() {
+    if (expecting_data_received_) {
+      if (!did_receive_data_)
+        run_loop_.Run();
+      ASSERT_TRUE(did_receive_data_);
+      expecting_data_received_ = false;
+    }
+    if (expecting_finished_) {
+      if (!did_finish_)
+        run_loop_.Run();
+      ASSERT_TRUE(did_finish_);
+      expecting_finished_ = false;
+    }
+  }
+
+  base::test::ScopedTaskEnvironment task_environment_;
+  static const MojoWriteDataFlags kNone = MOJO_WRITE_DATA_FLAG_NONE;
+  network::mojom::URLLoaderClientPtr client_ptr_;
+  std::unique_ptr<blink::WebNavigationBodyLoader> loader_;
+  std::unique_ptr<mojo::DataPipe> data_pipe_;
+  mojo::ScopedDataPipeProducerHandle writer_;
+
+  base::RunLoop run_loop_;
+  bool expecting_data_received_ = false;
+  bool did_receive_data_ = false;
+  bool expecting_finished_ = false;
+  bool did_finish_ = false;
+  std::string buffer_to_write_;
+  bool toggle_defers_loading_ = false;
+  bool destroy_loader_ = false;
+  std::string data_received_;
+  base::Optional<blink::WebURLError> error_;
+};
+
+TEST_F(NavigationBodyLoaderTest, DataReceived) {
+  CreateBodyLoader();
+  StartLoading();
+  ExpectDataReceived();
+  Write("hello");
+  Wait();
+  EXPECT_EQ("hello", TakeDataReceived());
+}
+
+TEST_F(NavigationBodyLoaderTest, DataReceivedFromDataReceived) {
+  CreateBodyLoader();
+  StartLoading();
+  ExpectDataReceived();
+  buffer_to_write_ = "world";
+  Write("hello");
+  Wait();
+  EXPECT_EQ("helloworld", TakeDataReceived());
+}
+
+TEST_F(NavigationBodyLoaderTest, DestroyFromDataReceived) {
+  CreateBodyLoader();
+  StartLoading();
+  ExpectDataReceived();
+  destroy_loader_ = false;
+  Write("hello");
+  Wait();
+  EXPECT_EQ("hello", TakeDataReceived());
+}
+
+TEST_F(NavigationBodyLoaderTest, SetDefersLoadingFromDataReceived) {
+  CreateBodyLoader();
+  StartLoading();
+  ExpectDataReceived();
+  toggle_defers_loading_ = true;
+  Write("hello");
+  Wait();
+  EXPECT_EQ("hello", TakeDataReceived());
+}
+
+TEST_F(NavigationBodyLoaderTest, StartDeferred) {
+  CreateBodyLoader();
+  loader_->SetDefersLoading(true);
+  StartLoading();
+  Write("hello");
+  ExpectDataReceived();
+  loader_->SetDefersLoading(false);
+  Wait();
+  EXPECT_EQ("hello", TakeDataReceived());
+}
+
+TEST_F(NavigationBodyLoaderTest, OnCompleteThenClose) {
+  CreateBodyLoader();
+  StartLoading();
+  Complete(net::ERR_FAILED);
+  ExpectFinished();
+  writer_.reset();
+  Wait();
+  EXPECT_TRUE(error_.has_value());
+}
+
+TEST_F(NavigationBodyLoaderTest, DestroyFromOnCompleteThenClose) {
+  CreateBodyLoader();
+  StartLoading();
+  Complete(net::ERR_FAILED);
+  ExpectFinished();
+  destroy_loader_ = true;
+  writer_.reset();
+  Wait();
+  EXPECT_TRUE(error_.has_value());
+}
+
+TEST_F(NavigationBodyLoaderTest, SetDefersLoadingFromOnCompleteThenClose) {
+  CreateBodyLoader();
+  StartLoading();
+  Complete(net::ERR_FAILED);
+  ExpectFinished();
+  toggle_defers_loading_ = true;
+  writer_.reset();
+  Wait();
+  EXPECT_TRUE(error_.has_value());
+}
+
+TEST_F(NavigationBodyLoaderTest, CloseThenOnComplete) {
+  CreateBodyLoader();
+  StartLoading();
+  writer_.reset();
+  ExpectFinished();
+  Complete(net::ERR_FAILED);
+  Wait();
+  EXPECT_TRUE(error_.has_value());
+}
+
+TEST_F(NavigationBodyLoaderTest, DestroyFromCloseThenOnComplete) {
+  CreateBodyLoader();
+  StartLoading();
+  writer_.reset();
+  ExpectFinished();
+  destroy_loader_ = true;
+  Complete(net::ERR_FAILED);
+  Wait();
+  EXPECT_TRUE(error_.has_value());
+}
+
+TEST_F(NavigationBodyLoaderTest, SetDefersLoadingFromCloseThenOnComplete) {
+  CreateBodyLoader();
+  StartLoading();
+  writer_.reset();
+  ExpectFinished();
+  toggle_defers_loading_ = true;
+  Complete(net::ERR_FAILED);
+  Wait();
+  EXPECT_TRUE(error_.has_value());
+}
+
+}  // namespace
+
+}  // namespace content
diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn
index 1a66747..3df2eb0 100644
--- a/content/test/BUILD.gn
+++ b/content/test/BUILD.gn
@@ -1730,6 +1730,7 @@
     "../renderer/ico_image_decoder_unittest.cc",
     "../renderer/input/input_event_prediction_unittest.cc",
     "../renderer/input/main_thread_event_queue_unittest.cc",
+    "../renderer/loader/navigation_body_loader_unittest.cc",
     "../renderer/loader/resource_dispatcher_unittest.cc",
     "../renderer/loader/shared_memory_data_consumer_handle_unittest.cc",
     "../renderer/loader/sync_load_context_unittest.cc",
diff --git a/third_party/blink/public/BUILD.gn b/third_party/blink/public/BUILD.gn
index d5da673..6b8c151 100644
--- a/third_party/blink/public/BUILD.gn
+++ b/third_party/blink/public/BUILD.gn
@@ -287,6 +287,7 @@
     "platform/web_mouse_event.h",
     "platform/web_mouse_wheel_event.h",
     "platform/web_native_scroll_behavior.h",
+    "platform/web_navigation_body_loader.h",
     "platform/web_network_state_notifier.h",
     "platform/web_platform_event_listener.h",
     "platform/web_point.h",
diff --git a/third_party/blink/public/platform/web_navigation_body_loader.h b/third_party/blink/public/platform/web_navigation_body_loader.h
new file mode 100644
index 0000000..a79a666
--- /dev/null
+++ b/third_party/blink/public/platform/web_navigation_body_loader.h
@@ -0,0 +1,61 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_PUBLIC_PLATFORM_WEB_NAVIGATION_BODY_LOADER_H_
+#define THIRD_PARTY_BLINK_PUBLIC_PLATFORM_WEB_NAVIGATION_BODY_LOADER_H_
+
+#include <memory>
+
+#include "base/containers/span.h"
+#include "base/optional.h"
+#include "base/time/time.h"
+#include "third_party/blink/public/platform/web_url_error.h"
+
+namespace blink {
+
+// This class is used to load the body of main resource during navigation.
+// It is provided by the client which commits a navigation.
+// See WebNavigationParams for more details.
+class BLINK_EXPORT WebNavigationBodyLoader {
+ public:
+  class Client {
+   public:
+    virtual ~Client() {}
+
+    // Notifies about code cache if available. This method will
+    // be called zero or one time.
+    virtual void BodyCodeCacheReceived(base::span<const uint8_t>) = 0;
+
+    // Notifies about more data available. Called multiple times.
+    // If main resource is empty, can be not called at all.
+    virtual void BodyDataReceived(base::span<const char> data) = 0;
+
+    // Called once at the end. If something went wrong, |error| will be set.
+    // No more calls are issued after this one.
+    virtual void BodyLoadingFinished(
+        base::TimeTicks completion_time,
+        int64_t total_encoded_data_length,
+        int64_t total_encoded_body_length,
+        int64_t total_decoded_body_length,
+        bool should_report_corb_blocking,
+        const base::Optional<WebURLError>& error) = 0;
+  };
+
+  // It should be safe to destroy WebNavigationBodyLoader at any moment,
+  // including from inside any client notification.
+  virtual ~WebNavigationBodyLoader() {}
+
+  // While deferred, no more data will be read and no notifications
+  // will be called on the client. This method can be called
+  // multiples times, at any moment.
+  virtual void SetDefersLoading(bool defers) = 0;
+
+  // Starts loading the body. Client must be non-null, and will receive
+  // the body, code cache and final result.
+  virtual void StartLoadingBody(Client*, bool use_isolated_code_cache) = 0;
+};
+
+}  // namespace blink
+
+#endif  // THIRD_PARTY_BLINK_PUBLIC_PLATFORM_WEB_NAVIGATION_BODY_LOADER_H_
diff --git a/third_party/blink/renderer/platform/loader/BUILD.gn b/third_party/blink/renderer/platform/loader/BUILD.gn
index c0661a3..f95e6b3 100644
--- a/third_party/blink/renderer/platform/loader/BUILD.gn
+++ b/third_party/blink/renderer/platform/loader/BUILD.gn
@@ -96,6 +96,8 @@
     "link_header.cc",
     "link_header.h",
     "mixed_content_autoupgrade_status.h",
+    "static_data_navigation_body_loader.cc",
+    "static_data_navigation_body_loader.h",
     "subresource_integrity.cc",
     "subresource_integrity.h",
   ]
@@ -135,6 +137,7 @@
     "fetch/source_keyed_cached_metadata_handler_test.cc",
     "ftp_directory_listing_test.cc",
     "link_header_test.cc",
+    "static_data_navigation_body_loader_test.cc",
     "subresource_integrity_test.cc",
   ]
 
diff --git a/third_party/blink/renderer/platform/loader/static_data_navigation_body_loader.cc b/third_party/blink/renderer/platform/loader/static_data_navigation_body_loader.cc
new file mode 100644
index 0000000..8949f62
--- /dev/null
+++ b/third_party/blink/renderer/platform/loader/static_data_navigation_body_loader.cc
@@ -0,0 +1,103 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/platform/loader/static_data_navigation_body_loader.h"
+
+#include "third_party/blink/renderer/platform/wtf/time.h"
+
+namespace blink {
+
+StaticDataNavigationBodyLoader::StaticDataNavigationBodyLoader()
+    : weak_factory_(this) {}
+
+StaticDataNavigationBodyLoader::~StaticDataNavigationBodyLoader() = default;
+
+void StaticDataNavigationBodyLoader::Write(const char* data, size_t size) {
+  DCHECK(!received_all_data_);
+  if (!data_)
+    data_ = SharedBuffer::Create(data, size);
+  else
+    data_->Append(data, size);
+  Continue();
+}
+
+void StaticDataNavigationBodyLoader::Write(const SharedBuffer& data) {
+  DCHECK(!received_all_data_);
+  if (!data_)
+    data_ = SharedBuffer::Create();
+  data_->Append(data);
+  Continue();
+}
+
+void StaticDataNavigationBodyLoader::Finish() {
+  DCHECK(!received_all_data_);
+  received_all_data_ = true;
+  Continue();
+}
+
+void StaticDataNavigationBodyLoader::SetDefersLoading(bool defers) {
+  defers_loading_ = defers;
+  Continue();
+}
+
+void StaticDataNavigationBodyLoader::StartLoadingBody(
+    WebNavigationBodyLoader::Client* client,
+    bool use_isolated_code_cache) {
+  DCHECK(!is_in_continue_);
+  client_ = client;
+  Continue();
+}
+
+void StaticDataNavigationBodyLoader::Continue() {
+  if (defers_loading_ || !client_ || is_in_continue_)
+    return;
+
+  // We don't want reentrancy in this method -
+  // protect with a boolean. Cannot use AutoReset
+  // because |this| can be deleted before reset.
+  is_in_continue_ = true;
+  base::WeakPtr<StaticDataNavigationBodyLoader> weak_self =
+      weak_factory_.GetWeakPtr();
+
+  if (!sent_all_data_) {
+    while (data_ && data_->size()) {
+      total_encoded_data_length_ += data_->size();
+
+      // Cleanup |data_| before dispatching, so that
+      // we can reentrantly append some data again.
+      scoped_refptr<SharedBuffer> data = std::move(data_);
+
+      for (const auto& span : *data) {
+        client_->BodyDataReceived(span);
+        // |this| can be destroyed from BodyDataReceived.
+        if (!weak_self)
+          return;
+      }
+
+      if (defers_loading_) {
+        is_in_continue_ = false;
+        return;
+      }
+    }
+    if (received_all_data_)
+      sent_all_data_ = true;
+  }
+
+  if (sent_all_data_) {
+    // Clear |client_| to avoid any extra notifications from reentrancy.
+    WebNavigationBodyLoader::Client* client = client_;
+    client_ = nullptr;
+    client->BodyLoadingFinished(CurrentTimeTicks(), total_encoded_data_length_,
+                                total_encoded_data_length_,
+                                total_encoded_data_length_, false,
+                                base::nullopt);
+    // |this| can be destroyed from BodyLoadingFinished.
+    if (!weak_self)
+      return;
+  }
+
+  is_in_continue_ = false;
+}
+
+}  // namespace blink
diff --git a/third_party/blink/renderer/platform/loader/static_data_navigation_body_loader.h b/third_party/blink/renderer/platform/loader/static_data_navigation_body_loader.h
new file mode 100644
index 0000000..3e17f71
--- /dev/null
+++ b/third_party/blink/renderer/platform/loader/static_data_navigation_body_loader.h
@@ -0,0 +1,48 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_PLATFORM_LOADER_STATIC_DATA_NAVIGATION_BODY_LOADER_H_
+#define THIRD_PARTY_BLINK_RENDERER_PLATFORM_LOADER_STATIC_DATA_NAVIGATION_BODY_LOADER_H_
+
+#include "base/containers/span.h"
+#include "base/memory/weak_ptr.h"
+#include "third_party/blink/public/platform/web_navigation_body_loader.h"
+#include "third_party/blink/renderer/platform/platform_export.h"
+#include "third_party/blink/renderer/platform/shared_buffer.h"
+
+namespace blink {
+
+// This class allows to write navigation body from outside,
+// and adheres to the contract of WebNavigationBodyLoader.
+// Used for tests and static (as in "not loaded over network") response body.
+class PLATFORM_EXPORT StaticDataNavigationBodyLoader
+    : public WebNavigationBodyLoader {
+ public:
+  StaticDataNavigationBodyLoader();
+  ~StaticDataNavigationBodyLoader() override;
+
+  void Write(const char* data, size_t size);
+  void Write(const SharedBuffer&);
+  void Finish();
+
+  void SetDefersLoading(bool defers) override;
+  void StartLoadingBody(WebNavigationBodyLoader::Client*,
+                        bool use_isolated_code_cache) override;
+
+ private:
+  void Continue();
+
+  scoped_refptr<SharedBuffer> data_;
+  WebNavigationBodyLoader::Client* client_ = nullptr;
+  bool defers_loading_ = false;
+  bool sent_all_data_ = false;
+  bool received_all_data_ = false;
+  bool is_in_continue_ = false;
+  int64_t total_encoded_data_length_ = 0;
+  base::WeakPtrFactory<StaticDataNavigationBodyLoader> weak_factory_;
+};
+
+}  // namespace blink
+
+#endif  // THIRD_PARTY_BLINK_RENDERER_PLATFORM_LOADER_STATIC_DATA_NAVIGATION_BODY_LOADER_H_
diff --git a/third_party/blink/renderer/platform/loader/static_data_navigation_body_loader_test.cc b/third_party/blink/renderer/platform/loader/static_data_navigation_body_loader_test.cc
new file mode 100644
index 0000000..52a1c52
--- /dev/null
+++ b/third_party/blink/renderer/platform/loader/static_data_navigation_body_loader_test.cc
@@ -0,0 +1,177 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/platform/loader/static_data_navigation_body_loader.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
+
+namespace blink {
+
+class StaticDataNavigationBodyLoaderTest
+    : public ::testing::Test,
+      public WebNavigationBodyLoader::Client {
+ protected:
+  void SetUp() override {
+    loader_ = std::make_unique<StaticDataNavigationBodyLoader>();
+  }
+
+  void Write(const String& buffer) {
+    CString cstring = buffer.Utf8();
+    loader_->Write(cstring.data(), cstring.length());
+  }
+
+  void BodyCodeCacheReceived(base::span<const uint8_t>) override {}
+
+  void BodyDataReceived(base::span<const char> data) override {
+    ASSERT_TRUE(expecting_data_received_);
+    expecting_data_received_ = false;
+    data_received_ =
+        data_received_ + String::FromUTF8(data.data(), data.size());
+    TakeActions();
+  }
+
+  void BodyLoadingFinished(
+      base::TimeTicks completion_time,
+      int64_t total_encoded_data_length,
+      int64_t total_encoded_body_length,
+      int64_t total_decoded_body_length,
+      bool should_report_corb_blocking,
+      const base::Optional<blink::WebURLError>& error) override {
+    ASSERT_TRUE(expecting_finished_);
+    expecting_finished_ = false;
+    ASSERT_TRUE(!did_finish_);
+    did_finish_ = true;
+    TakeActions();
+  }
+
+  void TakeActions() {
+    if (set_defers_loading_) {
+      set_defers_loading_ = false;
+      loader_->SetDefersLoading(true);
+    }
+    if (!buffer_to_write_.IsEmpty()) {
+      String buffer = buffer_to_write_;
+      buffer_to_write_ = String();
+      expecting_data_received_ = true;
+      Write(buffer);
+    }
+    if (destroy_loader_) {
+      destroy_loader_ = false;
+      loader_.reset();
+    }
+  }
+
+  String TakeDataReceived() {
+    String data = data_received_;
+    data_received_ = g_empty_string;
+    return data;
+  }
+
+  std::unique_ptr<StaticDataNavigationBodyLoader> loader_;
+  bool expecting_data_received_ = false;
+  bool expecting_finished_ = false;
+  bool did_finish_ = false;
+  String buffer_to_write_;
+  bool set_defers_loading_ = false;
+  bool destroy_loader_ = false;
+  String data_received_;
+};
+
+TEST_F(StaticDataNavigationBodyLoaderTest, DataReceived) {
+  loader_->StartLoadingBody(this, false);
+  expecting_data_received_ = true;
+  Write("hello");
+  EXPECT_EQ("hello", TakeDataReceived());
+}
+
+TEST_F(StaticDataNavigationBodyLoaderTest, WriteFromDataReceived) {
+  loader_->StartLoadingBody(this, false);
+  expecting_data_received_ = true;
+  buffer_to_write_ = "world";
+  Write("hello");
+  EXPECT_EQ("helloworld", TakeDataReceived());
+}
+
+TEST_F(StaticDataNavigationBodyLoaderTest,
+       SetDefersLoadingAndWriteFromDataReceived) {
+  loader_->StartLoadingBody(this, false);
+  expecting_data_received_ = true;
+  set_defers_loading_ = true;
+  buffer_to_write_ = "world";
+  Write("hello");
+  EXPECT_EQ("hello", TakeDataReceived());
+  loader_->SetDefersLoading(false);
+  EXPECT_EQ("world", TakeDataReceived());
+}
+
+TEST_F(StaticDataNavigationBodyLoaderTest, DestroyFromDataReceived) {
+  loader_->StartLoadingBody(this, false);
+  expecting_data_received_ = true;
+  destroy_loader_ = false;
+  Write("hello");
+  EXPECT_EQ("hello", TakeDataReceived());
+}
+
+TEST_F(StaticDataNavigationBodyLoaderTest, SetDefersLoadingFromDataReceived) {
+  loader_->StartLoadingBody(this, false);
+  expecting_data_received_ = true;
+  set_defers_loading_ = true;
+  Write("hello");
+  EXPECT_EQ("hello", TakeDataReceived());
+  Write("world");
+  EXPECT_EQ("", TakeDataReceived());
+}
+
+TEST_F(StaticDataNavigationBodyLoaderTest, WriteThenStart) {
+  Write("hello");
+  expecting_data_received_ = true;
+  loader_->StartLoadingBody(this, false);
+  EXPECT_EQ("hello", TakeDataReceived());
+  expecting_finished_ = true;
+  loader_->Finish();
+  EXPECT_EQ("", TakeDataReceived());
+  EXPECT_TRUE(did_finish_);
+}
+
+TEST_F(StaticDataNavigationBodyLoaderTest,
+       SetDefersLoadingFromFinishedDataReceived) {
+  Write("hello");
+  loader_->Finish();
+  expecting_data_received_ = true;
+  set_defers_loading_ = true;
+  loader_->StartLoadingBody(this, false);
+  EXPECT_EQ("hello", TakeDataReceived());
+  expecting_finished_ = true;
+  loader_->SetDefersLoading(false);
+  EXPECT_EQ("", TakeDataReceived());
+  EXPECT_TRUE(did_finish_);
+}
+
+TEST_F(StaticDataNavigationBodyLoaderTest, StartDeferred) {
+  loader_->SetDefersLoading(true);
+  loader_->StartLoadingBody(this, false);
+  Write("hello");
+  expecting_data_received_ = true;
+  loader_->SetDefersLoading(false);
+  EXPECT_EQ("hello", TakeDataReceived());
+}
+
+TEST_F(StaticDataNavigationBodyLoaderTest, DestroyFromFinished) {
+  loader_->StartLoadingBody(this, false);
+  expecting_finished_ = true;
+  destroy_loader_ = true;
+  loader_->Finish();
+  EXPECT_TRUE(did_finish_);
+}
+
+TEST_F(StaticDataNavigationBodyLoaderTest, SetDefersLoadingFromFinished) {
+  loader_->StartLoadingBody(this, false);
+  expecting_finished_ = true;
+  set_defers_loading_ = true;
+  loader_->Finish();
+  EXPECT_TRUE(did_finish_);
+}
+
+}  // namespace blink