Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Service worker fetch abort tests #7674

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>Fetch event request abort signal</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="resources/test-helpers.sub.js"></script>
<script>
const SW_URL = 'resources/abort-monitor-worker.js';
const SCOPE = 'resources/blank.html';

function raf() {
return new Promise(r => requestAnimationFrame(r));
}

async function nextFrame() {
await raf();
await raf();
}

function broadcastResponse(bc) {
return new Promise(resolve => {
bc.onmessage = event => {
resolve(event.data);
};
});
}

function reset() {
for (const iframe of [...document.querySelectorAll('.test-iframe')]) {
iframe.remove();
}
return navigator.serviceWorker.getRegistrations().then(registrations => {
return Promise.all(registrations.map(r => r.unregister()));
});
}

function abortTest(callback, name) {
return promise_test(async t => {
await reset();

// Register and activate.
const reg = await service_worker_unregister_and_register(t, SW_URL, SCOPE);
await wait_for_state(t, reg.installing, 'activated');

// Create a channel for communication with the service worker.
const channelName = Math.random() + '';
const channel = new BroadcastChannel(channelName);

// Promise for the next broadcast (from the service worker).
const bcResponse = broadcastResponse(channel);

// URL to fetch.
// This will always be caught by the service worker.
// 'whatever' doesn't need to exist.
const fetchUrl = new URL('whatever', location);
fetchUrl.searchParams.set('bc', channelName);

await callback(t, channel, bcResponse, fetchUrl);
}, name);
}

abortTest(async (t, channel, bcResponse, fetchUrl) => {
fetchUrl.searchParams.set('wait-for-abort', '');

const frame = await with_iframe(SCOPE);
const controller = new AbortController();
const signal = controller.signal;

frame.contentWindow.fetch(fetchUrl, { signal });
await nextFrame();
controller.abort();
await nextFrame();
channel.postMessage('abort-expected');

assert_equals(await bcResponse, 'aborted');
}, 'SW signal reacts to fetch abort');

abortTest(async (t, channel, bcResponse, fetchUrl) => {
fetchUrl.searchParams.set('wait-for-abort', '');

const frame = await with_iframe(SCOPE);
const xhr = new frame.contentWindow.XMLHttpRequest();
xhr.open('GET', fetchUrl);
xhr.send();

await nextFrame();
xhr.abort();
await nextFrame();
channel.postMessage('abort-expected');

assert_equals(await bcResponse, 'aborted');
}, 'SW signal reacts to XHR abort');

abortTest(async (t, channel, bcResponse, fetchUrl) => {
fetchUrl.searchParams.set('wait-for-abort', '');

const frame = await with_iframe(SCOPE);

let img = new frame.contentWindow.Image();
img.src = fetchUrl;
frame.contentDocument.body.appendChild(img);
img = undefined;

await nextFrame();
frame.remove();
await nextFrame();
channel.postMessage('abort-expected');

assert_equals(await bcResponse, 'aborted');
}, 'SW signal reacts to fetch group termination');

abortTest(async (t, channel, bcResponse, fetchUrl) => {
fetchUrl.searchParams.set('wait-for-abort', '');
// The fetch URL isn't 'whatever' in this case, it's the SW scope.
fetchUrl.pathname = new URL(SCOPE, location).pathname;

const frame = document.createElement('iframe');
frame.src = fetchUrl;
document.body.append(frame);

await nextFrame();
frame.remove();
await nextFrame();
channel.postMessage('abort-expected');

assert_equals(await bcResponse, 'aborted');
}, 'SW signal reacts to navigation termination');

abortTest(async (t, channel, bcResponse, fetchUrl) => {
fetchUrl.searchParams.set('abort-not-expected', '');

const frame = await with_iframe(SCOPE);
const response = await frame.contentWindow.fetch(fetchUrl);
const data = await response.json();

assert_true(data.ok);

channel.postMessage('ping');

assert_equals(await bcResponse, 'aborted-false');
}, `SW signal doesn't react to successful fetch`);

promise_test(async t => {
await reset();
}, 'Cleanup');
</script>

73 changes: 73 additions & 0 deletions service-workers/service-worker/navigation-preload/abort.https.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>Fetch event request abort signal</title>
<script src="/common/utils.js"></script>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="../resources/test-helpers.sub.js"></script>
<script>
const SW_URL = 'resources/preload-abort-worker.js?pipe=header(Service-Worker-Allowed,/)';
let abortKey;

