blob: 920604611edd6ded123e01c67588c13ffa83409d [file] [log] [blame]
// Copyright (c) 2011 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/browser/appcache/appcache_request_handler.h"
#include <utility>
#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/command_line.h"
#include "base/containers/contains.h"
#include "base/feature_list.h"
#include "content/browser/appcache/appcache.h"
#include "content/browser/appcache/appcache_backend_impl.h"
#include "content/browser/appcache/appcache_host.h"
#include "content/browser/appcache/appcache_policy.h"
#include "content/browser/appcache/appcache_request.h"
#include "content/browser/appcache/appcache_subresource_url_factory.h"
#include "content/browser/appcache/appcache_url_loader.h"
#include "content/browser/navigation_subresource_loader_params.h"
#include "content/browser/renderer_host/frame_tree_node.h"
#include "content/browser/renderer_host/navigation_request.h"
#include "content/public/common/content_client.h"
#include "services/metrics/public/cpp/ukm_source_id.h"
#include "services/network/public/cpp/wrapper_shared_url_loader_factory.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/loader/resource_type_util.h"
#include "third_party/blink/public/mojom/appcache/appcache_info.mojom.h"
#include "third_party/blink/public/mojom/loader/resource_load_info.mojom-shared.h"
namespace content {
// If this feature is enabled, we behave as if all manifests include a
// NETWORK: * line, indicating that all requests should fall back to the
// network.
const base::Feature kAppCacheAlwaysFallbackToNetwork{
"AppCacheAlwaysFallbackToNetwork", base::FEATURE_ENABLED_BY_DEFAULT};
namespace {
bool g_running_in_tests = false;
} // namespace
AppCacheRequestHandler::AppCacheRequestHandler(
AppCacheHost* host,
network::mojom::RequestDestination request_destination,
bool should_reset_appcache,
std::unique_ptr<AppCacheRequest> request,
int frame_tree_node_id)
: host_(host),
request_destination_(request_destination),
should_reset_appcache_(should_reset_appcache),
is_waiting_for_cache_selection_(false),
found_group_id_(0),
found_cache_id_(0),
found_network_namespace_(false),
cache_entry_not_found_(false),
is_delivering_network_response_(false),
maybe_load_resource_executed_(false),
cache_id_(blink::mojom::kAppCacheNoCacheId),
service_(host_->service()),
request_(std::move(request)),
frame_tree_node_id_(frame_tree_node_id) {
DCHECK(host_);
DCHECK(service_);
host_->AddObserver(this);
service_->AddObserver(this);
}
AppCacheRequestHandler::~AppCacheRequestHandler() {
if (host_) {
storage()->CancelDelegateCallbacks(this);
host_->RemoveObserver(this);
}
if (service_)
service_->RemoveObserver(this);
if (loader_)
loader_->DeleteIfNeeded();
}
AppCacheStorage* AppCacheRequestHandler::storage() const {
DCHECK(host_);
return host_->storage();
}
AppCacheURLLoader* AppCacheRequestHandler::MaybeLoadResource(
net::NetworkDelegate* network_delegate) {
maybe_load_resource_executed_ = true;
if (!host_ ||
!AppCacheRequest::IsSchemeAndMethodSupportedForAppCache(request_.get()) ||
cache_entry_not_found_) {
return nullptr;
}
// This method can get called multiple times over the life
// of a request. The case we detect here is having scheduled
// delivery of a "network response" using a loader set up on an
// earlier call through this method. To send the request through
// to the network involves restarting the request altogether,
// which will call through to our interception layer again.
// This time through, we return NULL so the request hits the wire.
if (is_delivering_network_response_) {
is_delivering_network_response_ = false;
return nullptr;
}
// Clear out our 'found' fields since we're starting a request for a
// new resource, any values in those fields are no longer valid.
found_entry_ = AppCacheEntry();
found_fallback_entry_ = AppCacheEntry();
found_cache_id_ = blink::mojom::kAppCacheNoCacheId;
found_manifest_url_ = GURL();
found_network_namespace_ = false;
std::unique_ptr<AppCacheURLLoader> loader;
if (is_main_resource())
loader = MaybeLoadMainResource(network_delegate);
else
loader = MaybeLoadSubResource(network_delegate);
// If its been setup to deliver a network response, we can just delete
// it now and return NULL instead to achieve that since it couldn't
// have been started yet.
if (loader && loader->IsDeliveringNetworkResponse()) {
DCHECK(!loader->IsStarted());
loader.release()->DeleteIfNeeded();
loader_ = nullptr;
}
return loader.release();
}
AppCacheURLLoader* AppCacheRequestHandler::MaybeLoadFallbackForRedirect(
net::NetworkDelegate* network_delegate,
const GURL& location) {
if (!host_ ||
!AppCacheRequest::IsSchemeAndMethodSupportedForAppCache(request_.get()) ||
cache_entry_not_found_)
return nullptr;
if (is_main_resource())
return nullptr;
// If MaybeLoadResourceExecuted did not run, this might be, e.g., a redirect
// caused by the Web Request API late in the loading progress. AppCache might
// misinterpret the rules for the new target and cause unnecessary
// fallbacks/errors, therefore it is better to give up on app-caching in this
// case. More information in https://crbug.com/141114 and the discussion at
// https://chromiumcodereview.appspot.com/10829356.
if (!maybe_load_resource_executed_)
return nullptr;
if (request_->GetURL().GetOrigin() == location.GetOrigin())
return nullptr;
DCHECK(!loader_.get()); // our jobs never generate redirects
std::unique_ptr<AppCacheURLLoader> loader;
if (found_fallback_entry_.has_response_id()) {
// 7.9.6, step 4: If this results in a redirect to another origin,
// get the resource of the fallback entry.
loader = CreateLoader(network_delegate);
DeliverAppCachedResponse(found_fallback_entry_, found_cache_id_,
found_manifest_url_, true,
found_namespace_entry_url_);
} else if (!found_network_namespace_ &&
!base::FeatureList::IsEnabled(kAppCacheAlwaysFallbackToNetwork)) {
// 7.9.6, step 6: Fail the resource load.
loader = CreateLoader(network_delegate);
DeliverErrorResponse();
} else {
// 7.9.6 step 3 and 5: Fetch the resource normally.
}
return loader.release();
}
AppCacheURLLoader* AppCacheRequestHandler::MaybeLoadFallbackForResponse(
net::NetworkDelegate* network_delegate) {
if (!host_ ||
!AppCacheRequest::IsSchemeAndMethodSupportedForAppCache(request_.get()) ||
cache_entry_not_found_)
return nullptr;
if (!found_fallback_entry_.has_response_id())
return nullptr;
if (request_->IsCancelled()) {
// 7.9.6, step 4: But not if the user canceled the download.
return nullptr;
}
// We don't fallback for responses that we delivered.
if (loader_.get()) {
if (loader_->IsDeliveringAppCacheResponse() ||
loader_->IsDeliveringErrorResponse()) {
return nullptr;
}
}
if (request_->IsSuccess()) {
int code_major = request_->GetResponseCode() / 100;
if (code_major !=4 && code_major != 5)
return nullptr;
// Servers can override the fallback behavior with a response header.
const std::string kFallbackOverrideHeader(
"x-chromium-appcache-fallback-override");
const std::string kFallbackOverrideValue(
"disallow-fallback");
std::string header_value;
header_value = request_->GetResponseHeaderByName(kFallbackOverrideHeader);
if (header_value == kFallbackOverrideValue)
return nullptr;
}
// 7.9.6, step 4: If this results in a 4xx or 5xx status code
// or there were network errors, get the resource of the fallback entry.
std::unique_ptr<AppCacheURLLoader> loader = CreateLoader(network_delegate);
DeliverAppCachedResponse(found_fallback_entry_, found_cache_id_,
found_manifest_url_, true,
found_namespace_entry_url_);
return loader.release();
}
void AppCacheRequestHandler::GetExtraResponseInfo(int64_t* cache_id,
GURL* manifest_url) {
*cache_id = cache_id_;
*manifest_url = manifest_url_;
}
// static
std::unique_ptr<AppCacheRequestHandler>
AppCacheRequestHandler::InitializeForMainResourceNetworkService(
const network::ResourceRequest& request,
base::WeakPtr<AppCacheHost> appcache_host,
int frame_tree_node_id) {
std::unique_ptr<AppCacheRequestHandler> handler =
appcache_host->CreateRequestHandler(
std::make_unique<AppCacheRequest>(request), request.destination,
request.should_reset_appcache, frame_tree_node_id);
if (handler)
handler->appcache_host_ = std::move(appcache_host);
return handler;
}
// static
bool AppCacheRequestHandler::IsMainRequestDestination(
network::mojom::RequestDestination destination) {
// This returns false for kWorker, which is typically considered a main
// resource. In appcache, dedicated workers are treated as subresources of
// their nearest ancestor frame's appcache host unlike shared workers.
return blink::IsRequestDestinationFrame(destination) ||
destination == network::mojom::RequestDestination::kSharedWorker;
}
void AppCacheRequestHandler::OnDestructionImminent(AppCacheHost* host) {
storage()->CancelDelegateCallbacks(this);
host_ = nullptr; // no need to RemoveObserver, the host is being deleted
// Since the host is being deleted, we don't have to drive an in-progress
// loader to completion. It's destined for the bit bucket anyway.
if (loader_.get())
loader_.reset();
}
void AppCacheRequestHandler::OnServiceDestructionImminent(
AppCacheServiceImpl* service) {
service_ = nullptr;
if (!host_) {
DCHECK(!loader_);
return;
}
host_->RemoveObserver(this);
OnDestructionImminent(host_);
}
void AppCacheRequestHandler::DeliverAppCachedResponse(
const AppCacheEntry& entry,
int64_t cache_id,
const GURL& manifest_url,
bool is_fallback,
const GURL& namespace_entry_url) {
DCHECK(host_ && loader_.get() && loader_->IsWaiting());
DCHECK(entry.has_response_id());
// Cache information about the response, for use by GetExtraResponseInfo.
cache_id_ = cache_id;
manifest_url_ = manifest_url;
if (blink::IsRequestDestinationFrame(request_destination_) &&
!namespace_entry_url.is_empty())
host_->NotifyMainResourceIsNamespaceEntry(namespace_entry_url);
loader_->DeliverAppCachedResponse(manifest_url, cache_id, entry, is_fallback);
}
void AppCacheRequestHandler::DeliverErrorResponse() {
DCHECK(loader_.get() && loader_->IsWaiting());
DCHECK_EQ(blink::mojom::kAppCacheNoCacheId, cache_id_);
DCHECK(manifest_url_.is_empty());
loader_->DeliverErrorResponse();
}
void AppCacheRequestHandler::DeliverNetworkResponse() {
DCHECK(loader_.get() && loader_->IsWaiting());
DCHECK_EQ(blink::mojom::kAppCacheNoCacheId, cache_id_);
DCHECK(manifest_url_.is_empty());
loader_->DeliverNetworkResponse();
}
std::unique_ptr<AppCacheURLLoader> AppCacheRequestHandler::CreateLoader(
net::NetworkDelegate* network_delegate) {
auto loader = std::make_unique<AppCacheURLLoader>(
request_.get(), storage(), std::move(loader_callback_));
loader_ = loader->GetWeakPtr();
return loader;
}
// Main-resource handling ----------------------------------------------
std::unique_ptr<AppCacheURLLoader>
AppCacheRequestHandler::MaybeLoadMainResource(
net::NetworkDelegate* network_delegate) {
DCHECK(!loader_.get());
DCHECK(host_);
if (storage()->IsInitialized() &&
!base::Contains(service_->storage()->usage_map(),
url::Origin::Create(request_->GetURL()))) {
return nullptr;
}
host_->enable_cache_selection(true);
const AppCacheHost* spawning_host =
(request_destination_ ==
network::mojom::RequestDestination::kSharedWorker)
? host_
: host_->GetSpawningHost();
GURL preferred_manifest_url = spawning_host ?
spawning_host->preferred_manifest_url() : GURL();
// We may have to wait for our storage query to complete, but
// this query can also complete syncrhonously.
std::unique_ptr<AppCacheURLLoader> loader = CreateLoader(network_delegate);
storage()->FindResponseForMainRequest(request_->GetURL(),
preferred_manifest_url, this);
return loader;
}
void AppCacheRequestHandler::OnMainResponseFound(
const GURL& url,
const AppCacheEntry& entry,
const GURL& namespace_entry_url,
const AppCacheEntry& fallback_entry,
int64_t cache_id,
int64_t group_id,
const GURL& manifest_url) {
DCHECK(host_);
DCHECK(is_main_resource());
DCHECK(!entry.IsForeign());
DCHECK(!fallback_entry.IsForeign());
DCHECK(!(entry.has_response_id() && fallback_entry.has_response_id()));
// Request may have been canceled, but not yet deleted, while waiting on
// the cache.
if (!loader_.get())
return;
AppCachePolicy* policy = host_->service()->appcache_policy();
bool was_blocked_by_policy =
!manifest_url.is_empty() && policy &&
!policy->CanLoadAppCache(manifest_url,
host_->site_for_cookies().RepresentativeUrl(),
host_->top_frame_origin());
if (was_blocked_by_policy) {
if (blink::IsRequestDestinationFrame(request_destination_)) {
host_->NotifyMainResourceBlocked(manifest_url);
} else {
DCHECK_EQ(request_destination_,
network::mojom::RequestDestination::kSharedWorker);
host_->OnContentBlocked(manifest_url);
}
DeliverNetworkResponse();
return;
}
if (should_reset_appcache_ && !manifest_url.is_empty()) {
host_->service()->DeleteAppCacheGroup(manifest_url,
net::CompletionOnceCallback());
DeliverNetworkResponse();
return;
}
if (IsMainRequestDestination(request_destination_) &&
cache_id != blink::mojom::kAppCacheNoCacheId) {
// AppCacheHost loads and holds a reference to the main resource cache
// for two reasons, firstly to preload the cache into the working set
// in advance of subresource loads happening, secondly to prevent the
// AppCache from falling out of the working set on frame navigations.
host_->LoadMainResourceCache(cache_id);
host_->set_preferred_manifest_url(manifest_url);
}
// 6.11.1 Navigating across documents, steps 10 and 14.
found_entry_ = entry;
found_namespace_entry_url_ = namespace_entry_url;
found_fallback_entry_ = fallback_entry;
found_cache_id_ = cache_id;
found_group_id_ = group_id;
found_manifest_url_ = manifest_url;
found_network_namespace_ = false; // not applicable to main requests
if (found_entry_.has_response_id()) {
DCHECK(!found_fallback_entry_.has_response_id());
DeliverAppCachedResponse(found_entry_, found_cache_id_, found_manifest_url_,
false, found_namespace_entry_url_);
} else {
DeliverNetworkResponse();
}
}
// NetworkService loading:
void AppCacheRequestHandler::RunLoaderCallbackForMainResource(
BrowserContext* browser_context,
LoaderCallback callback,
SingleRequestURLLoaderFactory::RequestHandler handler) {
scoped_refptr<network::SharedURLLoaderFactory> single_request_factory;
// For now let |this| always also return the subresource loader if (and only
// if) this returns a non-null |handler| for handling the main resource.
if (handler) {
should_create_subresource_loader_ = true;
single_request_factory =
base::MakeRefCounted<SingleRequestURLLoaderFactory>(std::move(handler));
FrameTreeNode* frame_tree_node =
FrameTreeNode::GloballyFindByID(frame_tree_node_id_);
if (frame_tree_node && frame_tree_node->navigation_request()) {
mojo::PendingRemote<network::mojom::URLLoaderFactory> pending_factory;
auto factory_receiver = pending_factory.InitWithNewPipeAndPassReceiver();
bool use_proxy =
GetContentClient()->browser()->WillCreateURLLoaderFactory(
browser_context, frame_tree_node->current_frame_host(),
frame_tree_node->current_frame_host()->GetProcess()->GetID(),
ContentBrowserClient::URLLoaderFactoryType::kNavigation,
url::Origin(),
frame_tree_node->navigation_request()->GetNavigationId(),
ukm::SourceIdObj::FromInt64(frame_tree_node->navigation_request()
->GetNextPageUkmSourceId()),
&factory_receiver, nullptr /* header_client */,
nullptr /* bypass_redirect_checks */,
nullptr /* disable_secure_dns */, nullptr /* factory_override */);
if (use_proxy) {
single_request_factory->Clone(std::move(factory_receiver));
single_request_factory =
base::MakeRefCounted<network::WrapperSharedURLLoaderFactory>(
std::move(pending_factory));
}
}
}
std::move(callback).Run(std::move(single_request_factory));
}
// Sub-resource handling ----------------------------------------------
std::unique_ptr<AppCacheURLLoader> AppCacheRequestHandler::MaybeLoadSubResource(
net::NetworkDelegate* network_delegate) {
DCHECK(!loader_.get());
if (host_->is_selection_pending()) {
// We have to wait until cache selection is complete and the
// selected cache is loaded.
is_waiting_for_cache_selection_ = true;
return CreateLoader(network_delegate);
}
if (!host_->associated_cache() ||
!host_->associated_cache()->is_complete() ||
host_->associated_cache()->owning_group()->is_being_deleted()) {
return nullptr;
}
std::unique_ptr<AppCacheURLLoader> loader = CreateLoader(network_delegate);
ContinueMaybeLoadSubResource();
return loader;
}
void AppCacheRequestHandler::ContinueMaybeLoadSubResource() {
// 7.9.6 Changes to the networking model
// If the resource is not to be fetched using the HTTP GET mechanism or
// equivalent ... then fetch the resource normally.
DCHECK(loader_.get());
DCHECK(host_->associated_cache() && host_->associated_cache()->is_complete());
const GURL& url = request_->GetURL();
AppCache* cache = host_->associated_cache();
storage()->FindResponseForSubRequest(
host_->associated_cache(), url,
&found_entry_, &found_fallback_entry_, &found_network_namespace_);
if (found_entry_.has_response_id()) {
// Step 2: If there's an entry, get it instead.
DCHECK(!found_network_namespace_ &&
!found_fallback_entry_.has_response_id());
found_cache_id_ = cache->cache_id();
found_group_id_ = cache->owning_group()->group_id();
found_manifest_url_ = cache->owning_group()->manifest_url();
DeliverAppCachedResponse(found_entry_, found_cache_id_, found_manifest_url_,
false, GURL());
return;
}
if (found_fallback_entry_.has_response_id()) {
// Step 4: Fetch the resource normally, if this results
// in certain conditions, then use the fallback.
DCHECK(!found_network_namespace_ &&
!found_entry_.has_response_id());
found_cache_id_ = cache->cache_id();
found_manifest_url_ = cache->owning_group()->manifest_url();
DeliverNetworkResponse();
return;
}
if (found_network_namespace_ ||
base::FeatureList::IsEnabled(kAppCacheAlwaysFallbackToNetwork)) {
// Step 3 and 5: Fetch the resource normally.
DCHECK(!found_entry_.has_response_id() &&
!found_fallback_entry_.has_response_id());
DeliverNetworkResponse();
return;
}
// Step 6: Fail the resource load.
DeliverErrorResponse();
}
void AppCacheRequestHandler::OnCacheSelectionComplete(AppCacheHost* host) {
DCHECK(host == host_);
// Request may have been canceled, but not yet deleted, while waiting on
// the cache.
if (!loader_.get())
return;
if (is_main_resource())
return;
if (!is_waiting_for_cache_selection_)
return;
is_waiting_for_cache_selection_ = false;
if (!host_->associated_cache() ||
!host_->associated_cache()->is_complete()) {
DeliverNetworkResponse();
return;
}
ContinueMaybeLoadSubResource();
}
void AppCacheRequestHandler::MaybeCreateLoader(
const network::ResourceRequest& tentative_resource_request,
BrowserContext* browser_context,
LoaderCallback callback,
FallbackCallback fallback_callback) {
MaybeCreateLoaderInternal(
tentative_resource_request,
base::BindOnce(&AppCacheRequestHandler::RunLoaderCallbackForMainResource,
weak_factory_.GetWeakPtr(),
browser_context, std::move(callback)));
}
void AppCacheRequestHandler::MaybeCreateLoaderInternal(
const network::ResourceRequest& resource_request,
AppCacheLoaderCallback callback) {
loader_callback_ = std::move(callback);
// TODO(crbug.com/876531): Figure out how AppCache interception should
// interact with URLLoaderThrottles. It might be incorrect to store
// |tentative_resource_request| here, since throttles can rewrite headers
// between now and when the request handler passed to |loader_callback_| is
// invoked.
request_->set_request(resource_request);
MaybeLoadResource(nullptr);
// If a job is created, the job assumes ownership of the callback and
// the responsibility to call it. If no job is created, we call it with
// a null callback to let our client know we have no loader for this request.
if (loader_callback_)
std::move(loader_callback_).Run({});
}
bool AppCacheRequestHandler::MaybeCreateLoaderForResponse(
const network::ResourceRequest& request,
network::mojom::URLResponseHeadPtr* response,
mojo::ScopedDataPipeConsumerHandle* response_body,
mojo::PendingRemote<network::mojom::URLLoader>* loader,
mojo::PendingReceiver<network::mojom::URLLoaderClient>* client_receiver,
blink::ThrottlingURLLoader* url_loader,
bool* skip_other_interceptors,
bool* will_return_unsafe_redirect) {
// The sync interface of this method is inherited from the
// NavigationLoaderInterceptor class. The LoaderCallback created here is
// invoked synchronously in fallback cases, and only when there really is
// a loader to start.
bool was_called = false;
loader_callback_ = base::BindOnce(
[](const network::ResourceRequest& resource_request,
mojo::PendingRemote<network::mojom::URLLoader>* loader,
mojo::PendingReceiver<network::mojom::URLLoaderClient>*
client_receiver,
bool* was_called,
SingleRequestURLLoaderFactory::RequestHandler handler) {
*was_called = true;
mojo::PendingRemote<network::mojom::URLLoaderClient> client;
*client_receiver = client.InitWithNewPipeAndPassReceiver();
std::move(handler).Run(resource_request,
loader->InitWithNewPipeAndPassReceiver(),
std::move(client));
},
*(request_->GetResourceRequest()), loader, client_receiver, &was_called);
request_->set_response(response->Clone());
if (!MaybeLoadFallbackForResponse(nullptr)) {
DCHECK(!was_called);
loader_callback_.Reset();
return false;
}
DCHECK(was_called);
// Create a subresource loader if needed (it's a main resource or a dedicated
// worker).
// In appcache, dedicated workers are treated as subresources of their nearest
// ancestor frame's appcache host. On the other hand, dedicated workers need
// their own subresource loader.
if (IsMainRequestDestination(request_destination_) ||
request_destination_ == network::mojom::RequestDestination::kWorker) {
should_create_subresource_loader_ = true;
}
return true;
}
base::Optional<SubresourceLoaderParams>
AppCacheRequestHandler::MaybeCreateSubresourceLoaderParams() {
if (!should_create_subresource_loader_)
return base::nullopt;
// The factory is destroyed when the renderer drops the connection.
mojo::PendingRemote<network::mojom::URLLoaderFactory> factory_remote;
AppCacheSubresourceURLFactory::CreateURLLoaderFactory(
appcache_host_, factory_remote.InitWithNewPipeAndPassReceiver());
SubresourceLoaderParams params;
params.pending_appcache_loader_factory = std::move(factory_remote);
return base::Optional<SubresourceLoaderParams>(std::move(params));
}
void AppCacheRequestHandler::MaybeCreateSubresourceLoader(
const network::ResourceRequest& resource_request,
AppCacheLoaderCallback loader_callback) {
DCHECK(!loader_);
DCHECK(!is_main_resource());
// Subresource loads start out just like a main resource loads, but they go
// down different branches along the way to completion.
MaybeCreateLoaderInternal(resource_request, std::move(loader_callback));
}
void AppCacheRequestHandler::MaybeFallbackForSubresourceResponse(
network::mojom::URLResponseHeadPtr response,
AppCacheLoaderCallback loader_callback) {
DCHECK(!loader_);
DCHECK(!is_main_resource());
loader_callback_ = std::move(loader_callback);
request_->set_response(std::move(response));
MaybeLoadFallbackForResponse(nullptr);
if (loader_callback_)
std::move(loader_callback_).Run({});
}
void AppCacheRequestHandler::MaybeFallbackForSubresourceRedirect(
const net::RedirectInfo& redirect_info,
AppCacheLoaderCallback loader_callback) {
DCHECK(!loader_);
DCHECK(!is_main_resource());
loader_callback_ = std::move(loader_callback);
MaybeLoadFallbackForRedirect(nullptr, redirect_info.new_url);
if (loader_callback_)
std::move(loader_callback_).Run({});
}
void AppCacheRequestHandler::MaybeFollowSubresourceRedirect(
const net::RedirectInfo& redirect_info,
AppCacheLoaderCallback loader_callback) {
DCHECK(!loader_);
DCHECK(!is_main_resource());
loader_callback_ = std::move(loader_callback);
request_->UpdateWithRedirectInfo(redirect_info);
MaybeLoadResource(nullptr);
if (loader_callback_)
std::move(loader_callback_).Run({});
}
// static
void AppCacheRequestHandler::SetRunningInTests(bool in_tests) {
g_running_in_tests = in_tests;
}
// static
bool AppCacheRequestHandler::IsRunningInTests() {
return g_running_in_tests;
}
} // namespace content