diff --git a/fetch.bs b/fetch.bs index d398cf3b..74250557 100644 --- a/fetch.bs +++ b/fetch.bs @@ -42,6 +42,7 @@ urlPrefix:https://httpwg.org/specs/rfc9112.html#;type:dfn;spec:http1 url:status.line;text:reason-phrase url:https://w3c.github.io/resource-timing/#dfn-mark-resource-timing;text:mark resource timing;type:dfn;spec:resource-timing +url:https://w3c.github.io/webappsec-permissions-policy/#algo-define-inherited-policy-in-container;text:define an inherited policy for feature in container;type:dfn urlPrefix:https://w3c.github.io/hr-time/#;spec:hr-time type:dfn @@ -1818,7 +1819,8 @@ not always relevant and might require different behavior.
connect-src
navigator.sendBeacon()
, {{EventSource}},
HTML's <a ping="">
and <area ping="">
,
- fetch()
, {{XMLHttpRequest}}, {{WebSocket}}, Cache API
+ fetch()
, fetchLater()
, {{XMLHttpRequest}},
+ {{WebSocket}}, Cache API
object
"
object-src
@@ -2728,26 +2730,59 @@ functionality.
Each environment settings object has an associated fetch group. -
A fetch group holds an ordered list of -fetch records. +
A fetch group has an associated +fetch records, +a list of fetch records. -
A fetch record has an associated -request (a -request). +
A fetch group has an associated +deferred fetch records, +a list of deferred fetch records. -
A fetch record has an associated -controller (a -fetch controller or null). +
A fetch record is a [=struct=]. It has the following items: + +
A deferred fetch record is a struct used to maintain state needed +to invoke a fetch at a later time, e.g., when a {{Document}} object is unloaded or becomes +not fully active. It has the following items: + +
Each navigable container has an associated number +reserved deferred-fetch quota. Its possible values are +minimal quota, which is 8 kibibytes, +normal quota, which is 64 kibibytes, or 0. Unless +stated otherwise, it is 0.
When a fetch group is -terminated, for each associated -fetch record whose fetch record's -controller is non-null, and whose request's -done flag is unset or keepalive is false, -terminate the fetch record's -controller. +
When a fetch group fetchGroup is +terminated: + +
For each fetch record record of + fetchGroup's fetch records, if record's + controller is non-null and record's + request's done flag is unset and keepalive is + false, terminate record's + controller. + +
Process deferred fetches for fetchGroup. +
Deferred fetching allows callers to request that a fetch is invoked at the latest possible moment, +i.e., when a fetch group is terminated, or after a timeout. + +
The deferred fetch task source is a task source used to update the result of a
+deferred fetch. User agents must prioritize tasks in this task source before other task
+sources, specifically task sources that can result in running scripts such as the
+DOM manipulation task source, to reflect the most recent state of a
+fetchLater()
call before running any scripts that might depend on it.
+
+
To queue a deferred fetch given a request request, a +fetch group fetchGroup, a null or {{DOMHighResTimeStamp}} +activateAfter, and onActivatedWithoutTermination, which is an algorithm that +takes no arguments: + +
Populate request from client given request. + +
Set request's service-workers mode to "none
".
+
+
Set request's keepalive to true. + +
Let deferredRecord be a new deferred fetch record whose + request is request. + +
Append deferredRecord to fetchGroup's + deferred fetch records. + +
If activateAfter is non-null, then run the following steps in parallel:
+ +The user agent should wait until any of the following conditions is met: + +
At least activateAfter milliseconds have passed. + +
The user agent has a reason to believe that it is about to lose the opportunity to
+ execute scripts, e.g., when the browser is moved to the background, or when
+ request's client is a {{Document}} that had a
+ "hidden
" visibility state for a long period of time.
+
If the result of calling process a deferred fetch given deferredRecord + returns true, then queue a global task on the deferred fetch task source with + request's client's + global object to run + onActivatedWithoutTermination. +
Return deferredRecord. +
To compute the total request length of a request request: + +
Let totalRequestLength be the length of request's + URL, serialized with + [=URL serializer/exclude fragment=] set to true. + +
Increment totalRequestLength by the length of + request's referrer, serialized. + +
For each (name, value) of request's + header list, increment totalRequestLength by name's + length + value's length. + +
Increment totalRequestLength by request's body's + length. + +
Return totalRequestLength. +
To process deferred fetches given a fetch group fetchGroup: + +
For each deferred fetch record + deferredRecord of fetchGroup's + deferred fetch records, process a deferred fetch + deferredRecord. +
To process a deferred fetch deferredRecord: +
+The deferred-fetch quota is allocated to a top-level traversable (a "tab"), +amounting to 640 kibibytes. The top-level {{Document}} and its same-origin directly nested documents +can use this quota to queue deferred fetches, or delegate some of it to cross-origin nested +documents, using permissions policy. + +
By default, 128 kibibytes out of these 640 kibibytes are allocated to delegating the quota to +cross-origin nested documents, each reserving 8 kibibytes. + +
The top-level {{Document}}, and subsequently its nested documents, can control how much of their +quota is delegates to cross-origin child documents, using permissions policy. By default, +the "{{PermissionsPolicy/deferred-fetch-minimal}}" policy is enabled for any origin, while +"{{PermissionsPolicy/deferred-fetch}}" is enabled for the top-level document's origin only. +By relaxing the "{{PermissionsPolicy/deferred-fetch}}" policy for particular origins and nested +documents, the top-level document can allocate 64 kibibytes to those nested documents. Similarly, by +restricting the "{{PermissionsPolicy/deferred-fetch-minimal}}" policy for a particular origin or +nested document, the document can prevent the document from reserving the 8 kibibytes it would +receive by default. By disabling the "{{PermissionsPolicy/deferred-fetch-minimal}}" policy for the +top-level document itself, the entire 128 kibibytes delegated quota is collected back into the main +pool of 640 kibibytes. + +
Out of the allocated quota for a {{Document}}, only 64 kibibytes can be used concurrently for the +same reporting origin (the request's URL's origin). +This prevents a situation where particular 3rd party libraries would reserve quota +opportunistically, before they have data to send. + +
Any of the following calls to fetchLater()
would throw due to
+the request itself exceeding the 64 kibibytes quota allocated to a reporting origin. Note that the
+size of the request includes the URL itself, the body, the
+header list, and the referrer.
+
+ fetchLater(a_72_kb_url);
+ fetchLater("https://origin.example.com", {headers: headers_exceeding_64kb});
+ fetchLater(a_32_kb_url, {headers: headers_exceeding_32kb});
+ fetchLater("https://origin.example.com", {method: "POST", body: body_exceeding_64_kb});
+ fetchLater(a_62_kb_url /* with a 3kb referrer */);
+
+
+In the following sequence, the first two requests would succeed, but the third one would throw.
+That's because the overall 640 kibibytes quota was not exceeded in the first two calls, however the
+3rd request exceeds the reporting-origin quota for https://a.example.com
, and would
+throw.
+
+ fetchLater("https://a.example.com", {method: "POST", body: a_64kb_body});
+ fetchLater("https://b.example.com", {method: "POST", body: a_64kb_body});
+ fetchLater("https://a.example.com");
+
+
+Same-origin nested documents share the quota of their parent. However, cross-origin or +cross-agent iframes only receive 8kb of quota by default. So in the following example, the first 3 +calls would succeed and the last one would throw. +
+ // In main page
+ fetchLater("https://a.example.com", {method: "POST", body: a_64kb_body});
+
+ // In same-origin nested document
+ fetchLater("https://b.example.com", {method: "POST", body: a_64kb_body});
+
+ // In cross-origin nested document at https://frame.example.com
+ fetchLater("https://a.example.com", {body: a_5kb_body});
+ fetchLater("https://a.example.com", {body: a_12kb_body});
+
+
+
+To make the previous example not throw, the top-level {{Document}} can delegate some of its quota
+to https://frame.example.com
, for example by serving the following header:
+
Permissions-Policy: deferred-fetch=(self "https://frame.example.com")
+
+Each nested document reserves its own quota. So the following would work, because each frame +reserve 8 kibibytes: +
+ // In cross-origin nested document at https://frame.example.com/frame-1
+ fetchLater("https://a.example.com", {body: a_6kb_body});
+
+ // In cross-origin nested document at https://frame.example.com/frame-2
+ fetchLater("https://a.example.com", {body: a_6kb_body});
+
+
+The following chart illustrates how quota is distributed to different nested documents in a tree: + +
++ https://me.example.com with Permissions-policy: deferred-fetch=(self "https://ok.example.com")
+| (See below for quota)
+|
++ ---- + https://me.example.com
+| | Shares quota with the top-level traversable, as they're same origin.
+| |
+| + ---- + https://x.example.com
+| 8 kibibytes.
+|
+|
++ ---- + https://x.example.com
+| 8 kibibytes.
+| |
+| + https://me.example.com
+| 0. Even though it's same origin with the top-level traversable, it does not
+| automatically share its quota as they are separated by a cross-origin intermediary.
+|
++ ---- + https://ok.example.com/good
+| | 64 kibibytes, granted via the "{{PermissionsPolicy/deferred-fetch}}" policy.
+| |
+| + ---- + https://x.example.com
+| 0. Only documents with the same origin as the top-level traversable can
+| grant the 8 kibibytes based on the "{{PermissionsPolicy/deferred-fetch-minimal}}" policy.
+|
++ ---- + https://ok.example.com/redirect, navigated to https://x.example.com
+| 0. The reserved 64 kibibytes for https://ok.example.com are not available for https://x.example.com.
+|
++ ---- + https://ok.example.com/back, navigated to https://me.example.com
+ Shares quota with the top-level traversable, as they're same origin.
+
+
+In the above example, the top-level traversable and its same origin +descendants share a quota of 384 kibibytes. That value is computed as such: +
640 kibibytes are initially granted to the top-level traversable. + +
128 kibibytes are reserved for the "{{PermissionsPolicy/deferred-fetch-minimal}}" policy. + +
64 kibibytes are reserved for the container navigating to
+ https://ok.example/good
.
+
+
64 kibibytes are reserved for the container navigating to
+ https://ok.example/redirect
, and lost when it navigates away.
+
+
https://ok.example.com/back
did not reserve 64 kibibytes, because it navigated
+ back to top-level traversable's origin.
+
+ 640 - 128 - 64 - 64 = 384 kibibytes. +
This specification defines a policy-controlled feature identified by the string
+"deferred-fetch". Its
+default allowlist is "self
".
+
+
This specification defines a policy-controlled feature identified by the string
+"deferred-fetch-minimal". Its
+default allowlist is "*
".
+
+
The quota reserved for deferred-fetch-minimal
is 128 kibibytes.
+
+
To get the available deferred-fetch quota given a {{Document}} +controlDocument and an origin-or-null origin: + +
Let navigable be controlDocument's node navigable. + +
Let isTopLevel be true if controlDocument's node navigable is a + top-level traversable; otherwise false. + +
Let deferredFetchAllowed be true if controlDocument is + allowed to use the policy-controlled feature "{{PermissionsPolicy/deferred-fetch}}"; + otherwise false. + +
Let deferredFetchMinimalAllowed be true if controlDocument is + allowed to use the policy-controlled feature + "{{PermissionsPolicy/deferred-fetch-minimal}}"; otherwise false. + +
Let quota be the result of the first matching statement: + +
640 kibibytes +
640kb should be enough for everyone. + +
512 kibibytes +
The default of 640 kibibytes, decremented By
+ quota reserved for deferred-fetch-minimal
)
+
+
For each navigable in controlDocument's + node navigable's descendant navigables whose container document's + deferred-fetch control document is controlDocument, decrement quota + by navigable's navigable container's reserved deferred-fetch quota. + +
Delegate some of the quota to nested documents that reserved it. + +
If quota is equal or less than 0, then return 0. + +
Let quotaForRequestOrigin be 64 kibibytes. + +
For each deferred fetch record deferredRecord of + controlDocument's fetch group's + deferred fetch records:
+ +Let requestLength be the total request length of + deferredRecord's request. + +
Decrement quota by requestLength. + +
If deferredRecord's request's + URL's origin is same origin with origin, + then decrement quotaForRequestOrigin by requestLength. +
If quota is equal or less than 0, then return 0. + +
If quota is less than quotaForRequestOrigin, then return + quota. + +
Return quotaForRequestOrigin. +
To reserve deferred-fetch quota for a navigable container +container given an origin originToNavigateTo: + +
This is called on navigation, when the source document of the navigation is the +navigable's parent document. It potentially reserves either 64kb or 8kb of quota for +the container and its navigable, if allowed permissions policy. It is not observable to the +cotnainer document whether the reserved quota was used in practice. This algorithm assumes that the +container's document might delegate quota to the navigated frame, and the reserved quota would only +apply in that case, and would be ignored if it ends up being shared. If quota was reserved and the +document ends up being same origin with its parent, the quota would be +freed. + +
Set container's reserved deferred-fetch quota to 0. + +
Let controlDocument be container's node document's + deferred-fetch control document. + +
If the inherited policy
+ for "{{PermissionsPolicy/deferred-fetch}}", container and originToNavigateTo
+ is Enabled
, and the available deferred-fetch quota for
+ controlDocument is equal or greater than
+ normal quota, then set container's
+ reserved deferred-fetch quota to normal quota and
+ return.
+
+
If all of the following conditions are true: + +
controlDocument's node navigable is a top-level traversable + +
The inherited policy
+ for "{{PermissionsPolicy/deferred-fetch-minimal}}", container and
+ originToNavigateTo is Enabled
+
+
The size of controlDocument's node navigable's
+ descendant navigables, removing any navigable
+ whose navigable container's reserved deferred-fetch quota is not
+ minimal quota, is less than
+ quota reserved for deferred-fetch-minimal
/
+ minimal quota.
+
then set container's reserved deferred-fetch quota to + minimal quota. +
To potentially free deferred-fetch quota for a {{Document}} +document, if document's node navigable's container document is +not null, and its origin is same origin with document, then +set document's node navigable's navigable container's +reserved deferred-fetch quota to 0. + +
This is called when a {{Document}} is created. It ensures that same-origin nested +documents don't reserve quota, as they anyway share their parent quota. It can only be called upon +document creation, as the origin of the {{Document}} is only known after +redirects are handled. +
To get the deferred-fetch control document of a {{Document}} document: + +
If document' node navigable's container document is null or a + {{Document}} whose origin is not same origin with document, + return document; Otherwise return the deferred-fetch control document given + document' node navigable's container document. +
partial interface mixin WindowOrWorkerGlobalScope { [NewObject] Promise<Response> fetch(RequestInfo input, optional RequestInit init = {}); }; + + +dictionary DeferredRequestInit : RequestInit { + DOMHighResTimeStamp activateAfter; +}; + +[Exposed=Window] +interface FetchLaterResult { + readonly attribute boolean activated; +}; + +partial interface mixin Window { + [NewObject] FetchLaterResult fetchLater(RequestInfo input, optional DeferredRequestInit init = {}); +};
A {{FetchLaterResult}} has an associated activated getter steps, +which is an algorithm returning a boolean. + +
The activated
getter steps are to call
+this's activated getter steps.
+
The
+fetchLater(input, init)
+method steps are:
+
+
Let requestObject be the result of invoking the initial value of {{Request}} as + constructor with input and init as arguments. + +
If requestObject's signal is aborted, + then throw signal's abort reason. + +
Let request be requestObject's request. + +
Let activateAfter be null. + +
If init is given and init["{{DeferredRequestInit/activateAfter}}"] + exists, then set activateAfter to + init["{{DeferredRequestInit/activateAfter}}"]. + +
If activateAfter is less than 0, then throw a {{RangeError}}. + +
If request's client is not a fully active + {{Document}}, then throw a {{TypeError}}. + +
If request's URL's scheme is not an + HTTP(S) scheme, then throw a {{TypeError}}. + +
If request's URL is not a potentially trustworthy URL, + then throw a {{TypeError}}. + +
If request's body is not null, and request's + body length is null, then throw a {{TypeError}}. + +
Requests whose body is a {{ReadableStream}} cannot be deferred. + +
Let controlDocument be request's client's + deferred-fetch control document. + +
If the available deferred-fetch quota given controlDocument and + request's URL's origin is less than + request's total request length, then throw a "{{QuotaExceededError}}" + {{DOMException}}. + +
Let activated be false. + +
Let deferredRecord be the result of calling queue a deferred fetch given + request, controlDocument's fetch group, activateAfter, and + the following step: set activated to true. + +
Add the following abort steps to requestObject's + signal: + +
Set deferredRecord's done to true. + +
Remove deferredRecord from controlDocument's + fetch group's deferred fetch records. +
Return a new {{FetchLaterResult}} whose + activated getter steps are to return activated. +
The following call would queue a request to be fetched when the document is terminated: +
fetchLater("https://report.example.com", { method: "POST",
+ body: JSON.stringify(myReport) })
+
+ The following call would also queue this request after 5 seconds, and the returned value would + allow callers to observe if it was indeed activated. Note that the request is guaranteed to be + invoked, even in cases where the user agent throttles timers. + +
+ const result = fetchLater("https://report.example.com", {
+ method: "POST",
+ body: JSON.stringify(myReport),
+ activateAfter: 5000
+ });
+
+ function check_if_fetched() {
+ return result.activated;
+ }
+
+
+ The {{FetchLaterResult}} object can be used together with an {{AbortSignal}}. For example: +
+ let accumulated_events = [];
+ let previous_result = null;
+ const abort_signal = new AbortSignal();
+ function accumulate_event(event) {
+ if (previous_result) {
+ if (previous_result.activated) {
+ // The request is already activated, we can start from scratch.
+ accumulated_events = [];
+ } else {
+ // Abort this request, and start a new one with all the events.
+ signal.abort();
+ }
+ }
+
+ accumulated_events.push(event);
+ result = fetchLater("https://report.example.com", {
+ method: "POST",
+ body: JSON.stringify(accumulated_events),
+ activateAfter: 5000,
+ abort_signal
+ });
+ }
+
+
+
+ Any of the following calls to fetchLater()
would throw:
+
+ // Only potentially trustworthy urls are supported.
+ fetchLater("http://untrusted.example.com");
+
+ // The length of the deferred request has to be known when.
+ fetchLater("https://origin.example.com", {body: someDynamicStream});
+
+ // Deferred fetching only works on active windows.
+ const detachedWindow = iframe.contentWindow;
+ iframe.remove();
+ detachedWindow.fetchLater("https://origin.example.com");
+
+
+ See deferred fetch quota examples for examples
+ portraying how the deferred-fetch quota works.
+