function raf() {
return new Promise(r => requestAnimationFrame(r));
}

async function nextFrame() {
await raf();
await raf();
}

function broadcastResponse(bc) {
return new Promise(resolve => {
bc.onmessage = event => {
resolve(event.data);
};
});
}

async function reset() {
// This terminates a given infinite-slow-response.py response
if (abortKey) await fetch(`/fetch/api/resources/stash-put.py?key=${abortKey}&value=close`);
abortKey = null;

for (const iframe of [...document.querySelectorAll('.test-iframe')]) {
iframe.remove();
}
const registrations = await navigator.serviceWorker.getRegistrations();
return Promise.all(registrations.map(r => r.unregister()));
}

promise_test(async t => {
const scope = '/fetch/api/resources/infinite-slow-response.py';
await reset();
const reg = await service_worker_unregister_and_register(t, SW_URL, scope);

assert_true('navigationPreload' in reg, 'navigationPreload exists');

await wait_for_state(t, reg.installing, 'activated');
const channelName = Math.random() + '';
const channel = new BroadcastChannel(channelName);
const bcResponse = broadcastResponse(channel);

const fetchUrl = new URL(scope, location);
abortKey = token();
fetchUrl.searchParams.set('abortKey', abortKey);
fetchUrl.searchParams.set('bc', channelName);

const frame = document.createElement('iframe');
frame.src = fetchUrl;
document.body.append(frame);

await nextFrame();
frame.remove();
await nextFrame();

assert_equals(await bcResponse, 'rejected-aborterror');
}, 'Preload response aborts when navigation terminates');

promise_test(async t => {
await reset();
}, 'Cleanup');
</script>

Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
skipWaiting();

addEventListener('activate', event => {
event.waitUntil(self.registration.navigationPreload.enable());
});

addEventListener('fetch', event => {
const url = new URL(event.request.url);
const channelName = url.searchParams.get('bc');
const bc = new BroadcastChannel(channelName);

const timeout = new Promise(resolve => {
bc.addEventListener('message', function messageListener(event) {
if (event.data != 'abort-expected') return;
resolve('timeout');
bc.removeEventListener('message', messageListener);
});
});

event.waitUntil(
Promise.race([
(async function test() {
if (!event.preloadResponse) {
return 'no-preload-response';
}

try {
await event.preloadResponse;
return 'resolved';
}
catch (err) {
if (err.name == 'AbortError') {
return 'rejected-aborterror';
}
return 'rejected-other-error';
}
}()),
timeout
]).then(
val => bc.postMessage(val),
err => bc.postMessage(`Err: ${err.message}`)
)
);

event.respondWith(event.preloadResponse);
});
71 changes: 71 additions & 0 deletions service-workers/service-worker/resources/abort-monitor-worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
skipWaiting();

addEventListener('fetch', event => {
const url = new URL(event.request.url);

if (url.searchParams.has('wait-for-abort')) {
waitForAbort(event);
}
if (url.searchParams.has('abort-not-expected')) {
abortNotExpected(event);
}
});

function abortNotExpected(event) {
const url = new URL(event.request.url);
const channelName = url.searchParams.get('bc');
const bc = new BroadcastChannel(channelName);
const { request } = event;

event.respondWith(
new Response(JSON.stringify({ok: true}))
);

event.waitUntil(new Promise(resolve => {
bc.onmessage = () => {
resolve();
if (!request.signal) {
bc.postMessage('no-signal');
return;
}
bc.postMessage(`aborted-${request.signal.aborted}`);
};
}));
}

function waitForAbort(event) {
const url = new URL(event.request.url);
const channelName = url.searchParams.get('bc');
const bc = new BroadcastChannel(channelName);
const { request } = event;

const timeout = new Promise(resolve => {
bc.addEventListener('message', function messageListener(event) {
if (event.data != 'abort-expected') return;
resolve('timeout');
bc.removeEventListener('message', messageListener);
});
});

event.waitUntil(
(async function test() {
if (!request.signal) {
return 'no-signal';
}

if (request.signal.aborted) {
return 'already-aborted';
}

return Promise.race([
timeout,
new Promise(r => {
request.signal.onabort = () => r('aborted');
})
]);
}()).then(
val => bc.postMessage(val),
err => bc.postMessage(`Err: ${err.message}`)
)
);
}