Implement fetchLater (#39547)

Allows fetches to be deferred, only in a secure context. It does not yet
implement quota computation, since we don't have a concept of document
quota yet.

Also update the `fetch/api/idlharness` test to run in a secure context,
since this API is only available there.

Positive Mozilla position:
https://github.com/mozilla/standards-positions/issues/703
Positive WebKit position:
https://github.com/WebKit/standards-positions/issues/85

Closes whatwg/fetch#1858

Signed-off-by: Tim van der Lippe <tvanderlippe@gmail.com>
This commit is contained in:
Tim van der Lippe 2025-10-02 09:51:19 +02:00 committed by GitHub
parent 19c498af16
commit 680a780552
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 403 additions and 228 deletions

View file

@ -346,11 +346,15 @@ impl Callback for TransmitBodyPromiseRejectionHandler {
} }
} }
/// The result of <https://fetch.spec.whatwg.org/#concept-bodyinit-extract> /// <https://fetch.spec.whatwg.org/#body-with-type>
pub(crate) struct ExtractedBody { pub(crate) struct ExtractedBody {
/// <https://fetch.spec.whatwg.org/#concept-body-stream>
pub(crate) stream: DomRoot<ReadableStream>, pub(crate) stream: DomRoot<ReadableStream>,
/// <https://fetch.spec.whatwg.org/#concept-body-source>
pub(crate) source: BodySource, pub(crate) source: BodySource,
/// <https://fetch.spec.whatwg.org/#concept-body-total-bytes>
pub(crate) total_bytes: Option<usize>, pub(crate) total_bytes: Option<usize>,
/// <https://fetch.spec.whatwg.org/#body-with-type-type>
pub(crate) content_type: Option<DOMString>, pub(crate) content_type: Option<DOMString>,
} }

View file

