[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) {