Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

// 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");