@ -26,7 +26,7 @@ use crate::dom::bindings::str::DOMString;
use crate::dom::eventtarget::EventTarget; use crate::dom::eventtarget::EventTarget;
use crate::dom::globalscope::GlobalScope; use crate::dom::globalscope::GlobalScope;
use crate::dom::readablestream::PipeTo; use crate::dom::readablestream::PipeTo;
use crate::fetch::FetchContext; use crate::fetch::{DeferredFetchRecord, FetchContext};
use crate::realms::InRealm; use crate::realms::InRealm;
use crate::script_runtime::{CanGc, JSContext as SafeJSContext}; use crate::script_runtime::{CanGc, JSContext as SafeJSContext};
@ -49,6 +49,12 @@ pub(crate) enum AbortAlgorithm {
#[conditional_malloc_size_of] #[conditional_malloc_size_of]
Arc<Mutex<FetchContext>>, Arc<Mutex<FetchContext>>,
), ),
/// <https://fetch.spec.whatwg.org/#dom-window-fetchlater>
FetchLater(
#[no_trace]
#[conditional_malloc_size_of]
Arc<Mutex<DeferredFetchRecord>>,
),
} }
#[derive(Clone, JSTraceable, MallocSizeOf)] #[derive(Clone, JSTraceable, MallocSizeOf)]
@ -190,6 +196,9 @@ impl AbortSignal {
.unwrap() .unwrap()
.abort_fetch(reason.handle(), cx, can_gc); .abort_fetch(reason.handle(), cx, can_gc);
}, },
AbortAlgorithm::FetchLater(deferred_fetch_record) => {
deferred_fetch_record.lock().unwrap().abort();
},
AbortAlgorithm::DomEventListener(removable_listener) => { AbortAlgorithm::DomEventListener(removable_listener) => {
removable_listener removable_listener
.event_target .event_target

View file

@ -0,0 +1,59 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::sync::{Arc, Mutex};
use dom_struct::dom_struct;
use crate::dom::bindings::codegen::Bindings::FetchLaterResultBinding::FetchLaterResultMethods;
use crate::dom::bindings::reflector::{Reflector, reflect_dom_object};
use crate::dom::bindings::root::DomRoot;
use crate::dom::window::Window;
use crate::fetch::DeferredFetchRecord;
use crate::script_runtime::CanGc;
/// <https://fetch.spec.whatwg.org/#fetchlaterresult>
#[dom_struct]
pub(crate) struct FetchLaterResult {
reflector_: Reflector,
/// <https://fetch.spec.whatwg.org/#fetchlaterresult-activated-getter-steps>
#[conditional_malloc_size_of]
#[no_trace]
activated_getter_steps: Arc<Mutex<DeferredFetchRecord>>,
}
impl FetchLaterResult {
#[cfg_attr(crown, allow(crown::unrooted_must_root))]
fn new_inherited(activated_getter_steps: Arc<Mutex<DeferredFetchRecord>>) -> FetchLaterResult {
FetchLaterResult {
reflector_: Reflector::new(),
activated_getter_steps,
}
}
#[cfg_attr(crown, allow(crown::unrooted_must_root))]
pub(crate) fn new(
window: &Window,
activated_getter_steps: Arc<Mutex<DeferredFetchRecord>>,
can_gc: CanGc,
) -> DomRoot<FetchLaterResult> {
reflect_dom_object(
Box::new(FetchLaterResult::new_inherited(activated_getter_steps)),
window,
can_gc,
)
}
}
impl FetchLaterResultMethods<crate::DomTypeHolder> for FetchLaterResult {
/// <https://fetch.spec.whatwg.org/#dom-fetchlaterresult-activated>
fn Activated(&self) -> bool {
// The activated getter steps are to return the result of running thiss activated getter steps.
self.activated_getter_steps
.lock()
.expect("Activated getter not accessible")
.activated_getter_steps()
}
}

View file

@ -314,6 +314,7 @@ pub(crate) mod eventsource;
pub(crate) mod eventtarget; pub(crate) mod eventtarget;
pub(crate) mod extendableevent; pub(crate) mod extendableevent;
pub(crate) mod extendablemessageevent; pub(crate) mod extendablemessageevent;
pub(crate) mod fetchlaterresult;
pub(crate) mod file; pub(crate) mod file;
pub(crate) mod filelist; pub(crate) mod filelist;
pub(crate) mod filereader; pub(crate) mod filereader;

View file

@ -92,7 +92,7 @@ impl Request {
} }
// https://fetch.spec.whatwg.org/#dom-request // https://fetch.spec.whatwg.org/#dom-request
fn constructor( pub(crate) fn constructor(
global: &GlobalScope, global: &GlobalScope,
proto: Option<HandleObject>, proto: Option<HandleObject>,
can_gc: CanGc, can_gc: CanGc,

View file

@ -111,10 +111,11 @@ use crate::dom::bindings::codegen::Bindings::ImageBitmapBinding::{
}; };
use crate::dom::bindings::codegen::Bindings::MediaQueryListBinding::MediaQueryList_Binding::MediaQueryListMethods; use crate::dom::bindings::codegen::Bindings::MediaQueryListBinding::MediaQueryList_Binding::MediaQueryListMethods;
use crate::dom::bindings::codegen::Bindings::ReportingObserverBinding::Report; use crate::dom::bindings::codegen::Bindings::ReportingObserverBinding::Report;
use crate::dom::bindings::codegen::Bindings::RequestBinding::RequestInit; use crate::dom::bindings::codegen::Bindings::RequestBinding::{RequestInfo, RequestInit};
use crate::dom::bindings::codegen::Bindings::VoidFunctionBinding::VoidFunction; use crate::dom::bindings::codegen::Bindings::VoidFunctionBinding::VoidFunction;
use crate::dom::bindings::codegen::Bindings::WindowBinding::{ use crate::dom::bindings::codegen::Bindings::WindowBinding::{
self, FrameRequestCallback, ScrollBehavior, WindowMethods, WindowPostMessageOptions, self, DeferredRequestInit, FrameRequestCallback, ScrollBehavior, WindowMethods,
WindowPostMessageOptions,
}; };
use crate::dom::bindings::codegen::UnionTypes::{ use crate::dom::bindings::codegen::UnionTypes::{
RequestOrUSVString, TrustedScriptOrString, TrustedScriptOrStringOrFunction, RequestOrUSVString, TrustedScriptOrString, TrustedScriptOrStringOrFunction,
@ -140,6 +141,7 @@ use crate::dom::document::{AnimationFrameCallback, Document};
use crate::dom::element::Element; use crate::dom::element::Element;
use crate::dom::event::{Event, EventBubbles, EventCancelable}; use crate::dom::event::{Event, EventBubbles, EventCancelable};
use crate::dom::eventtarget::EventTarget; use crate::dom::eventtarget::EventTarget;
use crate::dom::fetchlaterresult::FetchLaterResult;
use crate::dom::globalscope::GlobalScope; use crate::dom::globalscope::GlobalScope;
use crate::dom::hashchangeevent::HashChangeEvent; use crate::dom::hashchangeevent::HashChangeEvent;
use crate::dom::history::History; use crate::dom::history::History;
@ -1821,6 +1823,16 @@ impl WindowMethods<crate::DomTypeHolder> for Window {
fetch::Fetch(self.upcast(), input, init, comp, can_gc) fetch::Fetch(self.upcast(), input, init, comp, can_gc)
} }
/// <https://fetch.spec.whatwg.org/#dom-window-fetchlater>
fn FetchLater(
&self,
input: RequestInfo,
init: RootedTraceableBox<DeferredRequestInit>,
can_gc: CanGc,
) -> Fallible<DomRoot<FetchLaterResult>> {
fetch::FetchLater(self, input, init, can_gc)
}
#[cfg(feature = "bluetooth")] #[cfg(feature = "bluetooth")]
fn TestRunner(&self) -> DomRoot<TestRunner> { fn TestRunner(&self) -> DomRoot<TestRunner> {
self.test_runner self.test_runner

View file

@ -2,13 +2,17 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::cell::Cell;
use std::rc::Rc; use std::rc::Rc;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::time::Duration;
use base::id::WebViewId; use base::id::WebViewId;
use ipc_channel::ipc; use ipc_channel::ipc;
use js::jsapi::{ExceptionStackBehavior, JS_IsExceptionPending};
use js::jsval::UndefinedValue; use js::jsval::UndefinedValue;
use js::rust::HandleValue; use js::rust::HandleValue;
use js::rust::wrappers::JS_SetPendingException;
use net_traits::policy_container::{PolicyContainer, RequestPolicyContainer}; use net_traits::policy_container::{PolicyContainer, RequestPolicyContainer};
use net_traits::request::{ use net_traits::request::{
CorsSettings, CredentialsMode, Destination, InsecureRequestsPolicy, Referrer, CorsSettings, CredentialsMode, Destination, InsecureRequestsPolicy, Referrer,
@ -20,6 +24,7 @@ use net_traits::{
ResourceTimingType, cancel_async_fetch, ResourceTimingType, cancel_async_fetch,
}; };
use servo_url::ServoUrl; use servo_url::ServoUrl;
use timers::TimerEventRequest;
use crate::body::BodyMixin; use crate::body::BodyMixin;
use crate::dom::abortsignal::AbortAlgorithm; use crate::dom::abortsignal::AbortAlgorithm;
@ -29,14 +34,17 @@ use crate::dom::bindings::codegen::Bindings::RequestBinding::{
}; };
use crate::dom::bindings::codegen::Bindings::ResponseBinding::Response_Binding::ResponseMethods; use crate::dom::bindings::codegen::Bindings::ResponseBinding::Response_Binding::ResponseMethods;
use crate::dom::bindings::codegen::Bindings::ResponseBinding::ResponseType as DOMResponseType; use crate::dom::bindings::codegen::Bindings::ResponseBinding::ResponseType as DOMResponseType;
use crate::dom::bindings::error::Error; use crate::dom::bindings::codegen::Bindings::WindowBinding::{DeferredRequestInit, WindowMethods};
use crate::dom::bindings::error::{Error, Fallible};
use crate::dom::bindings::import::module::SafeJSContext; use crate::dom::bindings::import::module::SafeJSContext;
use crate::dom::bindings::inheritance::Castable; use crate::dom::bindings::inheritance::Castable;
use crate::dom::bindings::num::Finite;
use crate::dom::bindings::refcounted::{Trusted, TrustedPromise}; use crate::dom::bindings::refcounted::{Trusted, TrustedPromise};
use crate::dom::bindings::reflector::DomGlobal; use crate::dom::bindings::reflector::DomGlobal;
use crate::dom::bindings::root::DomRoot; use crate::dom::bindings::root::DomRoot;
use crate::dom::bindings::trace::RootedTraceableBox; use crate::dom::bindings::trace::RootedTraceableBox;
use crate::dom::csp::{GlobalCspReporting, Violation}; use crate::dom::csp::{GlobalCspReporting, Violation};
use crate::dom::fetchlaterresult::FetchLaterResult;
use crate::dom::globalscope::GlobalScope; use crate::dom::globalscope::GlobalScope;
use crate::dom::headers::Guard; use crate::dom::headers::Guard;
use crate::dom::performanceresourcetiming::InitiatorType; use crate::dom::performanceresourcetiming::InitiatorType;
@ -44,6 +52,7 @@ use crate::dom::promise::Promise;
use crate::dom::request::Request; use crate::dom::request::Request;
use crate::dom::response::Response; use crate::dom::response::Response;
use crate::dom::serviceworkerglobalscope::ServiceWorkerGlobalScope; use crate::dom::serviceworkerglobalscope::ServiceWorkerGlobalScope;
use crate::dom::window::Window;
use crate::network_listener::{self, PreInvoke, ResourceTimingListener, submit_timing_data}; use crate::network_listener::{self, PreInvoke, ResourceTimingListener, submit_timing_data};
use crate::realms::{InRealm, enter_realm}; use crate::realms::{InRealm, enter_realm};
use crate::script_runtime::CanGc; use crate::script_runtime::CanGc;
@ -266,6 +275,189 @@ pub(crate) fn Fetch(
promise promise
} }
/// <https://fetch.spec.whatwg.org/#queue-a-deferred-fetch>
fn queue_deferred_fetch(
request: NetTraitsRequest,
activate_after: Finite<f64>,
global: &GlobalScope,
) -> Arc<Mutex<DeferredFetchRecord>> {
let trusted_global = Trusted::new(global);
// Step 1. Populate request from client given request.
// TODO
// Step 2. Set requests service-workers mode to "none".
// TODO
// Step 3. Set requests keepalive to true.
// TODO
// Step 4. Let deferredRecord be a new deferred fetch record whose request is request, and whose notify invoked is onActivatedWithoutTermination.
let deferred_record = Arc::new(Mutex::new(DeferredFetchRecord {
request,
global: trusted_global.clone(),
invoke_state: Cell::new(DeferredFetchRecordInvokeState::Pending),
activated: Cell::new(false),
}));
// Step 5. Append deferredRecord to requests clients fetch groups deferred fetch records.
// TODO
// Step 6. If activateAfter is non-null, then run the following steps in parallel:
let deferred_record_clone = deferred_record.clone();
global.schedule_timer(TimerEventRequest {
callback: Box::new(move || {
// Step 6.2. Process deferredRecord.
deferred_record_clone.lock().unwrap().process();
// Last step of https://fetch.spec.whatwg.org/#process-a-deferred-fetch
//
// Step 4. Queue a global task on the deferred fetch task source with
// deferredRecords requests clients global object to run deferredRecords notify invoked.
let deferred_record_clone = deferred_record_clone.clone();
trusted_global
.root()
.task_manager()
.deferred_fetch_task_source()
.queue(task!(notify_deferred_record: move || {
deferred_record_clone.lock().unwrap().activate();
}));
}),
// Step 6.1. The user agent should wait until any of the following conditions is met:
duration: Duration::from_millis(*activate_after as u64),
});
// Step 7. Return deferredRecord.
deferred_record
}
/// <https://fetch.spec.whatwg.org/#dom-window-fetchlater>
#[allow(non_snake_case, unsafe_code)]
pub(crate) fn FetchLater(
window: &Window,
input: RequestInfo,
init: RootedTraceableBox<DeferredRequestInit>,
can_gc: CanGc,
) -> Fallible<DomRoot<FetchLaterResult>> {
let global_scope = window.upcast();
// Step 1. Let requestObject be the result of invoking the initial value
// of Request as constructor with input and init as arguments.
let request_object = Request::constructor(global_scope, None, can_gc, input, &init.parent)?;
// Step 2. If requestObjects signal is aborted, then throw signals abort reason.
let signal = request_object.Signal();
if signal.aborted() {
let cx = GlobalScope::get_cx();
rooted!(in(*cx) let mut abort_reason = UndefinedValue());
signal.Reason(cx, abort_reason.handle_mut());
unsafe {
assert!(!JS_IsExceptionPending(*cx));
JS_SetPendingException(*cx, abort_reason.handle(), ExceptionStackBehavior::Capture);
}
return Err(Error::JSFailed);
}
// Step 3. Let request be requestObjects request.
let request = request_object.get_request();
// Step 4. Let activateAfter be null.
let mut activate_after = Finite::wrap(0_f64);
// Step 5. If init is given and init["activateAfter"] exists, then set
// activateAfter to init["activateAfter"].
if let Some(init_activate_after) = init.activateAfter.as_ref() {
activate_after = *init_activate_after;
}
// Step 6. If activateAfter is less than 0, then throw a RangeError.
if *activate_after < 0.0 {
return Err(Error::Range("activateAfter must be at least 0".to_owned()));
}
// Step 7. If thiss relevant global objects associated document is not fully active, then throw a TypeError.
if !window.Document().is_fully_active() {
return Err(Error::Type("Document is not fully active".to_owned()));
}
let url = request.url();
// Step 8. If requests URLs scheme is not an HTTP(S) scheme, then throw a TypeError.
if !matches!(url.scheme(), "http" | "https") {
return Err(Error::Type("URL is not http(s)".to_owned()));
}
// Step 9. If requests URL is not a potentially trustworthy URL, then throw a SecurityError.
if !url.is_potentially_trustworthy() {
return Err(Error::Type("URL is not trustworthy".to_owned()));
}
// Step 10. If requests body is not null, and requests body length is null, then throw a TypeError.
if let Some(body) = request.body.as_ref() {
if body.len().is_none() {
return Err(Error::Type("Body is null".to_owned()));
}
}
// Step 11. If the available deferred-fetch quota given requests client and requests URLs
// origin is less than requests total request length, then throw a "QuotaExceededError" DOMException.
// TODO
// Step 12. Let activated be false.
// Step 13. Let deferredRecord be the result of calling queue a deferred fetch given request,
// activateAfter, and the following step: set activated to true.
let deferred_record = queue_deferred_fetch(request, activate_after, global_scope);
// Step 14. Add the following abort steps to requestObjects signal: Set deferredRecords invoke state to "aborted".
signal.add(&AbortAlgorithm::FetchLater(deferred_record.clone()));
// Step 15. Return a new FetchLaterResult whose activated getter steps are to return activated.
Ok(FetchLaterResult::new(window, deferred_record, can_gc))
}
/// <https://fetch.spec.whatwg.org/#deferred-fetch-record-invoke-state>
#[derive(Clone, Copy, MallocSizeOf, PartialEq)]
enum DeferredFetchRecordInvokeState {
Pending,
Sent,
Aborted,
}
/// <https://fetch.spec.whatwg.org/#deferred-fetch-record>
#[derive(MallocSizeOf)]
pub(crate) struct DeferredFetchRecord {
/// <https://fetch.spec.whatwg.org/#deferred-fetch-record-request>
request: NetTraitsRequest,
/// <https://fetch.spec.whatwg.org/#deferred-fetch-record-invoke-state>
invoke_state: Cell<DeferredFetchRecordInvokeState>,
global: Trusted<GlobalScope>,
activated: Cell<bool>,
}
impl DeferredFetchRecord {
/// Part of step 13 of <https://fetch.spec.whatwg.org/#dom-window-fetchlater>
fn activate(&self) {
// and the following step: set activated to true.
self.activated.set(true);
}
/// Part of step 14 of <https://fetch.spec.whatwg.org/#dom-window-fetchlater>
pub(crate) fn abort(&self) {
// Set deferredRecords invoke state to "aborted".
self.invoke_state
.set(DeferredFetchRecordInvokeState::Aborted);
}
/// Part of step 15 of <https://fetch.spec.whatwg.org/#dom-window-fetchlater>
pub(crate) fn activated_getter_steps(&self) -> bool {
// whose activated getter steps are to return activated.
self.activated.get()
}
/// <https://fetch.spec.whatwg.org/#process-a-deferred-fetch>
fn process(&self) {
// Step 1. If deferredRecords invoke state is not "pending", then return.
if self.invoke_state.get() != DeferredFetchRecordInvokeState::Pending {
return;
}
// Step 2. Set deferredRecords invoke state to "sent".
self.invoke_state.set(DeferredFetchRecordInvokeState::Sent);
// Step 3. Fetch deferredRecords request.
let url = self.request.url().clone();
let fetch_later_listener = Arc::new(Mutex::new(FetchLaterListener {
url,
resource_timing: ResourceFetchTiming::new(ResourceTimingType::Resource),
global: self.global.clone(),
}));
let global = self.global.root();
let _realm = enter_realm(&*global);
let mut request_init = request_init_from_request(self.request.clone());
request_init.policy_container =
RequestPolicyContainer::PolicyContainer(global.policy_container());
global.fetch(
request_init,
fetch_later_listener,
global.task_manager().networking_task_source().to_sendable(),
);
// Step 4 is handled by caller
}
}
#[derive(JSTraceable, MallocSizeOf)] #[derive(JSTraceable, MallocSizeOf)]
pub(crate) struct FetchContext { pub(crate) struct FetchContext {
#[ignore_malloc_size_of = "unclear ownership semantics"] #[ignore_malloc_size_of = "unclear ownership semantics"]
@ -453,6 +645,74 @@ impl ResourceTimingListener for FetchContext {
} }
} }
struct FetchLaterListener {
/// URL of this request.
url: ServoUrl,
/// Timing data for this resource.
resource_timing: ResourceFetchTiming,
/// The global object fetching the report uri violation
global: Trusted<GlobalScope>,
}
impl FetchResponseListener for FetchLaterListener {
fn process_request_body(&mut self, _: RequestId) {}
fn process_request_eof(&mut self, _: RequestId) {}
fn process_response(
&mut self,
_: RequestId,
fetch_metadata: Result<FetchMetadata, NetworkError>,
) {
_ = fetch_metadata;
}
fn process_response_chunk(&mut self, _: RequestId, chunk: Vec<u8>) {
_ = chunk;
}
fn process_response_eof(
&mut self,
_: RequestId,
response: Result<ResourceFetchTiming, NetworkError>,
) {
_ = response;
}
fn resource_timing_mut(&mut self) -> &mut ResourceFetchTiming {
&mut self.resource_timing
}
fn resource_timing(&self) -> &ResourceFetchTiming {
&self.resource_timing
}
fn submit_resource_timing(&mut self) {
network_listener::submit_timing(self, CanGc::note())
}
fn process_csp_violations(&mut self, _request_id: RequestId, violations: Vec<Violation>) {
let global = self.resource_timing_global();
global.report_csp_violations(violations, None, None);
}
}
impl ResourceTimingListener for FetchLaterListener {
fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) {
(InitiatorType::Fetch, self.url.clone())
}
fn resource_timing_global(&self) -> DomRoot<GlobalScope> {
self.global.root()
}
}
impl PreInvoke for FetchLaterListener {
fn should_invoke(&self) -> bool {
true
}
}
fn fill_headers_with_metadata(r: DomRoot<Response>, m: Metadata, can_gc: CanGc) { fn fill_headers_with_metadata(r: DomRoot<Response>, m: Metadata, can_gc: CanGc) {
r.set_headers(m.headers, can_gc); r.set_headers(m.headers, can_gc);
r.set_status(&m.status); r.set_status(&m.status);

View file

@ -137,6 +137,7 @@ impl TaskManager {
task_source_functions!(self, clipboard_task_source, Clipboard); task_source_functions!(self, clipboard_task_source, Clipboard);
task_source_functions!(self, crypto_task_source, Crypto); task_source_functions!(self, crypto_task_source, Crypto);
task_source_functions!(self, database_access_task_source, DatabaseAccess); task_source_functions!(self, database_access_task_source, DatabaseAccess);
task_source_functions!(self, deferred_fetch_task_source, DeferredFetch);
task_source_functions!(self, dom_manipulation_task_source, DOMManipulation); task_source_functions!(self, dom_manipulation_task_source, DOMManipulation);
task_source_functions!(self, file_reading_task_source, FileReading); task_source_functions!(self, file_reading_task_source, FileReading);
task_source_functions!(self, font_loading_task_source, FontLoading); task_source_functions!(self, font_loading_task_source, FontLoading);

View file

@ -29,6 +29,8 @@ pub(crate) enum TaskSourceName {
/// <https://w3c.github.io/webcrypto/#dfn-crypto-task-source-0> /// <https://w3c.github.io/webcrypto/#dfn-crypto-task-source-0>
Crypto, Crypto,
DatabaseAccess, DatabaseAccess,
/// <https://fetch.spec.whatwg.org/#deferred-fetch-task-source>
DeferredFetch,
DOMManipulation, DOMManipulation,
FileReading, FileReading,
/// <https://drafts.csswg.org/css-font-loading/#task-source> /// <https://drafts.csswg.org/css-font-loading/#task-source>
@ -62,6 +64,7 @@ impl From<TaskSourceName> for ScriptThreadEventCategory {
TaskSourceName::Clipboard => ScriptThreadEventCategory::ScriptEvent, TaskSourceName::Clipboard => ScriptThreadEventCategory::ScriptEvent,
TaskSourceName::Crypto => ScriptThreadEventCategory::ScriptEvent, TaskSourceName::Crypto => ScriptThreadEventCategory::ScriptEvent,
TaskSourceName::DatabaseAccess => ScriptThreadEventCategory::ScriptEvent, TaskSourceName::DatabaseAccess => ScriptThreadEventCategory::ScriptEvent,
TaskSourceName::DeferredFetch => ScriptThreadEventCategory::NetworkEvent,
TaskSourceName::DOMManipulation => ScriptThreadEventCategory::ScriptEvent, TaskSourceName::DOMManipulation => ScriptThreadEventCategory::ScriptEvent,
TaskSourceName::FileReading => ScriptThreadEventCategory::FileRead, TaskSourceName::FileReading => ScriptThreadEventCategory::FileRead,
TaskSourceName::FontLoading => ScriptThreadEventCategory::FontLoading, TaskSourceName::FontLoading => ScriptThreadEventCategory::FontLoading,

View file

@ -659,7 +659,7 @@ DOMInterfaces = {
}, },
'Window': { 'Window': {
'canGc': ['CreateImageBitmap', 'CreateImageBitmap_', 'CookieStore', 'Fetch', 'Open', 'SetInterval', 'SetTimeout', 'Stop', 'TrustedTypes', 'WebdriverCallback', 'WebdriverException'], 'canGc': ['CreateImageBitmap', 'CreateImageBitmap_', 'CookieStore', 'Fetch', 'FetchLater', 'Open', 'SetInterval', 'SetTimeout', 'Stop', 'TrustedTypes', 'WebdriverCallback', 'WebdriverException'],
'inRealms': ['Fetch', 'GetOpener', 'WebdriverCallback', 'WebdriverException'], 'inRealms': ['Fetch', 'GetOpener', 'WebdriverCallback', 'WebdriverException'],
'additionalTraits': ['crate::interfaces::WindowHelpers'], 'additionalTraits': ['crate::interfaces::WindowHelpers'],
}, },

View file

@ -0,0 +1,9 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
// https://fetch.spec.whatwg.org/#fetch-method
[Exposed=Window]
interface FetchLaterResult {
readonly attribute boolean activated;
};

View file

@ -189,3 +189,12 @@ partial interface Window {
dictionary WindowPostMessageOptions : StructuredSerializeOptions { dictionary WindowPostMessageOptions : StructuredSerializeOptions {
USVString targetOrigin = "/"; USVString targetOrigin = "/";
}; };
// https://fetch.spec.whatwg.org/#fetch-method
dictionary DeferredRequestInit : RequestInit {
DOMHighResTimeStamp activateAfter;
};
partial interface Window {
[NewObject, SecureContext, Throws] FetchLaterResult fetchLater(RequestInfo input, optional DeferredRequestInit init = {});
};

View file

@ -140,6 +140,10 @@ skip: true
skip: false skip: false
[fetch] [fetch]
skip: false skip: false
[fetch-later]
skip: false
[quota]
skip: true
[FileAPI] [FileAPI]
skip: false skip: false
[focus] [focus]

View file

@ -674692,10 +674692,10 @@
] ]
] ]
}, },
"idlharness.any.js": [ "idlharness.https.any.js": [
"7b3c694e16ac3ec2776398067cf6ddbef949c969", "7b3c694e16ac3ec2776398067cf6ddbef949c969",
[ [
"fetch/api/idlharness.any.html", "fetch/api/idlharness.https.any.html",
{ {
"script_metadata": [ "script_metadata": [
[ [
@ -674719,7 +674719,7 @@
} }
], ],
[ [
"fetch/api/idlharness.any.serviceworker.html", "fetch/api/idlharness.https.any.serviceworker.html",
{ {
"script_metadata": [ "script_metadata": [
[ [
@ -674743,7 +674743,7 @@
} }
], ],
[ [
"fetch/api/idlharness.any.sharedworker.html", "fetch/api/idlharness.https.any.sharedworker.html",
{ {
"script_metadata": [ "script_metadata": [
[ [
@ -674767,7 +674767,7 @@
} }
], ],
[ [
"fetch/api/idlharness.any.worker.html", "fetch/api/idlharness.https.any.worker.html",
{ {
"script_metadata": [ "script_metadata": [
[ [
@ -680157,7 +680157,7 @@
] ]
], ],
"basic.https.window.js": [ "basic.https.window.js": [
"f3ed42fe35a078942980ff44f66eee26a71f38cf", "afedbf5d4c4fb6eb069e650911014a10e0d43b4a",
[ [
"fetch/fetch-later/basic.https.window.html", "fetch/fetch-later/basic.https.window.html",
{} {}

View file

@ -1,4 +1,4 @@
[idlharness.any.worker.html] [idlharness.https.any.html]
[Request interface: attribute keepalive] [Request interface: attribute keepalive]
expected: FAIL expected: FAIL
@ -24,10 +24,10 @@
expected: FAIL expected: FAIL
[idlharness.any.sharedworker.html] [idlharness.https.any.serviceworker.html]
expected: ERROR expected: ERROR
[idlharness.any.html] [idlharness.https.any.worker.html]
[Request interface: attribute keepalive] [Request interface: attribute keepalive]
expected: FAIL expected: FAIL
@ -52,36 +52,6 @@
[Request interface: new Request('about:blank') must inherit property "duplex" with the proper type] [Request interface: new Request('about:blank') must inherit property "duplex" with the proper type]
expected: FAIL expected: FAIL
[FetchLaterResult interface: existence and properties of interface object]
expected: FAIL
[FetchLaterResult interface object length] [idlharness.https.any.sharedworker.html]
expected: FAIL
[FetchLaterResult interface object name]
expected: FAIL
[FetchLaterResult interface: existence and properties of interface prototype object]
expected: FAIL
[FetchLaterResult interface: existence and properties of interface prototype object's "constructor" property]
expected: FAIL
[FetchLaterResult interface: existence and properties of interface prototype object's @@unscopables property]
expected: FAIL
[FetchLaterResult interface: attribute activated]
expected: FAIL
[Window interface: operation fetchLater(RequestInfo, optional DeferredRequestInit)]
expected: FAIL
[Window interface: window must inherit property "fetchLater(RequestInfo, optional DeferredRequestInit)" with the proper type]
expected: FAIL
[Window interface: calling fetchLater(RequestInfo, optional DeferredRequestInit) on window with too few arguments must throw TypeError]
expected: FAIL
[idlharness.any.serviceworker.html]
expected: ERROR expected: ERROR

View file

@ -0,0 +1,3 @@
prefs: [
"dom_abort_controller_enabled:true",
]

View file

@ -1,6 +1,4 @@
[activate-after.https.window.html] [activate-after.https.window.html]
[fetchLater() sends out based on activateAfter.] expected: TIMEOUT
expected: FAIL
[fetchLater() sends out based on activateAfter, even if document is in BFCache.] [fetchLater() sends out based on activateAfter, even if document is in BFCache.]
expected: FAIL expected: TIMEOUT

View file

@ -1,69 +1,3 @@
[basic.https.window.html] [basic.https.window.html]
[fetchLater() cannot be called without request.]
expected: FAIL
[fetchLater() with same-origin (https) URL does not throw.]
expected: FAIL
[fetchLater() with http://localhost URL does not throw.]
expected: FAIL
[fetchLater() with https://localhost URL does not throw.]
expected: FAIL
[fetchLater() with http://127.0.0.1 URL does not throw.]
expected: FAIL
[fetchLater() with https://127.0.0.1 URL does not throw.]
expected: FAIL
[fetchLater() with http://[::1\] URL does not throw.] [fetchLater() with http://[::1\] URL does not throw.]
expected: FAIL expected: FAIL
[fetchLater() with https://[::1\] URL does not throw.]
expected: FAIL
[fetchLater() with https://example.com URL does not throw.]
expected: FAIL
[fetchLater() throws SecurityError on non-trustworthy http URL.]
expected: FAIL
[fetchLater() throws TypeError on file:// scheme.]
expected: FAIL
[fetchLater() throws TypeError on ftp:// scheme.]
expected: FAIL
[fetchLater() throws TypeError on ssh:// scheme.]
expected: FAIL
[fetchLater() throws TypeError on wss:// scheme.]
expected: FAIL
[fetchLater() throws TypeError on about: scheme.]
expected: FAIL
[fetchLater() throws TypeError on javascript: scheme.]
expected: FAIL
[fetchLater() throws TypeError on data: scheme.]
expected: FAIL
[fetchLater() throws TypeError on blob: scheme.]
expected: FAIL
[fetchLater() throws RangeError on negative activateAfter.]
expected: FAIL
[fetchLater()'s return tells the deferred request is not yet sent.]
expected: FAIL
[fetchLater() throws TypeError when mutating its returned state.]
expected: FAIL
[fetchLater() throws AbortError when its initial abort signal is aborted.]
expected: FAIL
[fetchLater() does not throw error when it is aborted before sending.]
expected: FAIL

View file

@ -1,3 +0,0 @@
[header-referrer-no-referrer-when-downgrade.https.html]
[Test referer header https://web-platform.test:8443]
expected: FAIL

View file

@ -1,3 +0,0 @@
[header-referrer-no-referrer.https.html]
[Test referer header ]
expected: FAIL

View file

@ -1,6 +0,0 @@
[header-referrer-origin-when-cross-origin.https.html]
[Test referer header https://web-platform.test:8443]
expected: FAIL
[Test referer header https://www1.web-platform.test:8443]
expected: FAIL

View file

@ -1,3 +0,0 @@
[header-referrer-origin.https.html]
[Test referer header https://www1.web-platform.test:8443]
expected: FAIL

View file

@ -1,6 +0,0 @@
[header-referrer-same-origin.https.html]
[Test referer header ]
expected: FAIL
[Test referer header https://www1.web-platform.test:8443]
expected: FAIL

View file

@ -1,3 +0,0 @@
[header-referrer-strict-origin-when-cross-origin.https.html]
[Test referer header https://www1.web-platform.test:8443]
expected: FAIL

View file

@ -1,3 +0,0 @@
[header-referrer-strict-origin.https.html]
[Test referer header https://web-platform.test:8443]
expected: FAIL

View file

@ -1,3 +0,0 @@
[header-referrer-unsafe-url.https.html]
[Test referer header https://web-platform.test:8443]
expected: FAIL

View file

@ -1,4 +0,0 @@
[iframe.https.window.html]
expected: ERROR
[A blank iframe can trigger fetchLater.]
expected: FAIL

View file

@ -1,36 +0,0 @@
[new-window.https.window.html]
[A blank window[target=''\][features=''\] can trigger fetchLater.]
expected: FAIL
[A same-origin window[target=''\][features=''\] can trigger fetchLater.]
expected: FAIL
[A cross-origin window[target=''\][features=''\] can trigger fetchLater.]
expected: FAIL
[A blank window[target=''\][features='popup'\] can trigger fetchLater.]
expected: FAIL
[A same-origin window[target=''\][features='popup'\] can trigger fetchLater.]
expected: FAIL
[A cross-origin window[target=''\][features='popup'\] can trigger fetchLater.]
expected: FAIL
[A blank window[target='_blank'\][features=''\] can trigger fetchLater.]
expected: FAIL
[A same-origin window[target='_blank'\][features=''\] can trigger fetchLater.]
expected: FAIL
[A cross-origin window[target='_blank'\][features=''\] can trigger fetchLater.]
expected: FAIL
[A blank window[target='_blank'\][features='popup'\] can trigger fetchLater.]
expected: FAIL
[A same-origin window[target='_blank'\][features='popup'\] can trigger fetchLater.]
expected: FAIL
[A cross-origin window[target='_blank'\][features='popup'\] can trigger fetchLater.]
expected: FAIL

View file

@ -1,12 +1,3 @@
[deferred-fetch-allowed-by-permissions-policy.https.window.html] [deferred-fetch-allowed-by-permissions-policy.https.window.html]
[Permissions policy header: "deferred-fetch=*" allows fetchLater() in the top-level document.]
expected: FAIL
[Permissions policy header: "deferred-fetch=*" allows fetchLater() in the same-origin iframe.]
expected: FAIL
[Permissions policy header: "deferred-fetch=*" allows fetchLater() in the cross-origin iframe.]
expected: FAIL
[Permissions policy header: "deferred-fetch=*" allow="deferred-fetch" allows fetchLater() in the cross-origin iframe.] [Permissions policy header: "deferred-fetch=*" allow="deferred-fetch" allows fetchLater() in the cross-origin iframe.]
expected: FAIL expected: FAIL

View file

@ -1,9 +0,0 @@
[deferred-fetch-default-permissions-policy.https.window.html]
[Default "deferred-fetch" permissions policy ["self"\] allows fetchLater() in the top-level document.]
expected: FAIL
[Default "deferred-fetch" permissions policy ["self"\] allows fetchLater() in the same-origin iframe.]
expected: FAIL
[Default "deferred-fetch-minimal" permissions policy ["*"\] allows fetchLater() in the cross-origin iframe.]
expected: FAIL

View file

@ -1,3 +0,0 @@
[csp-allowed.https.window.html]
[FetchLater allowed by CSP should succeed]
expected: FAIL

View file

@ -1,3 +0,0 @@
[csp-blocked.https.window.html]
[FetchLater blocked by CSP should reject]
expected: FAIL

View file

@ -1,15 +1,16 @@
[send-on-deactivate.https.window.html] [send-on-deactivate.https.window.html]
expected: TIMEOUT
[fetchLater() sends on page entering BFCache if BackgroundSync is off.] [fetchLater() sends on page entering BFCache if BackgroundSync is off.]
expected: FAIL expected: TIMEOUT
[Call fetchLater() when BFCached with activateAfter=0 sends immediately.] [Call fetchLater() when BFCached with activateAfter=0 sends immediately.]
expected: FAIL expected: TIMEOUT
[fetchLater() sends on navigating away a page w/o BFCache.] [fetchLater() sends on navigating away a page w/o BFCache.]
expected: FAIL expected: TIMEOUT
[fetchLater() does not send aborted request on navigating away a page w/o BFCache.] [fetchLater() does not send aborted request on navigating away a page w/o BFCache.]
expected: FAIL expected: TIMEOUT
[fetchLater() with activateAfter=1m sends on page entering BFCache if BackgroundSync is off.] [fetchLater() with activateAfter=1m sends on page entering BFCache if BackgroundSync is off.]
expected: FAIL expected: TIMEOUT

View file

@ -1,3 +0,0 @@
[not-send-after-abort.https.window.html]
[A discarded document does not send an already aborted fetchLater request.]
expected: FAIL

View file

@ -1,3 +0,0 @@
[send-multiple.https.window.html]
[A discarded document sends all its fetchLater requests.]
expected: FAIL

View file

@ -13775,7 +13775,7 @@
] ]
], ],
"interfaces.https.html": [ "interfaces.https.html": [
"1397b723a1cb001521ac1b2032c380f1e02cf1f0", "027e138f4af5e2a13df9bf8c78765dcf57f6b611",
[ [
null, null,
{} {}

View file

@ -92,6 +92,7 @@ test_interfaces([
"Event", "Event",
"EventSource", "EventSource",
"EventTarget", "EventTarget",
"FetchLaterResult",
"File", "File",
"FileList", "FileList",
"FileReader", "FileReader",

View file

@ -52,11 +52,8 @@ test(() => {
}, `fetchLater() with https://example.com URL does not throw.`); }, `fetchLater() with https://example.com URL does not throw.`);
test(() => { test(() => {
const httpUrl = 'http://example.com'; assert_throws_js(TypeError, () => fetchLater('http://example.com'));
assert_throws_dom( }, `fetchLater() throws TypeError on non-trustworthy http URL.`);
'SecurityError', () => fetchLater(httpUrl),
`should throw SecurityError for insecure http url ${httpUrl}`);
}, `fetchLater() throws SecurityError on non-trustworthy http URL.`);
test(() => { test(() => {
assert_throws_js(TypeError, () => fetchLater('file://tmp')); assert_throws_js(TypeError, () => fetchLater('file://tmp'));