[fetch-later] Limit max amount of deferred requests per origin
The [spec][1] of the "Deferred fetching" algorithm sets the maximum
bytes of deferred requests (a fetchLater request) for a request URL
origin to 64 kilobytes.
This CL implements the restriction by surfacing a fetch's Request
body size from Request::ExtractBody() to
FetchRequestData::BufferByteLength(). The body size is extracted from
v8 value when `ExtractBody()` performs the body extraction.
Intentionally not to modify `Body` (Requests's base class) nor
`BodyStreamBuffer` (FetchRequestData::Buffer()'s return type), as they
are all widely used beyond a request, e.g. it can also be a response body.
[1]: https://whatpr.org/fetch/1647/53e4c3d...71fd383.html#deferred-fetching
Bug: 1465781
Change-Id: I075be5043f5658971d347729923f968b428bc7c1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4835437
Commit-Queue: Ming-Ying Chung <mych@chromium.org>
Reviewed-by: Nidhi Jaju <nidhijaju@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1192831}
diff --git a/fetch/fetch-later/quota.tentative.https.window.js b/fetch/fetch-later/quota.tentative.https.window.js
new file mode 100644
index 0000000..4fc5979
--- /dev/null
+++ b/fetch/fetch-later/quota.tentative.https.window.js
@@ -0,0 +1,130 @@
+// META: script=/resources/testharness.js
+// META: script=/resources/testharnessreport.js
+// META: script=/common/get-host-info.sub.js
+// META: script=/common/utils.js
+// META: script=/pending-beacon/resources/pending_beacon-helper.js
+
+'use strict';
+
+const kQuotaPerOrigin = 64 * 1024; // 64 kilobytes per spec.
+const {ORIGIN, HTTPS_NOTSAMESITE_ORIGIN} = get_host_info();
+
+// Runs a test case that cover a single fetchLater() call with `body` in its
+// request payload. The call is not expected to throw any errors.
+function fetchLaterPostTest(body, description) {
+ test(() => {
+ const controller = new AbortController();
+ const result = fetchLater(
+ '/fetch-later',
+ {method: 'POST', signal: controller.signal, body: body});
+ assert_false(result.activated);
+ // Release quota taken by the pending request for subsequent tests.
+ controller.abort();
+ }, description);
+}
+
+// Test small payload for each supported data types.
+for (const [dataType, skipCharset] of Object.entries(
+ BeaconDataTypeToSkipCharset)) {
+ fetchLaterPostTest(
+ makeBeaconData(generateSequentialData(0, 1024, skipCharset), dataType),
+ `A fetchLater() call accept small data in POST request of ${dataType}.`);
+}
+
+// Test various size of payloads for the same origin.
+for (const dataType in BeaconDataType) {
+ if (dataType !== BeaconDataType.FormData &&
+ dataType !== BeaconDataType.URLSearchParams) {
+ // Skips FormData & URLSearchParams, as browser adds extra bytes to them
+ // in addition to the user-provided content. It is difficult to test a
+ // request right at the quota limit.
+ fetchLaterPostTest(
+ // Generates data that is exactly 64 kilobytes.
+ makeBeaconData(generatePayload(kQuotaPerOrigin), dataType),
+ `A single fetchLater() call takes up the per-origin quota for its ` +
+ `body of ${dataType}.`);
+ }
+}
+
+// Test empty payload.
+for (const dataType in BeaconDataType) {
+ test(
+ () => {
+ assert_throws_js(
+ TypeError, () => fetchLater('/', {method: 'POST', body: ''}));
+ },
+ `A single fetchLater() call does not accept empty data in POST request ` +
+ `of ${dataType}.`);
+}
+
+// Test oversized payload.
+for (const dataType in BeaconDataType) {
+ test(
+ () => {
+ assert_throws_dom(
+ 'QuotaExceededError',
+ () => fetchLater('/fetch-later', {
+ method: 'POST',
+ // Generates data that exceeds 64 kilobytes.
+ body:
+ makeBeaconData(generatePayload(kQuotaPerOrigin + 1), dataType)
+ }));
+ },
+ `A single fetchLater() call is not allowed to exceed per-origin quota ` +
+ `for its body of ${dataType}.`);
+}
+
+// Test accumulated oversized request.
+for (const dataType in BeaconDataType) {
+ test(
+ () => {
+ const controller = new AbortController();
+ // Makes the 1st call that sends only half of allowed quota.
+ fetchLater('/fetch-later', {
+ method: 'POST',
+ signal: controller.signal,
+ body: makeBeaconData(generatePayload(kQuotaPerOrigin / 2), dataType)
+ });
+
+ // Makes the 2nd call that sends half+1 of allowed quota.
+ assert_throws_dom('QuotaExceededError', () => {
+ fetchLater('/fetch-later', {
+ method: 'POST',
+ signal: controller.signal,
+ body: makeBeaconData(
+ generatePayload(kQuotaPerOrigin / 2 + 1), dataType)
+ });
+ });
+ // Release quota taken by the pending requests for subsequent tests.
+ controller.abort();
+ },
+ `The 2nd fetchLater() call is not allowed to exceed per-origin quota ` +
+ `for its body of ${dataType}.`);
+}
+
+// Test various size of payloads across different origins.
+for (const dataType in BeaconDataType) {
+ test(
+ () => {
+ const controller = new AbortController();
+ // Makes the 1st call that sends only half of allowed quota.
+ fetchLater('/fetch-later', {
+ method: 'POST',
+ signal: controller.signal,
+ body: makeBeaconData(generatePayload(kQuotaPerOrigin / 2), dataType)
+ });
+
+ // Makes the 2nd call that sends half+1 of allowed quota, but to a
+ // different origin.
+ fetchLater(`${HTTPS_NOTSAMESITE_ORIGIN}/fetch-later`, {
+ method: 'POST',
+ signal: controller.signal,
+ body:
+ makeBeaconData(generatePayload(kQuotaPerOrigin / 2 + 1), dataType)
+ });
+ // Release quota taken by the pending requests for subsequent tests.
+ controller.abort();
+ },
+ `The 2nd fetchLater() call to another origin does not exceed per-origin` +
+ ` quota for its body of ${dataType}.`);
+}
diff --git a/pending-beacon/resources/pending_beacon-helper.js b/pending-beacon/resources/pending_beacon-helper.js
index 3e8bd20..e7b6ea5 100644
--- a/pending-beacon/resources/pending_beacon-helper.js
+++ b/pending-beacon/resources/pending_beacon-helper.js
@@ -84,12 +84,18 @@
}
function generatePayload(size) {
- let data = '';
- if (size > 0) {
- const prefix = String(size) + ':';
- data = prefix + Array(size - prefix.length).fill('*').join('');
+ if (size == 0) {
+ return '';
}
- return data;
+ const prefix = String(size) + ':';
+ if (size < prefix.length) {
+ return Array(size).fill('*').join('');
+ }
+ if (size == prefix.length) {
+ return prefix;
+ }
+
+ return prefix + Array(size - prefix.length).fill('*').join('');
}
function generateSetBeaconURL(uuid, options) {