[WPT] BFCache eligibility: in-flight fetch (#31227)
When there is an in-flight network request at the time of navigation,
Chrome: The page is BFCached and
the fetch request completes successfully after restored.
Firefox: The page is not BFCached
Safari: The page is BFCached but
the fetch request is canceled.
Bug: https://github.com/whatwg/html/issues/6699
Change-Id: I1d84d92c6396abad62eab9306f8656cbf303f8c6
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3222281
Reviewed-by: Kouhei Ueno <kouhei@chromium.org>
Commit-Queue: Hiroshige Hayashizaki <hiroshige@chromium.org>
Cr-Commit-Position: refs/heads/main@{#962858}
Co-authored-by: Hiroshige Hayashizaki <hiroshige@chromium.org>
diff --git a/common/dispatcher/dispatcher.js b/common/dispatcher/dispatcher.js
index b789c6e..1bac938 100644
--- a/common/dispatcher/dispatcher.js
+++ b/common/dispatcher/dispatcher.js
@@ -106,7 +106,7 @@
// `execute_script()` returns a rejected Promise with the error's
// `message`.
// Note that currently the type of error (e.g. DOMException) is not
- // preserved.
+ // preserved, except for `TypeError`.
// The values should be able to be serialized by JSON.stringify().
async execute_script(fn, args) {
const receiver = token();
@@ -117,6 +117,9 @@
}
// exception
+ if (response.name === 'TypeError') {
+ throw new TypeError(response.value);
+ }
throw new Error(response.value);
}
@@ -180,6 +183,7 @@
} catch(e) {
response = JSON.stringify({
status: 'exception',
+ name: e.name,
value: e.message
});
}
diff --git a/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch-cors.html b/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch-cors.html
new file mode 100644
index 0000000..c04089a
--- /dev/null
+++ b/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch-cors.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="../resources/helper.sub.js"></script>
+<script src="../resources/inflight-fetch-helper.js"></script>
+<script>
+// Check whether the page is BFCached when there are in-flight network requests
+// at the time of navigation.
+
+// CORS and failing fetch.
+runTest(crossSiteUrl + '?delayBeforeHeader=2000&cors=yes', false, true,
+ 'CORS succeeded when in BFCache');
+runTest(crossSiteUrl + '?delayBeforeHeader=2000', false, false,
+ 'CORS failed when in BFCache');
+</script>
diff --git a/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch-redirects.html b/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch-redirects.html
new file mode 100644
index 0000000..b0b49d5
--- /dev/null
+++ b/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch-redirects.html
@@ -0,0 +1,34 @@
+<!doctype html>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="../resources/helper.sub.js"></script>
+<script src="../resources/inflight-fetch-helper.js"></script>
+<script>
+// Check whether the page is BFCached when there are in-flight network requests
+// at the time of navigation.
+
+// Redirects and CSP.
+runTest(
+ '/common/slow-redirect.py?delay=2&location=' +
+ encodeURIComponent(sameOriginUrl),
+ false, true,
+ 'Redirect header received when in BFCache');
+runTest(
+ '/common/slow-redirect.py?delay=2&location=' +
+ encodeURIComponent(sameOriginUrl),
+ true, true,
+ 'Redirect header received when in BFCache w/ CSP passing');
+runTest(
+ '/common/slow-redirect.py?delay=2&location=' +
+ encodeURIComponent(crossSiteUrl + '?cors=yes'),
+ false, true,
+ 'Cross-origin redirect header received when in BFCache');
+runTest(
+ '/common/slow-redirect.py?delay=2&location=' +
+ encodeURIComponent(crossSiteUrl + '?cors=yes'),
+ true, false,
+ 'Cross-origin redirect header received when in BFCache w/ CSP failing');
+</script>
diff --git a/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch.html b/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch.html
new file mode 100644
index 0000000..3ce8e99
--- /dev/null
+++ b/html/browsers/browsing-the-web/back-forward-cache/eligibility/inflight-fetch.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="../resources/helper.sub.js"></script>
+<script src="../resources/inflight-fetch-helper.js"></script>
+<script>
+// Check whether the page is BFCached when there are in-flight network requests
+// at the time of navigation.
+
+// Successful fetch completion with different header/body timing.
+runTest(sameOriginUrl + '?delayBeforeBody=2000', false, true,
+ 'Header received before BFCache and body received when in BFCache');
+runTest(sameOriginUrl + '?delayBeforeBody=3500', false, true,
+ 'Header received before BFCache and body received after BFCache');
+runTest(sameOriginUrl + '?delayBeforeHeader=2000', false, true,
+ 'Header and body received when in BFCache');
+runTest(sameOriginUrl + '?delayBeforeHeader=2000&delayBeforeBody=1500',
+ false, true,
+ 'Header received when in BFCache and body received after BFCache');
+runTest(sameOriginUrl + '?delayBeforeHeader=3500', false, true,
+ 'Header and body received after BFCache');
+</script>
diff --git a/html/browsers/browsing-the-web/back-forward-cache/resources/helper.sub.js b/html/browsers/browsing-the-web/back-forward-cache/resources/helper.sub.js
index b2087d9..0af00f9 100644
--- a/html/browsers/browsing-the-web/back-forward-cache/resources/helper.sub.js
+++ b/html/browsers/browsing-the-web/back-forward-cache/resources/helper.sub.js
@@ -67,7 +67,8 @@
// Run a test that navigates A->B->A:
// 1. Page A is opened by `params.openFunc(url)`.
-// 2. `params.funcBeforeNavigation` is executed on page A.
+// 2. `params.funcBeforeNavigation(params.argsBeforeNavigation)` is executed
+// on page A.
// 3. The window is navigated to page B on `params.targetOrigin`.
// 4. The window is back navigated to page A (expecting BFCached).
//
@@ -104,7 +105,7 @@
runBfcacheTest(params, description);
}
-async function navigateAndThenBack(pageA, pageB, urlB) {
+async function navigateAndThenBack(pageA, pageB, urlB, funcBeforeBackNavigation) {
await pageA.execute_script(
(url) => {
prepareNavigation(() => {
@@ -115,6 +116,9 @@
);
await pageB.execute_script(waitForPageShow);
+ if (funcBeforeBackNavigation) {
+ await pageB.execute_script(funcBeforeBackNavigation);
+ }
await pageB.execute_script(
() => {
prepareNavigation(() => { history.back(); });
@@ -129,7 +133,9 @@
openFunc: url => window.open(url, '_blank', 'noopener'),
scripts: [],
funcBeforeNavigation: () => {},
+ argsBeforeNavigation: [],
targetOrigin: originCrossSite,
+ funcBeforeBackNavigation: () => {},
shouldBeCached: true,
funcAfterAssertion: () => {},
}
@@ -156,8 +162,10 @@
}, [src]);
}
- await pageA.execute_script(params.funcBeforeNavigation);
- await navigateAndThenBack(pageA, pageB, urlB);
+ await pageA.execute_script(params.funcBeforeNavigation,
+ params.argsBeforeNavigation);
+ await navigateAndThenBack(pageA, pageB, urlB,
+ params.funcBeforeBackNavigation);
if (params.shouldBeCached) {
await assert_bfcached(pageA);
@@ -166,7 +174,7 @@
}
if (params.funcAfterAssertion) {
- await params.funcAfterAssertion(pageA, pageB);
+ await params.funcAfterAssertion(pageA, pageB, t);
}
}, description);
}
diff --git a/html/browsers/browsing-the-web/back-forward-cache/resources/inflight-fetch-helper.js b/html/browsers/browsing-the-web/back-forward-cache/resources/inflight-fetch-helper.js
new file mode 100644
index 0000000..7832003
--- /dev/null
+++ b/html/browsers/browsing-the-web/back-forward-cache/resources/inflight-fetch-helper.js
@@ -0,0 +1,47 @@
+// Delay after fetch start:
+// - 0.0 seconds: before BFCache
+// - 2.0 seconds: when in BFCache
+// - 3.5 seconds: after restored from BFCache
+function runTest(urlToFetch, hasCSP, shouldSucceed, description) {
+ runBfcacheTest({
+ funcBeforeNavigation: async (urlToFetch, hasCSP) => {
+ if (hasCSP) {
+ // Set CSP.
+ const meta = document.createElement('meta');
+ meta.setAttribute('http-equiv', 'Content-Security-Policy');
+ meta.setAttribute('content', "connect-src 'self'");
+ document.head.appendChild(meta);
+ }
+
+ // Initiate a `fetch()`.
+ window.fetchPromise = fetch(urlToFetch);
+
+ // Wait for 0.5 seconds to receive response headers for the fetch()
+ // before BFCache, if any.
+ await new Promise(resolve => setTimeout(resolve, 500));
+ },
+ argsBeforeNavigation: [urlToFetch, hasCSP],
+ funcBeforeBackNavigation: () => {
+ // Wait for 2 seconds before back navigating to pageA.
+ return new Promise(resolve => setTimeout(resolve, 2000));
+ },
+ funcAfterAssertion: async (pageA, pageB, t) => {
+ // Wait for fetch() completion and check the result.
+ const result = pageA.execute_script(
+ () => window.fetchPromise.then(r => r.text()));
+ if (shouldSucceed) {
+ assert_equals(
+ await result,
+ 'Body',
+ 'Fetch should complete successfully after restored from BFCache');
+ } else {
+ await promise_rejects_js(t, TypeError, result,
+ 'Fetch should fail after restored from BFCache');
+ }
+ }
+ }, 'Eligibility (in-flight fetch): ' + description);
+}
+
+const url = new URL('../resources/slow.py', location);
+const sameOriginUrl = url.href;
+const crossSiteUrl = originCrossSite + url.pathname;
diff --git a/html/browsers/browsing-the-web/back-forward-cache/resources/slow.py b/html/browsers/browsing-the-web/back-forward-cache/resources/slow.py
new file mode 100644
index 0000000..01bb330
--- /dev/null
+++ b/html/browsers/browsing-the-web/back-forward-cache/resources/slow.py
@@ -0,0 +1,13 @@
+import time
+
+def main(request, response):
+ delay_before_header = float(request.GET.first(b"delayBeforeHeader", 0)) / 1000
+ delay_before_body = float(request.GET.first(b"delayBeforeBody", 0)) / 1000
+
+ time.sleep(delay_before_header)
+ if b"cors" in request.GET:
+ response.headers.set(b"Access-Control-Allow-Origin", b"*")
+ response.write_status_headers()
+
+ time.sleep(delay_before_body)
+ response.writer.write_content(b"Body")
diff --git a/lint.ignore b/lint.ignore
index e281f71..465b39b 100644
--- a/lint.ignore
+++ b/lint.ignore
@@ -153,6 +153,7 @@
SET TIMEOUT: encrypted-media/polyfill/clearkey-polyfill.js
SET TIMEOUT: encrypted-media/scripts/playback-temporary-events.js
SET TIMEOUT: generic-sensor/resources/iframe_sensor_handler.html
+SET TIMEOUT: html/browsers/browsing-the-web/back-forward-cache/resources/inflight-fetch-helper.js
SET TIMEOUT: html/browsers/browsing-the-web/history-traversal/*
SET TIMEOUT: html/browsers/browsing-the-web/navigating-across-documents/*
SET TIMEOUT: html/browsers/browsing-the-web/scroll-to-fragid/*