summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/fetch/api/abort/general.any.js
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/fetch/api/abort/general.any.js')
-rw-r--r--testing/web-platform/tests/fetch/api/abort/general.any.js572
1 files changed, 572 insertions, 0 deletions
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");