diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /testing/web-platform/tests/fetch/api/abort | |
parent | Initial commit. (diff) | |
download | firefox-43a97878ce14b72f0981164f87f2e35e14151312.tar.xz firefox-43a97878ce14b72f0981164f87f2e35e14151312.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/fetch/api/abort')
6 files changed, 1027 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..e9e8e93d30 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/abort/general.any.js @@ -0,0 +1,571 @@ +// 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', + 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, ['clone-aborted', 'original-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&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> |