blob: 4104663214042f1090bf4d30a57e94c9883f9c9b [file] [log] [blame]
// Copyright 2014 The Chromium Authors
// 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/core/fetch/fetch_manager.h"
#include <stdint.h>
#include <algorithm>
#include <optional>
#include <string_view>
#include <utility>
#include "base/check.h"
#include "base/feature_list.h"
#include "base/metrics/histogram_functions.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/strcat.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/time.h"
#include "base/unguessable_token.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "services/network/public/cpp/features.h"
#include "services/network/public/cpp/header_util.h"
#include "services/network/public/cpp/is_potentially_trustworthy.h"
#include "services/network/public/cpp/request_mode.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/mojom/fetch_api.mojom-blink.h"
#include "services/network/public/mojom/trust_tokens.mojom-blink.h"
#include "services/network/public/mojom/url_loader_factory.mojom-blink.h"
#include "third_party/blink/public/common/browser_interface_broker_proxy.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/scheme_registry.h"
#include "third_party/blink/public/mojom/fetch/fetch_api_request.mojom-blink.h"
#include "third_party/blink/public/mojom/loader/code_cache.mojom-blink.h"
#include "third_party/blink/public/mojom/loader/fetch_later.mojom-blink.h"
#include "third_party/blink/public/platform/task_type.h"
#include "third_party/blink/public/platform/web_url_request.h"
#include "third_party/blink/public/platform/web_url_request_util.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_response_init.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_throw_dom_exception.h"
#include "third_party/blink/renderer/core/dom/abort_signal.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/dom_exception.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/core/fetch/body.h"
#include "third_party/blink/renderer/core/fetch/body_stream_buffer.h"
#include "third_party/blink/renderer/core/fetch/fetch_later_result.h"
#include "third_party/blink/renderer/core/fetch/fetch_request_data.h"
#include "third_party/blink/renderer/core/fetch/form_data_bytes_consumer.h"
#include "third_party/blink/renderer/core/fetch/place_holder_bytes_consumer.h"
#include "third_party/blink/renderer/core/fetch/response.h"
#include "third_party/blink/renderer/core/fetch/trust_token_to_mojom.h"
#include "third_party/blink/renderer/core/fileapi/blob.h"
#include "third_party/blink/renderer/core/frame/csp/content_security_policy.h"
#include "third_party/blink/renderer/core/frame/frame.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/frame/local_frame_client.h"
#include "third_party/blink/renderer/core/inspector/console_message.h"
#include "third_party/blink/renderer/core/inspector/identifiers_factory.h"
#include "third_party/blink/renderer/core/inspector/inspector_audits_issue.h"
#include "third_party/blink/renderer/core/loader/subresource_integrity_helper.h"
#include "third_party/blink/renderer/core/loader/threadable_loader.h"
#include "third_party/blink/renderer/core/loader/threadable_loader_client.h"
#include "third_party/blink/renderer/core/page/chrome_client.h"
#include "third_party/blink/renderer/core/page/page.h"
#include "third_party/blink/renderer/core/probe/core_probes.h"
#include "third_party/blink/renderer/core/typed_arrays/dom_array_buffer.h"
#include "third_party/blink/renderer/core/workers/shared_worker_global_scope.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/bindings/script_forbidden_scope.h"
#include "third_party/blink/renderer/platform/bindings/script_state.h"
#include "third_party/blink/renderer/platform/bindings/thread_debugger.h"
#include "third_party/blink/renderer/platform/bindings/v8_throw_exception.h"
#include "third_party/blink/renderer/platform/heap/garbage_collected.h"
#include "third_party/blink/renderer/platform/heap/persistent.h"
#include "third_party/blink/renderer/platform/instrumentation/use_counter.h"
#include "third_party/blink/renderer/platform/loader/cors/cors.h"
#include "third_party/blink/renderer/platform/loader/fetch/buffering_bytes_consumer.h"
#include "third_party/blink/renderer/platform/loader/fetch/bytes_consumer.h"
#include "third_party/blink/renderer/platform/loader/fetch/cached_metadata.h"
#include "third_party/blink/renderer/platform/loader/fetch/fetch_initiator_type_names.h"
#include "third_party/blink/renderer/platform/loader/fetch/fetch_parameters.h"
#include "third_party/blink/renderer/platform/loader/fetch/fetch_utils.h"
#include "third_party/blink/renderer/platform/loader/fetch/resource.h"
#include "third_party/blink/renderer/platform/loader/fetch/resource_error.h"
#include "third_party/blink/renderer/platform/loader/fetch/resource_fetcher.h"
#include "third_party/blink/renderer/platform/loader/fetch/resource_fetcher_properties.h"
#include "third_party/blink/renderer/platform/loader/fetch/resource_loader_options.h"
#include "third_party/blink/renderer/platform/loader/fetch/resource_request.h"
#include "third_party/blink/renderer/platform/loader/fetch/resource_request_utils.h"
#include "third_party/blink/renderer/platform/loader/fetch/resource_response.h"
#include "third_party/blink/renderer/platform/loader/fetch/script_cached_metadata_handler.h"
#include "third_party/blink/renderer/platform/loader/fetch/unique_identifier.h"
#include "third_party/blink/renderer/platform/loader/fetch/url_loader/request_conversion.h"
#include "third_party/blink/renderer/platform/loader/subresource_integrity.h"
#include "third_party/blink/renderer/platform/mojo/heap_mojo_associated_remote.h"
#include "third_party/blink/renderer/platform/network/http_names.h"
#include "third_party/blink/renderer/platform/network/network_utils.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
#include "third_party/blink/renderer/platform/weborigin/kurl.h"
#include "third_party/blink/renderer/platform/weborigin/scheme_registry.h"
#include "third_party/blink/renderer/platform/weborigin/security_origin.h"
#include "third_party/blink/renderer/platform/weborigin/security_policy.h"
#include "third_party/blink/renderer/platform/wtf/functional.h"
#include "third_party/blink/renderer/platform/wtf/hash_set.h"
#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
#include "third_party/blink/renderer/platform/wtf/vector.h"
#include "v8/include/v8.h"
using network::mojom::CredentialsMode;
using network::mojom::FetchResponseType;
using network::mojom::RedirectMode;
using network::mojom::RequestMode;
namespace blink {
namespace {
// 64 kilobytes.
constexpr uint64_t kMaxScheduledDeferredBytesPerOrigin = 64 * 1024;
constexpr ResourceType kFetchLaterResourceType = ResourceType::kRaw;
constexpr TextResourceDecoderOptions::ContentType kFetchLaterContentType =
TextResourceDecoderOptions::kPlainTextContent;
constexpr net::NetworkTrafficAnnotationTag kFetchLaterTrafficAnnotationTag =
net::DefineNetworkTrafficAnnotation("blink_fetch_later_manager",
R"(
semantics {
sender: "Blink Fetch Later Manager"
description:
"This request is a website-initiated FetchLater request."
trigger:
"On document unloaded or after developer specified timeout has passed."
data: "Anything the initiator wants to send."
user_data {
type: ARBITRARY_DATA
}
destination: OTHER
internal {
contacts {
email: "pending-beacon-experiment@chromium.org"
}
}
last_reviewed: "2023-10-25"
}
policy {
cookies_allowed: YES
cookies_store: "user"
setting: "These requests cannot be fully disabled in settings. "
"Only for the requests intended to send after document in BFCache, "
"they can be disabled via the `Background Sync` section under the "
"`Privacy and security` tab in settings. "
"This feature is not yet enabled."
policy_exception_justification: "The policy for Background sync is not "
"yet implemented."
})");
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
//
// Must remain in sync with FetchLaterRendererMetricType in
// tools/metrics/histograms/enums.xml.
enum class FetchLaterRendererMetricType {
kAbortedByUser = 0,
kContextDestroyed = 1,
kActivatedByTimeout = 2,
kActivatedOnEnteredBackForwardCache = 3,
kMaxValue = kActivatedOnEnteredBackForwardCache,
};
void LogFetchLaterMetric(const FetchLaterRendererMetricType& type) {
base::UmaHistogramEnumeration("FetchLater.Renderer.Metrics", type);
}
// Tells whether the FetchLater request should use BackgroundSync permission to
// decide whether it should send out deferred requests on entering
// BackForwardCache.
bool IsFetchLaterUseBackgroundSyncPermissionEnabled() {
return base::GetFieldTrialParamByFeatureAsBool(
features::kFetchLaterAPI, "use_background_sync_permission", true);
}
// Allows manually overriding the "send-on-enter-bfcache" behavior without
// considering BackgroundSync permission.
// Defaults to true to flush on entering BackForwardCache.
// See also
// https://github.com/WICG/pending-beacon/issues/30#issuecomment-1333869614
bool IsFetchLaterSendOnEnterBackForwardCacheEnabled() {
return base::GetFieldTrialParamByFeatureAsBool(features::kFetchLaterAPI,
"send_on_enter_bfcache", true);
}
bool HasNonEmptyLocationHeader(const FetchHeaderList* headers) {
String value;
if (!headers->Get(http_names::kLocation, value))
return false;
return !value.empty();
}
const char* SerializeTrustTokenOperationType(
network::mojom::TrustTokenOperationType operation_type) {
switch (operation_type) {
case network::mojom::blink::TrustTokenOperationType::kIssuance:
return "Issuance";
case network::mojom::blink::TrustTokenOperationType::kRedemption:
return "Redemption";
case network::mojom::blink::TrustTokenOperationType::kSigning:
return "Signing";
}
}
// Logs a net error describing why a fetch with Trust Tokens parameters
// failed. This is a temporary measure for debugging a surprisingly high
// incidence of "TypeError: Failed to fetch" when executing Trust Tokens
// issuance operations (crbug.com/1128174).
void HistogramNetErrorForTrustTokensOperation(
network::mojom::blink::TrustTokenOperationType operation_type,
int net_error) {
base::UmaHistogramSparse(
base::StrCat({"Net.TrustTokens.NetErrorForFetchFailure", ".",
SerializeTrustTokenOperationType(operation_type)}),
net_error);
}
ResourceLoadPriority ComputeFetchLaterLoadPriority(
const FetchParameters& params) {
// FetchLater's ResourceType is ResourceType::kRaw, which should default to
// ResourceLoadPriority::kHigh priority. See also TypeToPriority() in
// resource_fetcher.cc
return AdjustPriorityWithPriorityHintAndRenderBlocking(
ResourceLoadPriority::kHigh, kFetchLaterResourceType,
params.GetResourceRequest().GetFetchPriorityHint(),
params.GetRenderBlockingBehavior());
// TODO(crbug.com/1465781): Apply kLow when IsSubframeDeprioritizationEnabled.
}
} // namespace
// FetchLoaderBase provides common logic to prepare a blink::ResourceRequest
// from a FetchRequestData.
class FetchLoaderBase : public GarbageCollectedMixin {
public:
explicit FetchLoaderBase(ExecutionContext* ec,
FetchRequestData* data,
ScriptState* script_state,
AbortSignal* signal)
: execution_context_(ec),
fetch_request_data_(data),
script_state_(script_state),
world_(std::move(&script_state->World())),
signal_(signal),
abort_handle_(signal->AddAlgorithm(
WTF::BindOnce(&FetchLoaderBase::Abort, WrapWeakPersistent(this)))) {
CHECK(world_);
}
// Starts to perform the "Fetching" algorithm.
// https://fetch.spec.whatwg.org/#fetching
// Note that the actual loading is delegated to subclass via `CreateLoader()`,
// which may or may not start loading immediately.
void Start(ExceptionState&);
// Disposes this loader.
// The owner of this loader uses this method to notify disposing of this
// loader after removing from its container.
// Depending on how subclass is implemented, this method may be called
// multiple times before this instance is gone.
virtual void Dispose() = 0;
void Trace(Visitor* visitor) const override {
visitor->Trace(execution_context_);
visitor->Trace(fetch_request_data_);
visitor->Trace(script_state_);
visitor->Trace(signal_);
visitor->Trace(abort_handle_);
visitor->Trace(world_);
}
protected:
virtual bool IsDeferred() const = 0;
virtual void Abort() = 0;
virtual void CreateLoader(
ResourceRequest request,
const ResourceLoaderOptions& resource_loader_options) = 0;
virtual void Failed(
const String& message,
DOMException* dom_exception,
std::optional<String> devtools_request_id = std::nullopt,
std::optional<base::UnguessableToken> issue_id = std::nullopt) = 0;
void PerformSchemeFetch(ExceptionState&);
void PerformNetworkError(
const String& message,
std::optional<base::UnguessableToken> issue_id = std::nullopt);
void FileIssueAndPerformNetworkError(RendererCorsIssueCode,
int64_t identifier);
void PerformHTTPFetch(ExceptionState&);
void PerformDataFetch();
bool AddConsoleMessage(const String& message,
std::optional<base::UnguessableToken> issue_id);
ExecutionContext* GetExecutionContext() { return execution_context_.Get(); }
void SetExecutionContext(ExecutionContext* ec) { execution_context_ = ec; }
FetchRequestData* GetFetchRequestData() const {
return fetch_request_data_.Get();
}
ScriptState* GetScriptState() { return script_state_.Get(); }
const DOMWrapperWorld* World() { return world_; }
AbortSignal* Signal() { return signal_.Get(); }
private:
Member<ExecutionContext> execution_context_;
Member<FetchRequestData> fetch_request_data_;
Member<ScriptState> script_state_;
Member<const DOMWrapperWorld> world_;
Member<AbortSignal> signal_;
Member<AbortSignal::AlgorithmHandle> abort_handle_;
};
class FetchManager::Loader final
: public GarbageCollected<FetchManager::Loader>,
public FetchLoaderBase,
public ThreadableLoaderClient {
public:
Loader(ExecutionContext*,
FetchManager*,
ScriptPromiseResolver<Response>*,
FetchRequestData*,
ScriptState*,
AbortSignal*);
~Loader() override;
void Trace(Visitor*) const override;
void Dispose() override;
void LogIfKeepalive(std::string_view request_state) const;
// ThreadableLoaderClient implementation.
bool WillFollowRedirect(uint64_t,
const KURL&,
const ResourceResponse&) override;
void DidReceiveResponse(uint64_t, const ResourceResponse&) override;
void DidReceiveCachedMetadata(mojo_base::BigBuffer) override;
void DidStartLoadingResponseBody(BytesConsumer&) override;
void DidFinishLoading(uint64_t) override;
void DidFail(uint64_t, const ResourceError&) override;
void DidFailRedirectCheck(uint64_t) override;
class SRIVerifier final : public GarbageCollected<SRIVerifier>,
public BytesConsumer::Client {
public:
SRIVerifier(BytesConsumer* body,
PlaceHolderBytesConsumer* updater,
Response* response,
FetchManager::Loader* loader,
String integrity_metadata,
const KURL& url,
FetchResponseType response_type)
: body_(body),
updater_(updater),
response_(response),
loader_(loader),
integrity_metadata_(integrity_metadata),
url_(url),
response_type_(response_type),
finished_(false) {
body_->SetClient(this);
OnStateChange();
}
void Cancel() { body_->Cancel(); }
void OnStateChange() override {
using Result = BytesConsumer::Result;
DCHECK(loader_);
DCHECK(response_);
Result result = Result::kOk;
while (result == Result::kOk) {
const char* buffer;
size_t available;
result = body_->BeginRead(&buffer, &available);
if (result == Result::kOk) {
buffer_.Append(buffer, base::checked_cast<wtf_size_t>(available));
result = body_->EndRead(available);
}
if (result == Result::kShouldWait)
return;
}
finished_ = true;
if (result == Result::kDone) {
SubresourceIntegrity::ReportInfo report_info;
bool check_result = true;
bool body_is_null = !updater_;
if (body_is_null || (response_type_ != FetchResponseType::kBasic &&
response_type_ != FetchResponseType::kCors &&
response_type_ != FetchResponseType::kDefault)) {
report_info.AddConsoleErrorMessage(
"Subresource Integrity: The resource '" + url_.ElidedString() +
"' has an integrity attribute, but the response is not "
"eligible for integrity validation.");
check_result = false;
}
if (check_result) {
check_result = SubresourceIntegrity::CheckSubresourceIntegrity(
integrity_metadata_,
SubresourceIntegrityHelper::GetFeatures(
loader_->GetExecutionContext()),
buffer_.data(), buffer_.size(), url_, report_info);
}
SubresourceIntegrityHelper::DoReport(*loader_->GetExecutionContext(),
report_info);
if (check_result) {
updater_->Update(MakeGarbageCollected<FormDataBytesConsumer>(
buffer_.data(), buffer_.size()));
loader_->resolver_->Resolve(response_);
loader_->resolver_.Clear();
return;
}
}
String error_message =
"Unknown error occurred while trying to verify integrity.";
if (updater_) {
updater_->Update(
BytesConsumer::CreateErrored(BytesConsumer::Error(error_message)));
}
loader_->PerformNetworkError(error_message);
}
String DebugName() const override { return "SRIVerifier"; }
bool IsFinished() const { return finished_; }
void Trace(Visitor* visitor) const override {
visitor->Trace(body_);
visitor->Trace(updater_);
visitor->Trace(response_);
visitor->Trace(loader_);
}
private:
Member<BytesConsumer> body_;
Member<PlaceHolderBytesConsumer> updater_;
// We cannot store a Response because its JS wrapper can be collected.
// TODO(yhirano): Fix this.
Member<Response> response_;
Member<FetchManager::Loader> loader_;
String integrity_metadata_;
KURL url_;
const FetchResponseType response_type_;
Vector<char> buffer_;
bool finished_;
};
private:
bool IsDeferred() const override;
void Abort() override;
void NotifyFinished();
void CreateLoader(
ResourceRequest request,
const ResourceLoaderOptions& resource_loader_options) override;
// If |dom_exception| is provided, throws the specified DOMException instead
// of the usual "Failed to fetch" TypeError.
void Failed(
const String& message,
DOMException* dom_exception,
std::optional<String> devtools_request_id = std::nullopt,
std::optional<base::UnguessableToken> issue_id = std::nullopt) override;
Member<FetchManager> fetch_manager_;
Member<ScriptPromiseResolver<Response>> resolver_;
Member<ThreadableLoader> threadable_loader_;
Member<PlaceHolderBytesConsumer> place_holder_body_;
bool failed_;
bool finished_;
int response_http_status_code_;
bool response_has_no_store_header_ = false;
Member<SRIVerifier> integrity_verifier_;
Vector<KURL> url_list_;
Member<ScriptCachedMetadataHandler> cached_metadata_handler_;
TraceWrapperV8Reference<v8::Value> exception_;
base::TimeTicks request_started_time_;
};
FetchManager::Loader::Loader(ExecutionContext* execution_context,
FetchManager* fetch_manager,
ScriptPromiseResolver<Response>* resolver,
FetchRequestData* fetch_request_data,
ScriptState* script_state,
AbortSignal* signal)
: FetchLoaderBase(execution_context,
fetch_request_data,
script_state,
signal),
fetch_manager_(fetch_manager),
resolver_(resolver),
failed_(false),
finished_(false),
response_http_status_code_(0),
integrity_verifier_(nullptr),
request_started_time_(base::TimeTicks::Now()) {
DCHECK(World());
url_list_.push_back(fetch_request_data->Url());
v8::Isolate* isolate = script_state->GetIsolate();
// Only use a handle scope as we should be in the right context already.
v8::HandleScope scope(isolate);
// Create the exception at this point so we get the stack-trace that belongs
// to the fetch() call.
v8::Local<v8::Value> exception =
V8ThrowException::CreateTypeError(isolate, "Failed to fetch");
exception_.Reset(isolate, exception);
}
FetchManager::Loader::~Loader() {
DCHECK(!threadable_loader_);
}
void FetchManager::Loader::Trace(Visitor* visitor) const {
visitor->Trace(fetch_manager_);
visitor->Trace(resolver_);
visitor->Trace(threadable_loader_);
visitor->Trace(place_holder_body_);
visitor->Trace(integrity_verifier_);
visitor->Trace(cached_metadata_handler_);
visitor->Trace(exception_);
FetchLoaderBase::Trace(visitor);
ThreadableLoaderClient::Trace(visitor);
}
bool FetchManager::Loader::WillFollowRedirect(
uint64_t identifier,
const KURL& url,
const ResourceResponse& response) {
const auto redirect_mode = GetFetchRequestData()->Redirect();
if (redirect_mode == network::mojom::RedirectMode::kError) {
DidFailRedirectCheck(identifier);
Dispose();
return false;
}
if (redirect_mode == network::mojom::RedirectMode::kManual) {
const uint64_t unused = 0;
// There is no need to read the body of redirect response because there is
// no way to read the body of opaque-redirect filtered response's internal
// response.
// TODO(horo): If we support any API which expose the internal body, we
// will have to read the body. And also HTTPCache changes will be needed
// because it doesn't store the body of redirect responses.
DidReceiveResponse(unused, response);
DidStartLoadingResponseBody(*BytesConsumer::CreateClosed());
if (threadable_loader_)
NotifyFinished();
Dispose();
return false;
}
DCHECK_EQ(redirect_mode, network::mojom::RedirectMode::kFollow);
url_list_.push_back(url);
return true;
}
void FetchManager::Loader::DidReceiveResponse(
uint64_t,
const ResourceResponse& response) {
// Verify that we're dealing with the URL we expect (which could be an
// HTTPS-upgraded variant of `url_list_.back()`.
DCHECK(
response.CurrentRequestUrl() == url_list_.back() ||
(response.CurrentRequestUrl().ProtocolIs("https") &&
url_list_.back().ProtocolIs("http") &&
response.CurrentRequestUrl().Host() == url_list_.back().Host() &&
response.CurrentRequestUrl().GetPath() == url_list_.back().GetPath() &&
response.CurrentRequestUrl().Query() == url_list_.back().Query()));
auto response_type = response.GetType();
DCHECK_NE(response_type, FetchResponseType::kError);
LogIfKeepalive("Succeeded");
ScriptState::Scope scope(GetScriptState());
response_http_status_code_ = response.HttpStatusCode();
if (response.MimeType() == "application/wasm" &&
(response.CurrentRequestUrl().ProtocolIsInHTTPFamily() ||
CommonSchemeRegistry::IsExtensionScheme(
response.CurrentRequestUrl().Protocol().Ascii()))) {
// We create a ScriptCachedMetadataHandler for WASM modules.
cached_metadata_handler_ =
MakeGarbageCollected<ScriptCachedMetadataHandler>(
WTF::TextEncoding(),
CachedMetadataSender::Create(
response, mojom::blink::CodeCacheType::kWebAssembly,
GetExecutionContext()->GetSecurityOrigin()));
}
place_holder_body_ = MakeGarbageCollected<PlaceHolderBytesConsumer>();
FetchResponseData* response_data = FetchResponseData::CreateWithBuffer(
BodyStreamBuffer::Create(GetScriptState(), place_holder_body_, Signal(),
cached_metadata_handler_));
if (!GetExecutionContext() || GetExecutionContext()->IsContextDestroyed() ||
response.GetType() == FetchResponseType::kError) {
// BodyStreamBuffer::Create() may run scripts and cancel this request.
// Do nothing in such a case.
// See crbug.com/1373785 for more details.
return;
}
DCHECK_EQ(response_type, response.GetType());
DCHECK(!(network_utils::IsRedirectResponseCode(response_http_status_code_) &&
HasNonEmptyLocationHeader(response_data->HeaderList()) &&
GetFetchRequestData()->Redirect() != RedirectMode::kManual));
if (network_utils::IsRedirectResponseCode(response_http_status_code_) &&
GetFetchRequestData()->Redirect() == RedirectMode::kManual) {
response_type = network::mojom::FetchResponseType::kOpaqueRedirect;
}
response_data->InitFromResourceResponse(
GetExecutionContext(), response_type, url_list_,
GetFetchRequestData()->Method(), GetFetchRequestData()->Credentials(),
response);
FetchResponseData* tainted_response = nullptr;
switch (response_type) {
case FetchResponseType::kBasic:
case FetchResponseType::kDefault:
tainted_response = response_data->CreateBasicFilteredResponse();
break;
case FetchResponseType::kCors: {
HTTPHeaderSet header_names = cors::ExtractCorsExposedHeaderNamesList(
GetFetchRequestData()->Credentials(), response);
tainted_response =
response_data->CreateCorsFilteredResponse(header_names);
break;
}
case FetchResponseType::kOpaque:
tainted_response = response_data->CreateOpaqueFilteredResponse();
break;
case FetchResponseType::kOpaqueRedirect:
tainted_response = response_data->CreateOpaqueRedirectFilteredResponse();
break;
case FetchResponseType::kError:
NOTREACHED();
break;
}
// TODO(crbug.com/1288221): Remove this once the investigation is done.
CHECK(tainted_response);
response_has_no_store_header_ = response.CacheControlContainsNoStore();
Response* r =
Response::Create(resolver_->GetExecutionContext(), tainted_response);
r->headers()->SetGuard(Headers::kImmutableGuard);
if (GetFetchRequestData()->Integrity().empty()) {
resolver_->Resolve(r);
resolver_.Clear();
} else {
DCHECK(!integrity_verifier_);
// We have another place holder body for SRI.
PlaceHolderBytesConsumer* verified = place_holder_body_;
place_holder_body_ = MakeGarbageCollected<PlaceHolderBytesConsumer>();
BytesConsumer* underlying = place_holder_body_;
integrity_verifier_ = MakeGarbageCollected<SRIVerifier>(
underlying, verified, r, this, GetFetchRequestData()->Integrity(),
response.CurrentRequestUrl(), r->GetResponse()->GetType());
}
}
void FetchManager::Loader::DidReceiveCachedMetadata(mojo_base::BigBuffer data) {
if (cached_metadata_handler_) {
cached_metadata_handler_->SetSerializedCachedMetadata(std::move(data));
}
}
void FetchManager::Loader::DidStartLoadingResponseBody(BytesConsumer& body) {
if (GetFetchRequestData()->Integrity().empty() &&
!response_has_no_store_header_) {
// BufferingBytesConsumer reads chunks from |bytes_consumer| as soon as
// they get available to relieve backpressure. Buffering starts after
// a short delay, however, to allow the Response to be drained; e.g.
// when the Response is passed to FetchEvent.respondWith(), etc.
//
// https://fetch.spec.whatwg.org/#fetching
// The user agent should ignore the suspension request if the ongoing
// fetch is updating the response in the HTTP cache for the request.
place_holder_body_->Update(BufferingBytesConsumer::CreateWithDelay(
&body, GetExecutionContext()->GetTaskRunner(TaskType::kNetworking)));
} else {
place_holder_body_->Update(&body);
}
place_holder_body_ = nullptr;
}
void FetchManager::Loader::DidFinishLoading(uint64_t) {
DCHECK(!place_holder_body_);
DCHECK(!failed_);
finished_ = true;
auto* window = DynamicTo<LocalDOMWindow>(GetExecutionContext());
if (window && window->GetFrame() &&
network::IsSuccessfulStatus(response_http_status_code_)) {
window->GetFrame()->GetPage()->GetChromeClient().AjaxSucceeded(
window->GetFrame());
}
NotifyFinished();
}
void FetchManager::Loader::DidFail(uint64_t identifier,
const ResourceError& error) {
if (GetFetchRequestData() && GetFetchRequestData()->TrustTokenParams()) {
HistogramNetErrorForTrustTokensOperation(
GetFetchRequestData()->TrustTokenParams()->operation,
error.ErrorCode());
}
if (error.TrustTokenOperationError() !=
network::mojom::blink::TrustTokenOperationStatus::kOk) {
Failed(String(),
TrustTokenErrorToDOMException(error.TrustTokenOperationError()),
IdentifiersFactory::SubresourceRequestId(identifier));
return;
}
auto issue_id = error.CorsErrorStatus()
? std::optional<base::UnguessableToken>(
error.CorsErrorStatus()->issue_id)
: std::nullopt;
Failed(String(), nullptr,
IdentifiersFactory::SubresourceRequestId(identifier), issue_id);
}
void FetchManager::Loader::DidFailRedirectCheck(uint64_t identifier) {
Failed(String(), nullptr,
IdentifiersFactory::SubresourceRequestId(identifier));
}
void FetchLoaderBase::Start(ExceptionState& exception_state) {
// "1. If |request|'s url contains a Known HSTS Host, modify it per the
// requirements of the 'URI [sic] Loading and Port Mapping' chapter of HTTP
// Strict Transport Security."
// FIXME: Implement this.
// "2. If |request|'s referrer is not none, set |request|'s referrer to the
// result of invoking determine |request|'s referrer."
// We set the referrer using workerGlobalScope's URL in
// WorkerThreadableLoader.
// "3. If |request|'s synchronous flag is unset and fetch is not invoked
// recursively, run the remaining steps asynchronously."
// We don't support synchronous flag.
// "4. Let response be the value corresponding to the first matching
// statement:"
// "- should fetching |request| be blocked as mixed content returns blocked"
// We do mixed content checking in ResourceFetcher.
// "- should fetching |request| be blocked as content security returns
// blocked"
CHECK(execution_context_);
if (!execution_context_->GetContentSecurityPolicyForWorld(world_.Get())
->AllowConnectToSource(fetch_request_data_->Url(),
fetch_request_data_->Url(),
RedirectStatus::kNoRedirect)) {
// "A network error."
PerformNetworkError(
"Refused to connect to '" + fetch_request_data_->Url().ElidedString() +
"' because it violates the document's Content Security Policy.");
return;
}
const KURL& url = fetch_request_data_->Url();
// "- |request|'s url's origin is same origin with |request|'s origin,
// |request|'s tainted origin flag is unset, and the CORS flag is unset"
// Note tainted origin flag is always unset here.
// Note we don't support to call this method with |CORS flag|
// "- |request|'s current URL's scheme is |data|"
// "- |request|'s mode is |navigate| or |websocket|".
if (fetch_request_data_->Origin()->CanReadContent(url) ||
(fetch_request_data_->IsolatedWorldOrigin() &&
fetch_request_data_->IsolatedWorldOrigin()->CanReadContent(url)) ||
fetch_request_data_->Mode() == network::mojom::RequestMode::kNavigate) {
// "The result of performing a scheme fetch using request."
PerformSchemeFetch(exception_state);
return;
}
// "- |request|'s mode is |same-origin|"
if (fetch_request_data_->Mode() == RequestMode::kSameOrigin) {
// This error is so early that there isn't an identifier yet, generate one.
FileIssueAndPerformNetworkError(RendererCorsIssueCode::kDisallowedByMode,
CreateUniqueIdentifier());
return;
}
// "- |request|'s mode is |no CORS|"
if (fetch_request_data_->Mode() == RequestMode::kNoCors) {
// "If |request|'s redirect mode is not |follow|, then return a network
// error.
if (fetch_request_data_->Redirect() != RedirectMode::kFollow) {
// This error is so early that there isn't an identifier yet, generate
// one.
FileIssueAndPerformNetworkError(
RendererCorsIssueCode::kNoCorsRedirectModeNotFollow,
CreateUniqueIdentifier());
return;
}
// "Set |request|'s response tainting to |opaque|."
// Response tainting is calculated in the CORS module in the network
// service.
//
// "The result of performing a scheme fetch using |request|."
PerformSchemeFetch(exception_state);
return;
}
// "- |request|'s url's scheme is not one of 'http' and 'https'"
// This may include other HTTP-like schemes if the embedder has added them
// to SchemeRegistry::registerURLSchemeAsSupportingFetchAPI.
if (!SchemeRegistry::ShouldTreatURLSchemeAsSupportingFetchAPI(
fetch_request_data_->Url().Protocol())) {
// This error is so early that there isn't an identifier yet, generate one.
FileIssueAndPerformNetworkError(RendererCorsIssueCode::kCorsDisabledScheme,
CreateUniqueIdentifier());
return;
}
// "Set |request|'s response tainting to |CORS|."
// Response tainting is calculated in the CORS module in the network
// service.
// "The result of performing an HTTP fetch using |request| with the
// |CORS flag| set."
PerformHTTPFetch(exception_state);
}
void FetchManager::Loader::Dispose() {
// Prevent notification
fetch_manager_ = nullptr;
if (threadable_loader_) {
if (GetFetchRequestData()->Keepalive()) {
threadable_loader_->Detach();
} else {
threadable_loader_->Cancel();
}
threadable_loader_ = nullptr;
}
if (integrity_verifier_)
integrity_verifier_->Cancel();
SetExecutionContext(nullptr);
}
// https://fetch.spec.whatwg.org/#abort-fetch
// To abort a fetch() call with a promise, request, responseObject, and an
// error:
void FetchManager::Loader::Abort() {
ScriptState* script_state = GetScriptState();
v8::Local<v8::Value> error = Signal()->reason(script_state).V8Value();
// 1. Reject promise with error.
if (resolver_) {
resolver_->Reject(error);
resolver_.Clear();
}
if (threadable_loader_) {
// Prevent re-entrancy.
auto loader = threadable_loader_;
threadable_loader_ = nullptr;
loader->Cancel();
}
// 2. If request’s body is non-null and is readable, then cancel request’s
// body with error.
if (FetchRequestData* fetch_request_data = GetFetchRequestData()) {
if (BodyStreamBuffer* body_stream_buffer = fetch_request_data->Buffer()) {
if (ReadableStream* readable_stream = body_stream_buffer->Stream()) {
ReadableStream::Cancel(script_state, readable_stream, error);
}
}
}
NotifyFinished();
}
void FetchLoaderBase::PerformSchemeFetch(ExceptionState& exception_state) {
// "To perform a scheme fetch using |request|, switch on |request|'s url's
// scheme, and run the associated steps:"
if (SchemeRegistry::ShouldTreatURLSchemeAsSupportingFetchAPI(
fetch_request_data_->Url().Protocol()) ||
fetch_request_data_->Url().ProtocolIs("blob")) {
// "Return the result of performing an HTTP fetch using |request|."
PerformHTTPFetch(exception_state);
} else if (fetch_request_data_->Url().ProtocolIsData()) {
PerformDataFetch();
} else {
// FIXME: implement other protocols.
// This error is so early that there isn't an identifier yet, generate one.
FileIssueAndPerformNetworkError(RendererCorsIssueCode::kCorsDisabledScheme,
CreateUniqueIdentifier());
}
}
void FetchLoaderBase::FileIssueAndPerformNetworkError(
RendererCorsIssueCode network_error,
int64_t identifier) {
auto issue_id = base::UnguessableToken::Create();
switch (network_error) {
case RendererCorsIssueCode::kCorsDisabledScheme: {
AuditsIssue::ReportCorsIssue(
execution_context_, identifier, network_error,
fetch_request_data_->Url().GetString(),
fetch_request_data_->Origin()->ToString(),
fetch_request_data_->Url().Protocol(), issue_id);
PerformNetworkError(
"Fetch API cannot load " + fetch_request_data_->Url().GetString() +
". URL scheme \"" + fetch_request_data_->Url().Protocol() +
"\" is not supported.",
issue_id);
break;
}
case RendererCorsIssueCode::kDisallowedByMode: {
AuditsIssue::ReportCorsIssue(execution_context_, identifier,
network_error,
fetch_request_data_->Url().GetString(),
fetch_request_data_->Origin()->ToString(),
WTF::g_empty_string, issue_id);
PerformNetworkError(
"Fetch API cannot load " + fetch_request_data_->Url().GetString() +
". Request mode is \"same-origin\" but the URL\'s "
"origin is not same as the request origin " +
fetch_request_data_->Origin()->ToString() + ".",
issue_id);
break;
}
case RendererCorsIssueCode::kNoCorsRedirectModeNotFollow: {
AuditsIssue::ReportCorsIssue(execution_context_, identifier,
network_error,
fetch_request_data_->Url().GetString(),
fetch_request_data_->Origin()->ToString(),
WTF::g_empty_string, issue_id);
PerformNetworkError(
"Fetch API cannot load " + fetch_request_data_->Url().GetString() +
". Request mode is \"no-cors\" but the redirect mode "
"is not \"follow\".",
issue_id);
break;
}
}
}
void FetchLoaderBase::PerformNetworkError(
const String& message,
std::optional<base::UnguessableToken> issue_id) {
Failed(message, nullptr, std::nullopt, issue_id);
}
void FetchLoaderBase::PerformHTTPFetch(ExceptionState& exception_state) {
// CORS preflight fetch procedure is implemented inside ThreadableLoader.
// "1. Let |HTTPRequest| be a copy of |request|, except that |HTTPRequest|'s
// body is a tee of |request|'s body."
// We use ResourceRequest class for HTTPRequest.
// FIXME: Support body.
ResourceRequest request(fetch_request_data_->Url());
request.SetRequestorOrigin(fetch_request_data_->Origin());
request.SetNavigationRedirectChain(
fetch_request_data_->NavigationRedirectChain());
request.SetIsolatedWorldOrigin(fetch_request_data_->IsolatedWorldOrigin());
request.SetRequestContext(mojom::blink::RequestContextType::FETCH);
request.SetRequestDestination(fetch_request_data_->Destination());
request.SetFetchLikeAPI(true);
request.SetHttpMethod(fetch_request_data_->Method());
request.SetFetchWindowId(fetch_request_data_->WindowId());
request.SetTrustTokenParams(fetch_request_data_->TrustTokenParams());
request.SetMode(fetch_request_data_->Mode());
request.SetTargetAddressSpace(fetch_request_data_->TargetAddressSpace());
request.SetCredentialsMode(fetch_request_data_->Credentials());
for (const auto& header : fetch_request_data_->HeaderList()->List()) {
request.AddHttpHeaderField(AtomicString(header.first),
AtomicString(header.second));
}
if (fetch_request_data_->Method() != http_names::kGET &&
fetch_request_data_->Method() != http_names::kHEAD) {
if (fetch_request_data_->Buffer()) {
scoped_refptr<EncodedFormData> form_data =
fetch_request_data_->Buffer()->DrainAsFormData(exception_state);
if (form_data) {
request.SetHttpBody(form_data);
} else if (RuntimeEnabledFeatures::FetchUploadStreamingEnabled(
execution_context_)) {
UseCounter::Count(execution_context_,
WebFeature::kFetchUploadStreaming);
DCHECK(!fetch_request_data_->Buffer()->IsStreamLocked());
mojo::PendingRemote<network::mojom::blink::ChunkedDataPipeGetter>
pending_remote;
fetch_request_data_->Buffer()->DrainAsChunkedDataPipeGetter(
script_state_, pending_remote.InitWithNewPipeAndPassReceiver(),
/*client=*/nullptr);
request.MutableBody().SetStreamBody(std::move(pending_remote));
}
}
}
request.SetCacheMode(fetch_request_data_->CacheMode());
request.SetRedirectMode(fetch_request_data_->Redirect());
request.SetFetchPriorityHint(fetch_request_data_->FetchPriorityHint());
request.SetPriority(fetch_request_data_->Priority());
request.SetUseStreamOnResponse(true);
request.SetReferrerString(fetch_request_data_->ReferrerString());
request.SetReferrerPolicy(fetch_request_data_->GetReferrerPolicy());
if (IsDeferred()) {
// https://whatpr.org/fetch/1647/9ca4bda...9994c1d.html#request-a-deferred-fetch
// "Deferred fetching"
// 4. Set request’s service-workers mode to "none".
request.SetSkipServiceWorker(true);
} else {
request.SetSkipServiceWorker(world_->IsIsolatedWorld());
}
if (fetch_request_data_->Keepalive()) {
request.SetKeepalive(true);
UseCounter::Count(execution_context_, mojom::WebFeature::kFetchKeepalive);
}
request.SetBrowsingTopics(fetch_request_data_->BrowsingTopics());
request.SetAdAuctionHeaders(fetch_request_data_->AdAuctionHeaders());
request.SetAttributionReportingEligibility(
fetch_request_data_->AttributionReportingEligibility());
request.SetSharedStorageWritableOptedIn(
fetch_request_data_->SharedStorageWritable());
request.SetOriginalDestination(fetch_request_data_->OriginalDestination());
request.SetServiceWorkerRaceNetworkRequestToken(
fetch_request_data_->ServiceWorkerRaceNetworkRequestToken());
request.SetFetchLaterAPI(IsDeferred());
if (execution_context_->IsSharedWorkerGlobalScope() &&
DynamicTo<SharedWorkerGlobalScope>(*execution_context_)
->DoesRequireCrossSiteRequestForCookies()) {
request.SetSiteForCookies(net::SiteForCookies());
}
// "3. Append `Host`, ..."
// FIXME: Implement this when the spec is fixed.
// "4.If |HTTPRequest|'s force Origin header flag is set, append `Origin`/
// |HTTPRequest|'s origin, serialized and utf-8 encoded, to |HTTPRequest|'s
// header list."
// We set Origin header in updateRequestForAccessControl() called from
// ThreadableLoader::makeCrossOriginAccessRequest
// "5. Let |credentials flag| be set if either |HTTPRequest|'s credentials
// mode is |include|, or |HTTPRequest|'s credentials mode is |same-origin|
// and the |CORS flag| is unset, and unset otherwise."
ResourceLoaderOptions resource_loader_options(world_);
resource_loader_options.initiator_info.name =
fetch_initiator_type_names::kFetch;
resource_loader_options.data_buffering_policy = kDoNotBufferData;
if (fetch_request_data_->URLLoaderFactory()) {
mojo::PendingRemote<network::mojom::blink::URLLoaderFactory> factory_clone;
fetch_request_data_->URLLoaderFactory()->Clone(
factory_clone.InitWithNewPipeAndPassReceiver());
resource_loader_options.url_loader_factory =
base::MakeRefCounted<base::RefCountedData<
mojo::PendingRemote<network::mojom::blink::URLLoaderFactory>>>(
std::move(factory_clone));
}
if (fetch_request_data_->Keepalive() && !request.IsFetchLaterAPI()) {
FetchUtils::LogFetchKeepAliveRequestMetric(
request.GetRequestContext(),
FetchUtils::FetchKeepAliveRequestState::kTotal);
}
CreateLoader(std::move(request), resource_loader_options);
}
// performDataFetch() is almost the same as performHTTPFetch(), except for:
// - We set AllowCrossOriginRequests to allow requests to data: URLs in
// 'same-origin' mode.
// - We reject non-GET method.
void FetchLoaderBase::PerformDataFetch() {
DCHECK(fetch_request_data_->Url().ProtocolIsData());
ResourceRequest request(fetch_request_data_->Url());
request.SetRequestorOrigin(fetch_request_data_->Origin());
request.SetRequestContext(mojom::blink::RequestContextType::FETCH);
request.SetRequestDestination(fetch_request_data_->Destination());
request.SetFetchLikeAPI(true);
request.SetUseStreamOnResponse(true);
request.SetHttpMethod(fetch_request_data_->Method());
request.SetCredentialsMode(network::mojom::CredentialsMode::kOmit);
request.SetRedirectMode(RedirectMode::kError);
request.SetFetchPriorityHint(fetch_request_data_->FetchPriorityHint());
request.SetPriority(fetch_request_data_->Priority());
// We intentionally skip 'setExternalRequestStateFromRequestorAddressSpace',
// as 'data:' can never be external.
ResourceLoaderOptions resource_loader_options(world_);
resource_loader_options.data_buffering_policy = kDoNotBufferData;
CreateLoader(std::move(request), resource_loader_options);
}
void FetchManager::Loader::CreateLoader(
ResourceRequest request,
const ResourceLoaderOptions& resource_loader_options) {
threadable_loader_ = MakeGarbageCollected<ThreadableLoader>(
*GetExecutionContext(), this, resource_loader_options);
threadable_loader_->Start(std::move(request));
}
bool FetchLoaderBase::AddConsoleMessage(
const String& message,
std::optional<base::UnguessableToken> issue_id) {
if (execution_context_->IsContextDestroyed())
return false;
bool issue_only =
base::FeatureList::IsEnabled(blink::features::kCORSErrorsIssueOnly) &&
issue_id;
if (!message.empty() && !issue_only) {
// CORS issues are reported via network service instrumentation, with the
// exception of early errors reported in FileIssueAndPerformNetworkError.
auto* console_message = MakeGarbageCollected<ConsoleMessage>(
mojom::blink::ConsoleMessageSource::kJavaScript,
mojom::blink::ConsoleMessageLevel::kError, message);
if (issue_id) {
console_message->SetCategory(mojom::blink::ConsoleMessageCategory::Cors);
}
execution_context_->AddConsoleMessage(console_message);
}
return true;
}
void FetchManager::Loader::Failed(
const String& message,
DOMException* dom_exception,
std::optional<String> devtools_request_id,
std::optional<base::UnguessableToken> issue_id) {
if (failed_ || finished_) {
return;
}
failed_ = true;
if (!AddConsoleMessage(message, issue_id)) {
return;
}
if (resolver_) {
ScriptState::Scope scope(GetScriptState());
if (dom_exception) {
resolver_->Reject(dom_exception);
} else {
v8::Local<v8::Value> value =
exception_.Get(GetScriptState()->GetIsolate());
exception_.Reset();
ThreadDebugger* debugger =
ThreadDebugger::From(GetScriptState()->GetIsolate());
if (devtools_request_id) {
debugger->GetV8Inspector()->associateExceptionData(
GetScriptState()->GetContext(), value,
V8AtomicString(GetScriptState()->GetIsolate(), "requestId"),
V8String(GetScriptState()->GetIsolate(), *devtools_request_id));
}
if (issue_id) {
debugger->GetV8Inspector()->associateExceptionData(
GetScriptState()->GetContext(), value,
V8AtomicString(GetScriptState()->GetIsolate(), "issueId"),
V8String(GetScriptState()->GetIsolate(),
IdentifiersFactory::IdFromToken(*issue_id)));
}
resolver_->Reject(value);
LogIfKeepalive("Failed");
}
}
NotifyFinished();
}
void FetchManager::Loader::NotifyFinished() {
if (fetch_manager_)
fetch_manager_->OnLoaderFinished(this);
}
bool FetchManager::Loader::IsDeferred() const {
return false;
}
void FetchManager::Loader::LogIfKeepalive(
std::string_view request_state) const {
return;
CHECK(request_state == "Succeeded" || request_state == "Failed");
if (!GetFetchRequestData()->Keepalive()) {
return;
}
base::TimeDelta duration = base::TimeTicks::Now() - request_started_time_;
base::UmaHistogramMediumTimes("FetchKeepAlive.RequestDuration", duration);
base::UmaHistogramMediumTimes(
base::StrCat({"FetchKeepAlive.RequestDuration.", request_state}),
duration);
}
// A subtype of FetchLoader to handle the deferred fetching algorithm [1].
//
// This loader and FetchManager::Loader are similar that they both runs the
// fetching algorithm provided by the base class. However, this loader does not
// go down ThreadableLoader and ResourceFetcher. Rather, it creates requests via
// a similar mojo FetchLaterLoaderFactory. Other differences include:
// - `IsDeferred()` is true, which helps the base generate different requests.
// - Expect no response after `Start()` is called.
// - Support activateAfter from [2] to allow sending at specified time.
// - Support FetchLaterResult from [2].
//
// Underlying, this loader intends to create a "deferred" fetch request,
// i.e. `ResourceRequest.is_fetch_later_api` is true, when `Start()` is called.
// The request will not be sent by network service (handled via browser)
// immediately until ExecutionContext of the FetchLaterManager is destroyed.
//
// Note that this loader does not use the "defer" mechanism as described in
// `ResourcFetcher::RequestResource()` or `ResourceFetcher::StartLoad()`, as
// the latter method can only be called when ResourcFetcher is not detached.
// Plus, the browser companion must be notified when the context is still alive.
//
// [1]:
// https://whatpr.org/fetch/1647/9ca4bda...9994c1d.html#request-a-deferred-fetch
// [2]:
// https://whatpr.org/fetch/1647/9ca4bda...9994c1d.html#dom-global-fetch-later
class FetchLaterManager::DeferredLoader final
: public GarbageCollected<FetchLaterManager::DeferredLoader>,
public FetchLoaderBase {
public:
DeferredLoader(ExecutionContext* ec,
FetchLaterManager* fetch_later_manager,
FetchRequestData* fetch_request_data,
ScriptState* script_state,
AbortSignal* signal,
const std::optional<base::TimeDelta>& activate_after)
: FetchLoaderBase(ec, fetch_request_data, script_state, signal),
fetch_later_manager_(fetch_later_manager),
fetch_later_result_(MakeGarbageCollected<FetchLaterResult>()),
activate_after_(activate_after),
timer_(ec->GetTaskRunner(FetchLaterManager::kTaskType),
this,
&DeferredLoader::TimerFired),
loader_(ec) {
base::UmaHistogramBoolean("FetchLater.Renderer.Total", true);
// `timer_` is started in `CreateLoader()` so that it won't end before a
// request is created.
}
FetchLaterResult* fetch_later_result() { return fetch_later_result_.Get(); }
// FetchLoaderBase overrides:
void Dispose() override {
// Prevent notification
fetch_later_manager_ = nullptr;
SetExecutionContext(nullptr);
timer_.Stop();
// The browser companion will take care of the actual request sending when
// discoverying the URL loading connections from here are gone.
}
void Process(const FetchLaterRendererMetricType& metric_type) {
// https://whatpr.org/fetch/1647/9ca4bda...9994c1d.html#process-a-deferred-fetch
// To process a deferred fetch deferredRecord:
// 1. If deferredRecord’s invoke state is not "deferred", then return.
if (invoke_state_ != InvokeState::DEFERRED) {
return;
}
// 2. Set deferredRecord’s invoke state to "activated".
SetInvokeState(InvokeState::ACTIVATED);
// 3. Fetch deferredRecord’s request.
if (loader_) {
LogFetchLaterMetric(metric_type);
loader_->SendNow();
}
}
// Returns this loader's request body length if the followings are all true:
// - this loader's request has a non-null body.
// - `url` is "same origin" with this loader's request URL.
uint64_t GetDeferredBytesForUrlOrigin(const KURL& url) const {
return GetFetchRequestData()->Buffer() &&
SecurityOrigin::AreSameOrigin(GetFetchRequestData()->Url(),
url)
? GetFetchRequestData()->BufferByteLength()
: 0;
}
void Trace(Visitor* visitor) const override {
visitor->Trace(fetch_later_manager_);
visitor->Trace(fetch_later_result_);
visitor->Trace(timer_);
visitor->Trace(loader_);
FetchLoaderBase::Trace(visitor);
}
// For testing only:
void RecreateTimerForTesting(
scoped_refptr<base::SingleThreadTaskRunner> task_runner,
const base::TickClock* tick_clock) {
timer_.Stop();
timer_.SetTaskRunnerForTesting(std::move(task_runner), tick_clock);
if (activate_after_.has_value()) {
timer_.StartOneShot(*activate_after_, FROM_HERE);
}
}
private:
enum class InvokeState {
DEFERRED,
ABORTED,
ACTIVATED
};
void SetInvokeState(InvokeState state) {
switch (state) {
case InvokeState::DEFERRED:
UseCounter::Count(GetExecutionContext(),
WebFeature::kFetchLaterInvokeStateDeferred);
break;
case InvokeState::ABORTED:
UseCounter::Count(GetExecutionContext(),
WebFeature::kFetchLaterInvokeStateAborted);
break;
case InvokeState::ACTIVATED:
UseCounter::Count(GetExecutionContext(),
WebFeature::kFetchLaterInvokeStateActivated);
break;
default:
NOTREACHED_NORETURN();
};
invoke_state_ = state;
fetch_later_result_->SetActivated(state == InvokeState::ACTIVATED);
}
// FetchLoaderBase overrides:
bool IsDeferred() const override { return true; }
void Abort() override {
// https://whatpr.org/fetch/1647/9ca4bda...9994c1d.html#dom-global-fetch-later
// 10. Add the following abort steps to requestObject’s signal:
// 10-1. Set deferredRecord’s invoke state to "aborted".
SetInvokeState(InvokeState::ABORTED);
// 10-2. Remove deferredRecord from request’s client’s fetch group’s
// deferred fetch records.
if (loader_) {
LogFetchLaterMetric(FetchLaterRendererMetricType::kAbortedByUser);
loader_->Cancel();
}
NotifyFinished();
}
// Triggered after `Start()`.
void CreateLoader(
ResourceRequest request,
const ResourceLoaderOptions& resource_loader_options) override {
auto* factory = fetch_later_manager_->GetFactory();
if (!factory) {
Failed(/*message=*/String(), /*dom_exception=*/nullptr);
return;
}
std::unique_ptr<network::ResourceRequest> network_request =
fetch_later_manager_->PrepareNetworkRequest(std::move(request),
resource_loader_options);
if (!network_request) {
Failed(/*message=*/String(), /*dom_exception=*/nullptr);
return;
}
// Don't do mime sniffing for fetch (crbug.com/2016)
uint32_t url_loader_options = network::mojom::blink::kURLLoadOptionNone;
// Computes a unique request_id for this renderer process.
int request_id = GenerateRequestId();
factory->CreateFetchLaterLoader(
loader_.BindNewEndpointAndPassReceiver(
GetExecutionContext()->GetTaskRunner(FetchLaterManager::kTaskType)),
request_id, url_loader_options, *network_request,
net::MutableNetworkTrafficAnnotationTag(
kFetchLaterTrafficAnnotationTag));
CHECK(loader_.is_bound());
loader_.set_disconnect_handler(WTF::BindOnce(
&DeferredLoader::NotifyFinished, WrapWeakPersistent(this)));
// https://whatpr.org/fetch/1647/9ca4bda...9994c1d.html#request-a-deferred-fetch
// Continued with "request a deferred fetch"
// 13. If `activate_after_` is not null, then run the following steps in
// parallel:
if (activate_after_.has_value()) {
// 13-1. The user agent should wait until `activate_after_`
// milliseconds have passed,
// Implementation followed by `TimerFired()`.
timer_.StartOneShot(*activate_after_, FROM_HERE);
}
}
void Failed(
const String& message,
DOMException* dom_exception,
std::optional<String> devtools_request_id = std::nullopt,
std::optional<base::UnguessableToken> issue_id = std::nullopt) override {
AddConsoleMessage(message, issue_id);
NotifyFinished();
}
// Notifies the owner to remove `this` from its container, after which
// `Dispose()` will also be called.
void NotifyFinished() {
if (fetch_later_manager_) {
fetch_later_manager_->OnDeferredLoaderFinished(this);
}
}
// Triggered by `timer_`.
void TimerFired(TimerBase*) {
// https://whatpr.org/fetch/1647/9ca4bda...9994c1d.html#request-a-deferred-fetch
// Continued with "request a deferred fetch":
// 13-3. Process a deferred fetch given deferredRecord.
Process(FetchLaterRendererMetricType::kActivatedByTimeout);
NotifyFinished();
}
// A deferred fetch record's "invoke state" field.
InvokeState invoke_state_ = InvokeState::DEFERRED;
// Owns this instance.
Member<FetchLaterManager> fetch_later_manager_;
// Retains strong reference to the returned V8 object of a FetchLater API call
// that creates this loader.
//
// The object itself may be held by a script, and may easily outlive `this` if
// the script keeps holding the object after the FetchLater request completes.
//
// This field should be updated whenever `invoke_state_` changes.
Member<FetchLaterResult> fetch_later_result_;
// The "activateAfter" to request a deferred fetch.
// https://whatpr.org/fetch/1647/9ca4bda...7bff4de.html#request-a-deferred-fetch
const std::optional<base::TimeDelta> activate_after_;
// A timer to handle `activate_after_`.
HeapTaskRunnerTimer<DeferredLoader> timer_;
// Connects to FetchLaterLoader in browser.
HeapMojoAssociatedRemote<mojom::blink::FetchLaterLoader> loader_;
};
FetchManager::FetchManager(ExecutionContext* execution_context)
: ExecutionContextLifecycleObserver(execution_context) {}
ScriptPromise<Response> FetchManager::Fetch(ScriptState* script_state,
FetchRequestData* request,
AbortSignal* signal,
ExceptionState& exception_state) {
DCHECK(signal);
if (signal->aborted()) {
exception_state.RethrowV8Exception(signal->reason(script_state).V8Value());
return ScriptPromise<Response>();
}
request->SetDestination(network::mojom::RequestDestination::kEmpty);
auto* resolver = MakeGarbageCollected<ScriptPromiseResolver<Response>>(
script_state, exception_state.GetContext());
auto promise = resolver->Promise();
auto* loader = MakeGarbageCollected<Loader>(
GetExecutionContext(), this, resolver, request, script_state, signal);
loaders_.insert(loader);
// TODO(ricea): Reject the Response body with AbortError, not TypeError.
loader->Start(exception_state);
return promise;
}
FetchLaterResult* FetchLaterManager::FetchLater(
ScriptState* script_state,
FetchRequestData* request,
AbortSignal* signal,
std::optional<DOMHighResTimeStamp> activate_after_ms,
ExceptionState& exception_state) {
// https://whatpr.org/fetch/1647/9ca4bda...9994c1d.html#dom-global-fetch-later
// Continuing the fetchLater(input, init) method steps:
CHECK(signal);
// 3. If request’s signal is aborted, then throw signal’s abort reason.
if (signal->aborted()) {
exception_state.ThrowDOMException(DOMExceptionCode::kAbortError,
"The user aborted a fetchLater request.");
return nullptr;
}
std::optional<base::TimeDelta> activate_after = std::nullopt;
if (activate_after_ms.has_value()) {
activate_after = base::Milliseconds(*activate_after_ms);
// 8. If `activate_after` is less than 0 then throw a RangeError.
if (activate_after->is_negative()) {
exception_state.ThrowRangeError(
"fetchLater's activateAfter cannot be negative.");
return nullptr;
}
}
// 12. Let deferredRecord be the result of calling request a deferred fetch
// given `request` and `activate_after`. This may throw an exception.
//
// "request a deferred fetch"
// https://whatpr.org/fetch/1647/9ca4bda...9994c1d.html#request-a-deferred-fetch
// 2. If request’s URL’s scheme is not an HTTPS scheme, then throw a
// TypeError.
if (!request->Url().ProtocolIs(WTF::g_https_atom)) {
exception_state.ThrowTypeError("fetchLater is only supported over HTTPS.");
return nullptr;
}
// 3. If request’s URL is not a potentially trustworthy url, then throw a
// "SecurityError" DOMException.
if (!network::IsUrlPotentiallyTrustworthy(GURL(request->Url()))) {
exception_state.ThrowSecurityError(
"fetchLater was passed an insecure URL.");
return nullptr;
}
// 4. Set request’s service-workers mode to "none".
// Done in `PerformHTTPFetch()`.
// 5. If request’s body is not null and request’s body’s source is null, then
// throw a TypeError.
// This disallows sending deferred fetches with a live ReadableStream.
// Equivalent to Step 7 below, as implementation does not set
// BufferByteLength() for ReadableStream.
uint64_t bytes_for_origin = 0;
// 7. If request’s body is not null then:
if (request->Buffer()) {
// 7-1. If request’s body’s length is null, then throw a TypeError.
if (request->BufferByteLength() == 0) {
UseCounter::Count(GetExecutionContext(),
WebFeature::kFetchLaterErrorUnknownBodyLength);
exception_state.ThrowTypeError(
"fetchLater doesn't support body with unknown length.");
return nullptr;
}
// 7-2. Set `bytes_for_origin` to request’s body’s length.
bytes_for_origin = request->BufferByteLength();
}
// Run Step 9 below for potential early termination. It also caps
// `bytes_per_origin`.
if (bytes_for_origin > kMaxScheduledDeferredBytesPerOrigin) {
UseCounter::Count(GetExecutionContext(),
WebFeature::kFetchLaterErrorQuotaExceeded);
exception_state.ThrowDOMException(
DOMExceptionCode::kQuotaExceededError,
"fetchLater exceeds its quota for the origin.");
return nullptr;
}
// 8. For each deferredRecord in request’s client’s fetch group’s deferred
// fetch records: if deferredRecord’s request’s body is not null and
// deferredRecord’s request’s URL’s origin is same origin with request’s
// URL’s origin, then increment `bytes_for_origin` by deferredRecord’s
// request’s body’s length.
for (const auto& deferred_loader : deferred_loaders_) {
// `bytes_for_orign` is capped below the max (64 kilobytes), and the value
// returned by every deferred_loader has run through the same cap. Hence,
// the sum here is guaranteed to be <= 128 kilobytes.
bytes_for_origin +=
deferred_loader->GetDeferredBytesForUrlOrigin(request->Url());
// 9. If `bytes_for_origin` is greater than 64 kilobytes, then throw a
// QuotaExceededError.
if (bytes_for_origin > kMaxScheduledDeferredBytesPerOrigin) {
UseCounter::Count(GetExecutionContext(),
WebFeature::kFetchLaterErrorQuotaExceeded);
exception_state.ThrowDOMException(
DOMExceptionCode::kQuotaExceededError,
"fetchLater exceeds its quota for the origin.");
return nullptr;
}
}
request->SetDestination(network::mojom::RequestDestination::kEmpty);
// 10. Set request’s keepalive to true.
request->SetKeepalive(true);
// 11. Let deferredRecord be a new deferred fetch record whose request is
// `request`.
auto* deferred_loader = MakeGarbageCollected<DeferredLoader>(
GetExecutionContext(), this, request, script_state, signal,
activate_after);
// 12. Append deferredRecord to request’s client’s fetch group’s deferred
// fetch records.
deferred_loaders_.insert(deferred_loader);
deferred_loader->Start(exception_state);
return deferred_loader->fetch_later_result();
}
void FetchManager::ContextDestroyed() {
// https://whatpr.org/fetch/1647/9ca4bda...7bff4de.html#concept-defer=fetch-record
// When a fetch group fetchGroup is terminated:
// 1. For each fetch record of fetchGroup's fetch records, if record's
// controller is non-null and record’s done flag is unset and keepalive is
// false, terminate the fetch record’s controller .
for (auto& loader : loaders_) {
loader->Dispose();
}
}
void FetchManager::OnLoaderFinished(Loader* loader) {
loaders_.erase(loader);
loader->Dispose();
}
void FetchManager::Trace(Visitor* visitor) const {
visitor->Trace(loaders_);
ExecutionContextLifecycleObserver::Trace(visitor);
}
FetchLaterManager::FetchLaterManager(ExecutionContext* ec)
: ExecutionContextLifecycleObserver(ec),
permission_observer_receiver_(this, ec) {
// TODO(crbug.com/1356128): FetchLater API is only supported in Document.
// Supporting it in workers is blocked by keepalive in browser migration.
CHECK(ec->IsWindow());
if (IsFetchLaterUseBackgroundSyncPermissionEnabled()) {
auto* permission_service =
DomWindow()->document()->GetPermissionService(ec);
CHECK(permission_service);
mojo::PendingRemote<mojom::blink::PermissionObserver> observer;
permission_observer_receiver_.Bind(
observer.InitWithNewPipeAndPassReceiver(),
// Same as `permission_service`'s task type.
ec->GetTaskRunner(TaskType::kPermission));
CHECK(permission_observer_receiver_.is_bound());
// Registers an observer for BackgroundSync permission.
// Cannot use `HasPermission()` as it's asynchronous. At the time the
// permission status is needed, e.g. on entering BackForwardCache, it may
// not have enough time to wait for response.
auto descriptor = mojom::blink::PermissionDescriptor::New();
descriptor->name = mojom::blink::PermissionName::BACKGROUND_SYNC;
permission_service->AddPermissionObserver(std::move(descriptor),
background_sync_permission_,
std::move(observer));
}
}
blink::ChildURLLoaderFactoryBundle* FetchLaterManager::GetFactory() {
// Do nothing if context is detached.
if (!DomWindow()) {
return nullptr;
}
return DomWindow()->GetFrame()->Client()->GetLoaderFactoryBundle();
}
void FetchLaterManager::ContextDestroyed() {
// https://whatpr.org/fetch/1647/9ca4bda...7bff4de.html#concept-defer=fetch-record
// When a fetch group fetchGroup is terminated:
// 2. process deferred fetches for fetchGroup.
// https://whatpr.org/fetch/1647/9ca4bda...9994c1d.html#process-deferred-fetches
// To process deferred fetches given a fetch group fetchGroup:
for (auto& deferred_loader : deferred_loaders_) {
// 3. For each deferred fetch record deferredRecord, process a deferred
// fetch given deferredRecord.
deferred_loader->Process(FetchLaterRendererMetricType::kContextDestroyed);
deferred_loader->Dispose();
}
// Unlike regular Fetch loaders, FetchLater loaders should be cleared
// immediately when the context is gone, as there is no work left here.
deferred_loaders_.clear();
}
void FetchLaterManager::ContextEnteredBackForwardCache() {
// TODO(crbug.com/1465781): Replace with spec once it's finalized.
// https://github.com/WICG/pending-beacon/issues/3#issuecomment-1286397825
// Sending any requests "after" the context goes into BackForwardCache
// requires BackgroundSync permission. If not granted, we should force sending
// all of them now instead of waiting until `ContextDestroyed()`.
if (IsFetchLaterSendOnEnterBackForwardCacheEnabled() ||
(IsFetchLaterUseBackgroundSyncPermissionEnabled() &&
!IsBackgroundSyncGranted())) {
for (auto& deferred_loader : deferred_loaders_) {
deferred_loader->Process(
FetchLaterRendererMetricType::kActivatedOnEnteredBackForwardCache);
deferred_loader->Dispose();
}
deferred_loaders_.clear();
}
}
void FetchLaterManager::OnDeferredLoaderFinished(
DeferredLoader* deferred_loader) {
deferred_loaders_.erase(deferred_loader);
deferred_loader->Dispose();
}
bool FetchLaterManager::IsBackgroundSyncGranted() const {
return background_sync_permission_ == mojom::blink::PermissionStatus::GRANTED;
}
void FetchLaterManager::OnPermissionStatusChange(
mojom::blink::PermissionStatus status) {
background_sync_permission_ = status;
}
size_t FetchLaterManager::NumLoadersForTesting() const {
return deferred_loaders_.size();
}
void FetchLaterManager::RecreateTimerForTesting(
scoped_refptr<base::SingleThreadTaskRunner> task_runner,
const base::TickClock* tick_clock) {
for (auto& deferred_loader : deferred_loaders_) {
deferred_loader->RecreateTimerForTesting(task_runner, tick_clock);
}
}
// static
ResourceLoadPriority FetchLaterManager::ComputeLoadPriorityForTesting(
const FetchParameters& params) {
return ComputeFetchLaterLoadPriority(params);
}
std::unique_ptr<network::ResourceRequest>
FetchLaterManager::PrepareNetworkRequest(
ResourceRequest request,
const ResourceLoaderOptions& options) const {
if (!GetExecutionContext()) {
// No requests if the context is destroyed.
return nullptr;
}
CHECK(DomWindow());
ResourceFetcher* fetcher = DomWindow()->Fetcher();
CHECK(fetcher);
FetchParameters params(std::move(request), options);
WebScopedVirtualTimePauser unused_virtual_time_pauser;
params.OverrideContentType(kFetchLaterContentType);
if (PrepareResourceRequest(
kFetchLaterResourceType,
fetcher->GetProperties().GetFetchClientSettingsObject(), params,
fetcher->Context(), unused_virtual_time_pauser,
WTF::BindOnce(&ComputeFetchLaterLoadPriority)) != std::nullopt) {
return nullptr;
}
// From `ResourceFetcher::StartLoad()`:
ScriptForbiddenScope script_forbidden_scope;
auto network_resource_request = std::make_unique<network::ResourceRequest>();
PopulateResourceRequest(
params.GetResourceRequest(),
std::move(params.MutableResourceRequest().MutableBody()),
network_resource_request.get());
return network_resource_request;
}
void FetchLaterManager::Trace(Visitor* visitor) const {
visitor->Trace(deferred_loaders_);
visitor->Trace(permission_observer_receiver_);
ExecutionContextLifecycleObserver::Trace(visitor);
}
} // namespace blink