Skip to content

Commit

Permalink
Fetch: Add tests for AbortSignal's abort reason (#35374)
Browse files Browse the repository at this point in the history
Add test cases to check the functionality of AbortSignal's abort reason when aborting fetch, including serialization and that the service worker can observe the reason.

See whatwg/fetch#1343 for accompanying spec changes.
  • Loading branch information
nidhijaju authored Oct 6, 2022
1 parent 820dda9 commit 0e5f85c
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 5 deletions.
29 changes: 29 additions & 0 deletions fetch/api/abort/general.any.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@

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 = [];
Expand All @@ -31,6 +34,16 @@ promise_test(async t => {
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;
Expand Down Expand Up @@ -91,6 +104,22 @@ promise_test(async t => {
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;
Expand Down
99 changes: 94 additions & 5 deletions fetch/api/abort/serviceworker-intercepted.https.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@
const SCOPE = '../resources/basic.html';
const BODY_METHODS = ['arrayBuffer', 'blob', 'formData', 'json', 'text'];

async function setupRegistration(t, scope) {
const reg = await navigator.serviceWorker.register('../resources/sw-intercept.js', { scope });
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;
Expand All @@ -23,7 +26,7 @@
promise_test(async t => {
const suffix = "?q=aborted-not-intercepted";
const scope = SCOPE + suffix;
await setupRegistration(t, scope);
await setupRegistration(t, scope, '../resources/sw-intercept.js');
const iframe = await with_iframe(scope);
add_completion_callback(_ => iframe.remove());
const w = iframe.contentWindow;
Expand Down Expand Up @@ -56,7 +59,7 @@
for (const bodyMethod of BODY_METHODS) {
promise_test(async t => {
const scope = SCOPE + "?q=aborted-" + bodyMethod + "-rejects";
await setupRegistration(t, scope);
await setupRegistration(t, scope, '../resources/sw-intercept.js');
const iframe = await with_iframe(scope);
add_completion_callback(_ => iframe.remove());
const w = iframe.contentWindow;
Expand Down Expand Up @@ -84,7 +87,7 @@

promise_test(async t => {
const scope = SCOPE + "?q=aborted-stream-errors";
await setupRegistration(t, scope);
await setupRegistration(t, scope, '../resources/sw-intercept.js');
const iframe = await with_iframe(scope);
add_completion_callback(_ => iframe.remove());
const w = iframe.contentWindow;
Expand All @@ -100,6 +103,92 @@
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 = await 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=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>
19 changes: 19 additions & 0 deletions fetch/api/resources/sw-intercept-abort.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
async function messageClient(clientId, message) {
const client = await clients.get(clientId);
client.postMessage(message);
}

addEventListener('fetch', event => {
let resolve;
const promise = new Promise(r => resolve = r);

function onAborted() {
messageClient(event.clientId, event.request.signal.reason);
resolve();
}

messageClient(event.clientId, 'fetch event has arrived');

event.respondWith(promise.then(() => new Response('hello')));
event.request.signal.addEventListener('abort', onAborted);
});

0 comments on commit 0e5f85c

Please sign in to comment.