summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/fetch/api/abort
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/fetch/api/abort')
-rw-r--r--testing/web-platform/tests/fetch/api/abort/cache.https.any.js47
-rw-r--r--testing/web-platform/tests/fetch/api/abort/destroyed-context.html27
-rw-r--r--testing/web-platform/tests/fetch/api/abort/general.any.js572
-rw-r--r--testing/web-platform/tests/fetch/api/abort/keepalive.html85
-rw-r--r--testing/web-platform/tests/fetch/api/abort/request.any.js85
-rw-r--r--testing/web-platform/tests/fetch/api/abort/serviceworker-intercepted.https.html212
6 files changed, 1028 insertions, 0 deletions
diff --git a/testing/web-platform/tests/fetch/api/abort/cache.https.any.js b/testing/web-platform/tests/fetch/api/abort/cache.https.any.js
new file mode 100644
index 0000000000..bdaf0e69e5
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/abort/cache.https.any.js
@@ -0,0 +1,47 @@
+// META: title=Request signals & the cache API
+// META: global=window,worker
+
+promise_test(async () => {
+ await caches.delete('test');
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const request = new Request('../resources/data.json', { signal });
+
+ const cache = await caches.open('test');
+ await cache.put(request, new Response(''));
+
+ const requests = await cache.keys();
+
+ assert_equals(requests.length, 1, 'Ensuring cleanup worked');
+
+ const [cachedRequest] = requests;
+
+ controller.abort();
+
+ assert_false(cachedRequest.signal.aborted, "Request from cache shouldn't be aborted");
+
+ const data = await fetch(cachedRequest).then(r => r.json());
+ assert_equals(data.key, 'value', 'Fetch fully completes');
+}, "Signals are not stored in the cache API");
+
+promise_test(async () => {
+ await caches.delete('test');
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const request = new Request('../resources/data.json', { signal });
+ controller.abort();
+
+ const cache = await caches.open('test');
+ await cache.put(request, new Response(''));
+
+ const requests = await cache.keys();
+
+ assert_equals(requests.length, 1, 'Ensuring cleanup worked');
+
+ const [cachedRequest] = requests;
+
+ assert_false(cachedRequest.signal.aborted, "Request from cache shouldn't be aborted");
+
+ const data = await fetch(cachedRequest).then(r => r.json());
+ assert_equals(data.key, 'value', 'Fetch fully completes');
+}, "Signals are not stored in the cache API, even if they're already aborted");
diff --git a/testing/web-platform/tests/fetch/api/abort/destroyed-context.html b/testing/web-platform/tests/fetch/api/abort/destroyed-context.html
new file mode 100644
index 0000000000..161d39bd9c
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/abort/destroyed-context.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+// This is a regression test for crbug.com/860063.
+window.controller = new AbortController();
+async_test(t => {
+ onmessage = t.step_func(event => {
+ assert_equals(event.data, 'started');
+ const iframe = document.querySelector('iframe');
+ document.body.removeChild(iframe);
+ controller.abort();
+ t.done();
+ });
+}, 'aborting a fetch in a destroyed context should not crash');
+</script>
+<iframe srcdoc="
+ <!DOCTYPE html>
+ <meta charset=utf-8>
+ <script>
+ fetch('../resources/infinite-slow-response.py', { signal: parent.controller.signal }).then(() => {
+ parent.postMessage('started', '*');
+ });
+ </script>
+ ">
+</iframe>
diff --git a/testing/web-platform/tests/fetch/api/abort/general.any.js b/testing/web-platform/tests/fetch/api/abort/general.any.js
new file mode 100644
index 0000000000..3727bb42af
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/abort/general.any.js
@@ -0,0 +1,572 @@
+// META: timeout=long
+// META: global=window,worker
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=../request/request-error.js
+
+const BODY_METHODS = ['arrayBuffer', 'blob', 'formData', 'json', 'text'];
+
+const error1 = new Error('error1');
+error1.name = 'error1';
+
+// This is used to close connections that weren't correctly closed during the tests,
+// otherwise you can end up running out of HTTP connections.
+let requestAbortKeys = [];
+
+function abortRequests() {
+ const keys = requestAbortKeys;
+ requestAbortKeys = [];
+ return Promise.all(
+ keys.map(key => fetch(`../resources/stash-put.py?key=${key}&value=close`))
+ );
+}
+
+const hostInfo = get_host_info();
+const urlHostname = hostInfo.REMOTE_HOST;
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const fetchPromise = fetch('../resources/data.json', { signal });
+
+ await promise_rejects_dom(t, "AbortError", fetchPromise);
+}, "Aborting rejects with AbortError");
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort(error1);
+
+ const fetchPromise = fetch('../resources/data.json', { signal });
+
+ await promise_rejects_exactly(t, error1, fetchPromise, 'fetch() should reject with abort reason');
+}, "Aborting rejects with abort reason");
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const url = new URL('../resources/data.json', location);
+ url.hostname = urlHostname;
+
+ const fetchPromise = fetch(url, {
+ signal,
+ mode: 'no-cors'
+ });
+
+ await promise_rejects_dom(t, "AbortError", fetchPromise);
+}, "Aborting rejects with AbortError - no-cors");
+
+// Test that errors thrown from the request constructor take priority over abort errors.
+// badRequestArgTests is from response-error.js
+for (const { args, testName } of badRequestArgTests) {
+ promise_test(async t => {
+ try {
+ // If this doesn't throw, we'll effectively skip the test.
+ // It'll fail properly in ../request/request-error.html
+ new Request(...args);
+ }
+ catch (err) {
+ const controller = new AbortController();
+ controller.abort();
+
+ // Add signal to 2nd arg
+ args[1] = args[1] || {};
+ args[1].signal = controller.signal;
+ await promise_rejects_js(t, TypeError, fetch(...args));
+ }
+ }, `TypeError from request constructor takes priority - ${testName}`);
+}
+
+test(() => {
+ const request = new Request('');
+ assert_true(Boolean(request.signal), "Signal member is present & truthy");
+ assert_equals(request.signal.constructor, AbortSignal);
+}, "Request objects have a signal property");
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const request = new Request('../resources/data.json', { signal });
+
+ assert_true(Boolean(request.signal), "Signal member is present & truthy");
+ assert_equals(request.signal.constructor, AbortSignal);
+ assert_not_equals(request.signal, signal, 'Request has a new signal, not a reference');
+ assert_true(request.signal.aborted, `Request's signal has aborted`);
+
+ const fetchPromise = fetch(request);
+
+ await promise_rejects_dom(t, "AbortError", fetchPromise);
+}, "Signal on request object");
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort(error1);
+
+ const request = new Request('../resources/data.json', { signal });
+
+ assert_not_equals(request.signal, signal, 'Request has a new signal, not a reference');
+ assert_true(request.signal.aborted, `Request's signal has aborted`);
+ assert_equals(request.signal.reason, error1, `Request's signal's abort reason is error1`);
+
+ const fetchPromise = fetch(request);
+
+ await promise_rejects_exactly(t, error1, fetchPromise, "fetch() should reject with abort reason");
+}, "Signal on request object should also have abort reason");
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const request = new Request('../resources/data.json', { signal });
+ const requestFromRequest = new Request(request);
+
+ const fetchPromise = fetch(requestFromRequest);
+
+ await promise_rejects_dom(t, "AbortError", fetchPromise);
+}, "Signal on request object created from request object");
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const request = new Request('../resources/data.json');
+ const requestFromRequest = new Request(request, { signal });
+
+ const fetchPromise = fetch(requestFromRequest);
+
+ await promise_rejects_dom(t, "AbortError", fetchPromise);
+}, "Signal on request object created from request object, with signal on second request");
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const request = new Request('../resources/data.json', { signal: new AbortController().signal });
+ const requestFromRequest = new Request(request, { signal });
+
+ const fetchPromise = fetch(requestFromRequest);
+
+ await promise_rejects_dom(t, "AbortError", fetchPromise);
+}, "Signal on request object created from request object, with signal on second request overriding another");
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const request = new Request('../resources/data.json', { signal });
+
+ const fetchPromise = fetch(request, {method: 'POST'});
+
+ await promise_rejects_dom(t, "AbortError", fetchPromise);
+}, "Signal retained after unrelated properties are overridden by fetch");
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const request = new Request('../resources/data.json', { signal });
+
+ const data = await fetch(request, { signal: null }).then(r => r.json());
+ assert_equals(data.key, 'value', 'Fetch fully completes');
+}, "Signal removed by setting to null");
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const log = [];
+
+ await Promise.all([
+ fetch('../resources/data.json', { signal }).then(
+ () => assert_unreached("Fetch must not resolve"),
+ () => log.push('fetch-reject')
+ ),
+ Promise.resolve().then(() => log.push('next-microtask'))
+ ]);
+
+ assert_array_equals(log, ['fetch-reject', 'next-microtask']);
+}, "Already aborted signal rejects immediately");
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const request = new Request('../resources/data.json', {
+ signal,
+ method: 'POST',
+ body: 'foo',
+ headers: { 'Content-Type': 'text/plain' }
+ });
+
+ await fetch(request).catch(() => {});
+
+ assert_true(request.bodyUsed, "Body has been used");
+}, "Request is still 'used' if signal is aborted before fetching");
+
+for (const bodyMethod of BODY_METHODS) {
+ promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+
+ const log = [];
+ const response = await fetch('../resources/data.json', { signal });
+
+ controller.abort();
+
+ const bodyPromise = response[bodyMethod]();
+
+ await Promise.all([
+ bodyPromise.catch(() => log.push(`${bodyMethod}-reject`)),
+ Promise.resolve().then(() => log.push('next-microtask'))
+ ]);
+
+ await promise_rejects_dom(t, "AbortError", bodyPromise);
+
+ assert_array_equals(log, [`${bodyMethod}-reject`, 'next-microtask']);
+ }, `response.${bodyMethod}() rejects if already aborted`);
+}
+
+promise_test(async (t) => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+
+ const res = await fetch('../resources/data.json', { signal });
+ controller.abort();
+
+ await promise_rejects_dom(t, 'AbortError', res.text());
+ await promise_rejects_dom(t, 'AbortError', res.text());
+}, 'Call text() twice on aborted response');
+
+promise_test(async t => {
+ await abortRequests();
+
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const stateKey = token();
+ const abortKey = token();
+ requestAbortKeys.push(abortKey);
+ controller.abort();
+
+ await fetch(`../resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, { signal }).catch(() => {});
+
+ // I'm hoping this will give the browser enough time to (incorrectly) make the request
+ // above, if it intends to.
+ await fetch('../resources/data.json').then(r => r.json());
+
+ const response = await fetch(`../resources/stash-take.py?key=${stateKey}`);
+ const data = await response.json();
+
+ assert_equals(data, null, "Request hasn't been made to the server");
+}, "Already aborted signal does not make request");
+
+promise_test(async t => {
+ await abortRequests();
+
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const fetches = [];
+
+ for (let i = 0; i < 3; i++) {
+ const abortKey = token();
+ requestAbortKeys.push(abortKey);
+
+ fetches.push(
+ fetch(`../resources/infinite-slow-response.py?${i}&abortKey=${abortKey}`, { signal })
+ );
+ }
+
+ for (const fetchPromise of fetches) {
+ await promise_rejects_dom(t, "AbortError", fetchPromise);
+ }
+}, "Already aborted signal can be used for many fetches");
+
+promise_test(async t => {
+ await abortRequests();
+
+ const controller = new AbortController();
+ const signal = controller.signal;
+
+ await fetch('../resources/data.json', { signal }).then(r => r.json());
+
+ controller.abort();
+
+ const fetches = [];
+
+ for (let i = 0; i < 3; i++) {
+ const abortKey = token();
+ requestAbortKeys.push(abortKey);
+
+ fetches.push(
+ fetch(`../resources/infinite-slow-response.py?${i}&abortKey=${abortKey}`, { signal })
+ );
+ }
+
+ for (const fetchPromise of fetches) {
+ await promise_rejects_dom(t, "AbortError", fetchPromise);
+ }
+}, "Signal can be used to abort other fetches, even if another fetch succeeded before aborting");
+
+promise_test(async t => {
+ await abortRequests();
+
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const stateKey = token();
+ const abortKey = token();
+ requestAbortKeys.push(abortKey);
+
+ await fetch(`../resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, { signal });
+
+ const beforeAbortResult = await fetch(`../resources/stash-take.py?key=${stateKey}`).then(r => r.json());
+ assert_equals(beforeAbortResult, "open", "Connection is open");
+
+ controller.abort();
+
+ // The connection won't close immediately, but it should close at some point:
+ const start = Date.now();
+
+ while (true) {
+ // Stop spinning if 10 seconds have passed
+ if (Date.now() - start > 10000) throw Error('Timed out');
+
+ const afterAbortResult = await fetch(`../resources/stash-take.py?key=${stateKey}`).then(r => r.json());
+ if (afterAbortResult == 'closed') break;
+ }
+}, "Underlying connection is closed when aborting after receiving response");
+
+promise_test(async t => {
+ await abortRequests();
+
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const stateKey = token();
+ const abortKey = token();
+ requestAbortKeys.push(abortKey);
+
+ const url = new URL(`../resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, location);
+ url.hostname = urlHostname;
+
+ await fetch(url, {
+ signal,
+ mode: 'no-cors'
+ });
+
+ const stashTakeURL = new URL(`../resources/stash-take.py?key=${stateKey}`, location);
+ stashTakeURL.hostname = urlHostname;
+
+ const beforeAbortResult = await fetch(stashTakeURL).then(r => r.json());
+ assert_equals(beforeAbortResult, "open", "Connection is open");
+
+ controller.abort();
+
+ // The connection won't close immediately, but it should close at some point:
+ const start = Date.now();
+
+ while (true) {
+ // Stop spinning if 10 seconds have passed
+ if (Date.now() - start > 10000) throw Error('Timed out');
+
+ const afterAbortResult = await fetch(stashTakeURL).then(r => r.json());
+ if (afterAbortResult == 'closed') break;
+ }
+}, "Underlying connection is closed when aborting after receiving response - no-cors");
+
+for (const bodyMethod of BODY_METHODS) {
+ promise_test(async t => {
+ await abortRequests();
+
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const stateKey = token();
+ const abortKey = token();
+ requestAbortKeys.push(abortKey);
+
+ const response = await fetch(`../resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, { signal });
+
+ const beforeAbortResult = await fetch(`../resources/stash-take.py?key=${stateKey}`).then(r => r.json());
+ assert_equals(beforeAbortResult, "open", "Connection is open");
+
+ const bodyPromise = response[bodyMethod]();
+
+ controller.abort();
+
+ await promise_rejects_dom(t, "AbortError", bodyPromise);
+
+ const start = Date.now();
+
+ while (true) {
+ // Stop spinning if 10 seconds have passed
+ if (Date.now() - start > 10000) throw Error('Timed out');
+
+ const afterAbortResult = await fetch(`../resources/stash-take.py?key=${stateKey}`).then(r => r.json());
+ if (afterAbortResult == 'closed') break;
+ }
+ }, `Fetch aborted & connection closed when aborted after calling response.${bodyMethod}()`);
+}
+
+promise_test(async t => {
+ await abortRequests();
+
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const stateKey = token();
+ const abortKey = token();
+ requestAbortKeys.push(abortKey);
+
+ const response = await fetch(`../resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, { signal });
+ const reader = response.body.getReader();
+
+ controller.abort();
+
+ await promise_rejects_dom(t, "AbortError", reader.read());
+ await promise_rejects_dom(t, "AbortError", reader.closed);
+
+ // The connection won't close immediately, but it should close at some point:
+ const start = Date.now();
+
+ while (true) {
+ // Stop spinning if 10 seconds have passed
+ if (Date.now() - start > 10000) throw Error('Timed out');
+
+ const afterAbortResult = await fetch(`../resources/stash-take.py?key=${stateKey}`).then(r => r.json());
+ if (afterAbortResult == 'closed') break;
+ }
+}, "Stream errors once aborted. Underlying connection closed.");
+
+promise_test(async t => {
+ await abortRequests();
+
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const stateKey = token();
+ const abortKey = token();
+ requestAbortKeys.push(abortKey);
+
+ const response = await fetch(`../resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, { signal });
+ const reader = response.body.getReader();
+
+ await reader.read();
+
+ controller.abort();
+
+ await promise_rejects_dom(t, "AbortError", reader.read());
+ await promise_rejects_dom(t, "AbortError", reader.closed);
+
+ // The connection won't close immediately, but it should close at some point:
+ const start = Date.now();
+
+ while (true) {
+ // Stop spinning if 10 seconds have passed
+ if (Date.now() - start > 10000) throw Error('Timed out');
+
+ const afterAbortResult = await fetch(`../resources/stash-take.py?key=${stateKey}`).then(r => r.json());
+ if (afterAbortResult == 'closed') break;
+ }
+}, "Stream errors once aborted, after reading. Underlying connection closed.");
+
+promise_test(async t => {
+ await abortRequests();
+
+ const controller = new AbortController();
+ const signal = controller.signal;
+
+ const response = await fetch(`../resources/empty.txt`, { signal });
+
+ // Read whole response to ensure close signal has sent.
+ await response.clone().text();
+
+ const reader = response.body.getReader();
+
+ controller.abort();
+
+ const item = await reader.read();
+
+ assert_true(item.done, "Stream is done");
+}, "Stream will not error if body is empty. It's closed with an empty queue before it errors.");
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ let cancelReason;
+
+ const body = new ReadableStream({
+ pull(controller) {
+ controller.enqueue(new Uint8Array([42]));
+ },
+ cancel(reason) {
+ cancelReason = reason;
+ }
+ });
+
+ const fetchPromise = fetch('../resources/empty.txt', {
+ body, signal,
+ method: 'POST',
+ duplex: 'half',
+ headers: {
+ 'Content-Type': 'text/plain'
+ }
+ });
+
+ assert_true(!!cancelReason, 'Cancel called sync');
+ assert_equals(cancelReason.constructor, DOMException);
+ assert_equals(cancelReason.name, 'AbortError');
+
+ await promise_rejects_dom(t, "AbortError", fetchPromise);
+
+ const fetchErr = await fetchPromise.catch(e => e);
+
+ assert_equals(cancelReason, fetchErr, "Fetch rejects with same error instance");
+}, "Readable stream synchronously cancels with AbortError if aborted before reading");
+
+test(() => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const request = new Request('.', { signal });
+ const requestSignal = request.signal;
+
+ const clonedRequest = request.clone();
+
+ assert_equals(requestSignal, request.signal, "Original request signal the same after cloning");
+ assert_true(request.signal.aborted, "Original request signal aborted");
+ assert_not_equals(clonedRequest.signal, request.signal, "Cloned request has different signal");
+ assert_true(clonedRequest.signal.aborted, "Cloned request signal aborted");
+}, "Signal state is cloned");
+
+test(() => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+
+ const request = new Request('.', { signal });
+ const clonedRequest = request.clone();
+
+ const log = [];
+
+ request.signal.addEventListener('abort', () => log.push('original-aborted'));
+ clonedRequest.signal.addEventListener('abort', () => log.push('clone-aborted'));
+
+ controller.abort();
+
+ assert_array_equals(log, ['original-aborted', 'clone-aborted'], "Abort events fired in correct order");
+ assert_true(request.signal.aborted, 'Signal aborted');
+ assert_true(clonedRequest.signal.aborted, 'Signal aborted');
+}, "Clone aborts with original controller");
diff --git a/testing/web-platform/tests/fetch/api/abort/keepalive.html b/testing/web-platform/tests/fetch/api/abort/keepalive.html
new file mode 100644
index 0000000000..db12df0d28
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/abort/keepalive.html
@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script>
+// This controller must be on the window so it is visible to the iframe.
+window.sharedController = new AbortController();
+
+async function fetchJson(url) {
+ const response = await fetch(url);
+ assert_true(response.ok, 'response should be ok');
+ return response.json();
+}
+
+promise_test(async () => {
+ const stateKey = token();
+ const controller = new AbortController();
+ await fetch(`../resources/infinite-slow-response.py?stateKey=${stateKey}`,
+ {
+ signal: controller.signal,
+ keepalive: true
+ });
+ const before = await fetchJson(`../resources/stash-take.py?key=${stateKey}`);
+ assert_equals(before, 'open', 'connection should be open');
+
+ controller.abort();
+
+ // Spin until the abort completes.
+ while (true) {
+ const after = await fetchJson(`../resources/stash-take.py?key=${stateKey}`);
+ if (after) {
+ // stateKey='open' was removed from the dictionary by the first fetch of
+ // stash-take.py, so we should only ever see the value 'closed' here.
+ assert_equals(after, 'closed', 'connection should have closed');
+ break;
+ }
+ }
+}, 'aborting a keepalive fetch should work');
+
+promise_test(async t => {
+ const key = token();
+ const iframeEl = document.querySelector('iframe');
+
+ // Tell the iframe to start the fetch, and wait until it says it has.
+ await new Promise(resolve => {
+ onmessage = t.step_func(event => {
+ assert_equals(event.data, 'started', 'event data should be "started"');
+ resolve();
+ });
+ iframeEl.contentWindow.postMessage(key, '*');
+ });
+
+ // Detach the context of the fetch.
+ iframeEl.remove();
+
+ sharedController.abort();
+
+ // The abort should not do anything. The connection should stay open. Wait 1
+ // second to give time for the fetch to complete.
+ await new Promise(resolve => t.step_timeout(resolve, 1000));
+
+ const after = await fetchJson(`../resources/stash-take.py?key=${key}`);
+ assert_equals(after, 'on', 'fetch should have completed');
+}, 'aborting a detached keepalive fetch should not do anything');
+</script>
+
+<iframe srcdoc="
+ <!DOCTYPE html>
+ <meta charset=utf-8>
+ <script>
+ onmessage = async event => {
+ const key = event.data;
+ await fetch(
+ `../resources/redirect.py?delay=500&amp;location=` +
+ `../resources/stash-put.py%3fkey=${key}%26value=on`,
+ {
+ signal: parent.sharedController.signal,
+ keepalive: true
+ });
+ parent.postMessage('started', '*');
+ };
+ </script>
+ ">
+</iframe>
diff --git a/testing/web-platform/tests/fetch/api/abort/request.any.js b/testing/web-platform/tests/fetch/api/abort/request.any.js
new file mode 100644
index 0000000000..dcc7803abe
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/abort/request.any.js
@@ -0,0 +1,85 @@
+// META: timeout=long
+// META: global=window,worker
+
+const BODY_FUNCTION_AND_DATA = {
+ arrayBuffer: null,
+ blob: null,
+ formData: new FormData(),
+ json: new Blob(["{}"]),
+ text: null,
+};
+
+for (const [bodyFunction, body] of Object.entries(BODY_FUNCTION_AND_DATA)) {
+ promise_test(async () => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const request = new Request("../resources/data.json", {
+ method: "post",
+ signal,
+ body,
+ });
+
+ controller.abort();
+ await request[bodyFunction]();
+ assert_true(
+ true,
+ `An aborted request should still be able to run ${bodyFunction}()`
+ );
+ }, `Calling ${bodyFunction}() on an aborted request`);
+
+ promise_test(async () => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const request = new Request("../resources/data.json", {
+ method: "post",
+ signal,
+ body,
+ });
+
+ const p = request[bodyFunction]();
+ controller.abort();
+ await p;
+ assert_true(
+ true,
+ `An aborted request should still be able to run ${bodyFunction}()`
+ );
+ }, `Aborting a request after calling ${bodyFunction}()`);
+
+ if (!body) {
+ promise_test(async () => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const request = new Request("../resources/data.json", {
+ method: "post",
+ signal,
+ body,
+ });
+
+ // consuming happens synchronously, so don't wait
+ fetch(request).catch(() => {});
+
+ controller.abort();
+ await request[bodyFunction]();
+ assert_true(
+ true,
+ `An aborted consumed request should still be able to run ${bodyFunction}() when empty`
+ );
+ }, `Calling ${bodyFunction}() on an aborted consumed empty request`);
+ }
+
+ promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const request = new Request("../resources/data.json", {
+ method: "post",
+ signal,
+ body: body || new Blob(["foo"]),
+ });
+
+ // consuming happens synchronously, so don't wait
+ fetch(request).catch(() => {});
+
+ controller.abort();
+ await promise_rejects_js(t, TypeError, request[bodyFunction]());
+ }, `Calling ${bodyFunction}() on an aborted consumed nonempty request`);
+}
diff --git a/testing/web-platform/tests/fetch/api/abort/serviceworker-intercepted.https.html b/testing/web-platform/tests/fetch/api/abort/serviceworker-intercepted.https.html
new file mode 100644
index 0000000000..ed9bc973e8
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/abort/serviceworker-intercepted.https.html
@@ -0,0 +1,212 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Aborting fetch when intercepted by a service worker</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="../../../service-workers/service-worker/resources/test-helpers.sub.js"></script>
+</head>
+<body>
+<script>
+ // Duplicating this resource to make service worker scoping simpler.
+ const SCOPE = '../resources/basic.html';
+ const BODY_METHODS = ['arrayBuffer', 'blob', 'formData', 'json', 'text'];
+
+ const error1 = new Error('error1');
+ error1.name = 'error1';
+
+ async function setupRegistration(t, scope, service_worker) {
+ const reg = await navigator.serviceWorker.register(service_worker, { scope });
+ await wait_for_state(t, reg.installing, 'activated');
+ add_completion_callback(_ => reg.unregister());
+ return reg;
+ }
+
+ promise_test(async t => {
+ const suffix = "?q=aborted-not-intercepted";
+ const scope = SCOPE + suffix;
+ await setupRegistration(t, scope, '../resources/sw-intercept.js');
+ const iframe = await with_iframe(scope);
+ add_completion_callback(_ => iframe.remove());
+ const w = iframe.contentWindow;
+
+ const controller = new w.AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const nextData = new Promise(resolve => {
+ w.navigator.serviceWorker.addEventListener('message', function once(event) {
+ // The message triggered by the iframe's document's fetch
+ // request cannot get dispatched by the time we add the event
+ // listener, so we have to guard against it.
+ if (!event.data.endsWith(suffix)) {
+ w.navigator.serviceWorker.removeEventListener('message', once);
+ resolve(event.data);
+ }
+ })
+ });
+
+ const fetchPromise = w.fetch('data.json', { signal });
+
+ await promise_rejects_dom(t, "AbortError", w.DOMException, fetchPromise);
+
+ await w.fetch('data.json?no-abort');
+
+ assert_true((await nextData).endsWith('?no-abort'), "Aborted request does not go through service worker");
+ }, "Already aborted request does not land in service worker");
+
+ for (const bodyMethod of BODY_METHODS) {
+ promise_test(async t => {
+ const scope = SCOPE + "?q=aborted-" + bodyMethod + "-rejects";
+ await setupRegistration(t, scope, '../resources/sw-intercept.js');
+ const iframe = await with_iframe(scope);
+ add_completion_callback(_ => iframe.remove());
+ const w = iframe.contentWindow;
+
+ const controller = new w.AbortController();
+ const signal = controller.signal;
+
+ const log = [];
+ const response = await w.fetch('data.json', { signal });
+
+ controller.abort();
+
+ const bodyPromise = response[bodyMethod]();
+
+ await Promise.all([
+ bodyPromise.catch(() => log.push(`${bodyMethod}-reject`)),
+ Promise.resolve().then(() => log.push('next-microtask'))
+ ]);
+
+ await promise_rejects_dom(t, "AbortError", w.DOMException, bodyPromise);
+
+ assert_array_equals(log, [`${bodyMethod}-reject`, 'next-microtask']);
+ }, `response.${bodyMethod}() rejects if already aborted`);
+ }
+
+ promise_test(async t => {
+ const scope = SCOPE + "?q=aborted-stream-errors";
+ await setupRegistration(t, scope, '../resources/sw-intercept.js');
+ const iframe = await with_iframe(scope);
+ add_completion_callback(_ => iframe.remove());
+ const w = iframe.contentWindow;
+
+ const controller = new w.AbortController();
+ const signal = controller.signal;
+
+ const response = await w.fetch('data.json', { signal });
+ const reader = response.body.getReader();
+
+ controller.abort();
+
+ await promise_rejects_dom(t, "AbortError", w.DOMException, reader.read());
+ await promise_rejects_dom(t, "AbortError", w.DOMException, reader.closed);
+ }, "Stream errors once aborted.");
+
+ promise_test(async t => {
+ const scope = SCOPE + "?q=aborted-with-abort-reason";
+ await setupRegistration(t, scope, '../resources/sw-intercept.js');
+ const iframe = await with_iframe(scope);
+ add_completion_callback(_ => iframe.remove());
+ const w = iframe.contentWindow;
+
+ const controller = new w.AbortController();
+ const signal = controller.signal;
+
+ const fetchPromise = w.fetch('data.json', { signal });
+
+ controller.abort(error1);
+
+ await promise_rejects_exactly(t, error1, fetchPromise);
+ }, "fetch() rejects with abort reason");
+
+
+ promise_test(async t => {
+ const scope = SCOPE + "?q=aborted-with-abort-reason-in-body";
+ await setupRegistration(t, scope, '../resources/sw-intercept.js');
+ const iframe = await with_iframe(scope);
+ add_completion_callback(_ => iframe.remove());
+ const w = iframe.contentWindow;
+
+ const controller = new w.AbortController();
+ const signal = controller.signal;
+
+ const fetchResponse = await w.fetch('data.json', { signal });
+ const bodyPromise = fetchResponse.body.getReader().read();
+ controller.abort(error1);
+
+ await promise_rejects_exactly(t, error1, bodyPromise);
+ }, "fetch() response body has abort reason");
+
+ promise_test(async t => {
+ const scope = SCOPE + "?q=service-worker-observes-abort-reason";
+ await setupRegistration(t, scope, '../resources/sw-intercept-abort.js');
+ const iframe = await with_iframe(scope);
+ add_completion_callback(_ => iframe.remove());
+ const w = iframe.contentWindow;
+
+ const controller = new w.AbortController();
+ const signal = controller.signal;
+
+ const fetchPromise = w.fetch('data.json', { signal });
+
+ await new Promise(resolve => {
+ w.navigator.serviceWorker.addEventListener('message', t.step_func(event => {
+ assert_equals(event.data, "fetch event has arrived");
+ resolve();
+ }), {once: true});
+ });
+
+ controller.abort(error1);
+
+ await new Promise(resolve => {
+ w.navigator.serviceWorker.addEventListener('message', t.step_func(event => {
+ assert_equals(event.data.message, error1.message);
+ resolve();
+ }), {once: true});
+ });
+
+ await promise_rejects_exactly(t, error1, fetchPromise);
+ }, "Service Worker can observe the fetch abort and associated abort reason");
+
+ promise_test(async t => {
+ let incrementing_error = new Error('error1');
+ incrementing_error.name = 'error1';
+
+ const scope = SCOPE + "?q=serialization-on-abort";
+ await setupRegistration(t, scope, '../resources/sw-intercept-abort.js');
+ const iframe = await with_iframe(scope);
+ add_completion_callback(_ => iframe.remove());
+ const w = iframe.contentWindow;
+
+ const controller = new w.AbortController();
+ const signal = controller.signal;
+
+ const fetchPromise = w.fetch('data.json', { signal });
+
+ await new Promise(resolve => {
+ w.navigator.serviceWorker.addEventListener('message', t.step_func(event => {
+ assert_equals(event.data, "fetch event has arrived");
+ resolve();
+ }), {once: true});
+ });
+
+ controller.abort(incrementing_error);
+
+ const original_error_name = incrementing_error.name;
+
+ incrementing_error.name = 'error2';
+
+ await new Promise(resolve => {
+ w.navigator.serviceWorker.addEventListener('message', t.step_func(event => {
+ assert_equals(event.data.name, original_error_name);
+ resolve();
+ }), {once: true});
+ });
+
+ await promise_rejects_exactly(t, incrementing_error, fetchPromise);
+ }, "Abort reason serialization happens on abort");
+</script>
+</body>
+</html>