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: + +

+
request +
A request. + +
controller +
A fetch controller or null. +
+
+ +

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: + +

+
request +
A request. + +
done (default false) +
A boolean. +
+ +

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: + +

    +
  1. 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. + +

  2. Process deferred fetches for fetchGroup. +

+

Resolving domains

@@ -6718,6 +6753,415 @@ agent's CORS-preflight cache for which there is a cache entry match +

Deferred fetching

+ +

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: + +

    +
  1. Populate request from client given request. + +

  2. Set request's service-workers mode to "none". + +

  3. Set request's keepalive to true. + +

  4. Let deferredRecord be a new deferred fetch record whose + request is request. + +

  5. +

    Append deferredRecord to fetchGroup's + deferred fetch records. + +

  6. +

    If activateAfter is non-null, then run the following steps in parallel:

    + +
      +
    1. +

      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. +

      + +
    2. 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. +

    +
  7. + +
  8. Return deferredRecord. +

+
+ +
+

To compute the total request length of a request request: + +

    +
  1. Let totalRequestLength be the length of request's + URL, serialized with + [=URL serializer/exclude fragment=] set to true. + +

  2. Increment totalRequestLength by the length of + request's referrer, serialized. + +

  3. For each (name, value) of request's + header list, increment totalRequestLength by name's + length + value's length. + +

  4. Increment totalRequestLength by request's body's + length. + +

  5. Return totalRequestLength. +

+
+ +
+

To process deferred fetches given a fetch group fetchGroup: + +

    +
  1. For each deferred fetch record + deferredRecord of fetchGroup's + deferred fetch records, process a deferred fetch + deferredRecord. +

+
+ +
+

To process a deferred fetch deferredRecord: +

    +
  1. If deferredRecord's done is true, then + return false. + +

  2. Set deferredRecord's done to true. + +

  3. Fetch deferredRecord's request. + +

  4. Return true. +

+
+ +

Deferred fetching quota

+ + +

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: +

+
+ + +

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: + +

    +
  1. Let navigable be controlDocument's node navigable. + +

  2. Let isTopLevel be true if controlDocument's node navigable is a + top-level traversable; otherwise false. + +

  3. Let deferredFetchAllowed be true if controlDocument is + allowed to use the policy-controlled feature "{{PermissionsPolicy/deferred-fetch}}"; + otherwise false. + +

  4. Let deferredFetchMinimalAllowed be true if controlDocument is + allowed to use the policy-controlled feature + "{{PermissionsPolicy/deferred-fetch-minimal}}"; otherwise false. + +

  5. +

    Let quota be the result of the first matching statement: + +

    +
    isTopLevel is true and deferredFetchAllowed is false +
    0 + +
    isTopLevel is true and deferredFetchMinimalAllowed is false +
    +

    640 kibibytes +

    640kb should be enough for everyone. + +

    isTopLevel is true +
    +

    512 kibibytes +

    The default of 640 kibibytes, decremented By + quota reserved for deferred-fetch-minimal) + +

    deferredFetchAllowed is true, and navigable's + navigable container's reserved deferred-fetch quota is + normal quota +
    normal quota + +
    deferredFetchMinimalAllowed is true, and navigable's + navigable container's reserved deferred-fetch quota is + minimal quota +
    minimal quota +
    + +
  6. +

    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. + +

  7. If quota is equal or less than 0, then return 0. + +

  8. Let quotaForRequestOrigin be 64 kibibytes. + +

  9. +

    For each deferred fetch record deferredRecord of + controlDocument's fetch group's + deferred fetch records:

    + +
      +
    1. Let requestLength be the total request length of + deferredRecord's request. + +

    2. Decrement quota by requestLength. + +

    3. If deferredRecord's request's + URL's origin is same origin with origin, + then decrement quotaForRequestOrigin by requestLength. +

    + +
  10. If quota is equal or less than 0, then return 0. + +

  11. If quota is less than quotaForRequestOrigin, then return + quota. + +

  12. 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. + +

    +
  1. Set container's reserved deferred-fetch quota to 0. + +

  2. Let controlDocument be container's node document's + deferred-fetch control document. + +

  3. 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. + +

  4. +

    If all of the following conditions are true: + +

    + +

    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: + +

    +
  1. 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. +

+

Fetch API

@@ -8445,12 +8889,26 @@ otherwise false. -

Fetch method

+

Fetch methods

 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 = {});
+};
 
@@ -8584,6 +9042,145 @@ with a promise, request, responseObject, and an
+

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: + +

    +
  1. Let requestObject be the result of invoking the initial value of {{Request}} as + constructor with input and init as arguments. + +

  2. If requestObject's signal is aborted, + then throw signal's abort reason. + +

  3. Let request be requestObject's request. + +

  4. Let activateAfter be null. + +

  5. If init is given and init["{{DeferredRequestInit/activateAfter}}"] + exists, then set activateAfter to + init["{{DeferredRequestInit/activateAfter}}"]. + +

  6. If activateAfter is less than 0, then throw a {{RangeError}}. + +

  7. If request's client is not a fully active + {{Document}}, then throw a {{TypeError}}. + +

  8. If request's URL's scheme is not an + HTTP(S) scheme, then throw a {{TypeError}}. + +

  9. If request's URL is not a potentially trustworthy URL, + then throw a {{TypeError}}. + +

  10. +

    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. + +

  11. Let controlDocument be request's client's + deferred-fetch control document. + +

  12. 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}}. + +

  13. Let activated be false. + +

  14. 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. + +

  15. +

    Add the following abort steps to requestObject's + signal: + +

      +
    1. Set deferredRecord's done to true. + +

    2. Remove deferredRecord from controlDocument's + fetch group's deferred fetch records. +

    + +
  16. 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. +

Garbage collection