WARNING: This API is being replaced with fetchLater()
, a Fetch-based approach.
--
This document is an explainer for PendingRequest API. It is proposed in response to a series of discussions and concerns around the experimental PendingBeacon API. There many open discussions within the explainers, which leads to the fetchLater() API.
Note that this proposal is NEVER implemented.
The basic idea is to extend the Fetch API by adding a new stateful option:
Rather than a developer manually calling fetch(url, {keepalive: true})
within a visibilitychange
event listener, the developer registers that they would like to send a pending request, i.e. a beacon, for this page when it gets discarded.
The developer can then call signal controller registered on this request to updates based on its state or abort.
Then, at some point later after the user leaves the page, the browser will send the request. From the point of view of the developer the exact send time is unknown. On successful sending, the whole response will be ignored, including body and headers. Nothing at all should be processed or updated, as the page is already gone.
The following new fetch options are introduced into RequestInit
:
deferSend
: ADeferSend
object. If set, the browser should defer the request sending until page discard or bfcache eviction. Underlying implementation should ensure the request is kept alive until suceeds or fails. Hence it cannot work withkeepalive: false
. The object may optionally set the following field:sendAfterBeingBackgroundedTimeout
: Specifies a timeout in seconds for a timer that only starts after the page enters the nexthidden
visibility state. Default to-1
.
sentSignal
: ASentSignal
object to allow user to listen to thesent
event of the request when it gets sent.
fetch('/send_beacon', {deferSend: new DeferSend()}).then(res => {
// Promise may never be resolved and response may be dropped.
})
Defer a request until next hidden
+ 1 minute
fetch('/send_beacon', {
deferSend: new DeferSend(sendAfterBeingBackgroundedTimeout: 60)
}).then(res => {
// Possibly resolved after next `hidden` + 1 minute.
// But this may still not be resolved if page is already in bfcache.
})
let abort = null;
let pending = true;
function createBeacon(data) {
pending = true;
abort = new AbortController();
let sentSignal = new SentSignal();
fetch(data, {
deferSend: new DeferSend(),
signal: abortController.signal,
sentSignal: sentsentSignal
});
sentSignal.addEventListener("sent", () => {
pending = false;
});
}
function updateBeacon(data) {
if (pending) {
abort.abort();
}
createBeacon(data);
}
NOTE: Discussions in #72.
Even if moving toward a fetch-based design, this proposal does still not focus on supporting every type of requests as beacons.
For example, it's non-goal to support most of HTTP methods, i.e. being able to defer an OPTION
or TRACE
.
We should look into RequestInit
and decide whether deferSend
should throw errors on some of their values:
keepalive
: must betrue
.{deferSend: new DeferSend(), keepalive: false}
conflicts with each other.url
: supported.method
: one ofGET
orPOST
.headers
: supported.body
: only supported forPOST
.signal
: supported.credentials
: enforcingsame-origin
to be consistent.cache
: not supported?redirect
: enforcingfollow
?referrer
: enforcing same-origin URL?referrerPolicy
: enforcingsame-origin
?integrity
: not supported?priority
: enforcingauto
?
As shown above, at least keepalive: true
and method
need to be enforced.
If going with this route, can we also consider the PendingRequest API approach that proposes a subclass of Request
to enforce the above?
class DeferSend {
constructor(sendAfterBeingBackgroundedTimeout)
}
Current proposal is to make deferSend
a class, and sendAfterBeingBackgroundedTimeout
its optional field.
- Should this be a standalone option in
RequestInit
? But it is not relevant to other existing fetch options. - Should it be after
hidden
orpagehide
(bfcached)? (Previous discussion in #13). - Need user input for how desirable for this option.
- Need better naming suggestion.
NOTE: Discussions in #74.
To maintain the same semantic, browser should resolve Promise when the pending request is sent. But in reality, the Promise may or may not be resolved, or resolved when the page is in bfcache and JS context is frozen. User should not rely on it.
NOTE: Discussions in #75.
This is to observe a event to tell if a deferSend
request is still pending.
To prevent from data races, the underlying implementation should ensure that renderer is authoritative to the request's send state when it's alive. Similar to this discussion for PendingBeacon.
NOTE: Discussions in #76.
As setting deferSend
implies keepalive
is also true, such request has to share the same size limit budget as a regular keepalive request’s one: "for each fetch group, the sum of contentLength and inflightKeepaliveBytes <= 64 KB".
To comply with the limit, there are several options:
fetch()
throwsTypeError
whenever the budget has exceeded. Users will not be able to create new pending requests.- The browser forces sending out other existing pending requests, in FIFO order, when the budget has exceeded. For a single request > 64KB,
fetch()
should still throwsTypeError
. - Ignore the size limit if BackgroundFetch Permission is enabled for the page.
NOTE: Discussions in #77.
Given that most reporting API providers are crossed origins, we propose to allow this feature by default for 3rd-party iframes. User should be able to opt out the feature with the corresponding Permissions Policy.