<!DOCTYPE html>
<meta charset="utf-8">
<head>
<title>link rel=preload with various errors/non-errors</title>
<meta name="timeout" content="long">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="resources/preload_helper.js"></script>
<meta http-equiv="Content-Security-Policy"
content="default-src 'self' http://{{hosts[alt][]}}:{{ports[http][0]}} 'unsafe-inline'">
<script>
// For various error/non-error network responses,, this test checks
// - load/error events fired on <link rel=preload>,
// - load/error events on main requests (e.g. <img>), and
// - preloads are reused for main requests
// (by verifyLoadedAndNoDoubleDownload()).
//
// While this test expects <link rel=preload> error events only for network errors
// as specified in
// https://html.spec.whatwg.org/multipage/links.html#link-type-preload:fetch-and-process-the-linked-resource
// https://github.com/whatwg/html/pull/7799
// the actual browsers' behavior is different, and the feasibility of changing
// the behavior has not yet been investigated.
// https://github.com/whatwg/html/issues/1142.
setup({allow_uncaught_exception: true});
function preload(t, as, url, shouldPreloadSucceed) {
return new Promise(resolve => {
const link = document.createElement('link');
link.setAttribute('rel', 'preload');
link.setAttribute('as', as);
link.setAttribute('crossorigin', 'anonymous');
link.setAttribute('href', url);
link.onload = t.step_func_done(() => {
resolve();
if (!shouldPreloadSucceed) {
assert_unreached('preload onload');
}
});
link.onerror = t.step_func_done(() => {
resolve();
if (shouldPreloadSucceed) {
assert_unreached('preload onerror');
}
});
document.head.appendChild(link);
});
}
function runTest(api, as, description, shouldPreloadSucceed, shouldMainLoadSucceed,
urlWithoutLabel) {
description += ' (' + api + ')';
const url = new URL(urlWithoutLabel, location.href);
url.searchParams.set('label', api);
const tPreload = async_test(description + ': preload events');
promise_test(async t => {
let messageOnTimeout = 'timeout';
t.step_timeout(() => t.unreached_func(messageOnTimeout)(), 3000);
const preloadPromise = preload(tPreload, as, url, shouldPreloadSucceed);
// The main request is started just after preloading is started and thus
// HTTP response headers and errors are not observed yet.
let mainPromise;
if (api === 'image') {
mainPromise = new Promise(t.step_func((resolve, reject) => {
const img = document.createElement('img');
img.setAttribute('crossorigin', 'anonymous');
img.onload = resolve;
img.onerror = () => reject(new TypeError('img onerror'));
img.src = url;
document.head.appendChild(img);
}));
} else if (api === 'style') {
mainPromise = new Promise(t.step_func((resolve, reject) => {
const link = document.createElement('link');
link.setAttribute('rel', 'stylesheet');
link.setAttribute('crossorigin', 'anonymous');
link.onload = resolve;
link.onerror = () => reject(new TypeError('link rel=stylesheet onerror'));
link.href = url;
document.head.appendChild(link);
}));
} else if (api === 'script') {
mainPromise = new Promise(t.step_func((resolve, reject) => {
const script = document.createElement('script');
script.setAttribute('crossorigin', 'anonymous');
script.onload = resolve;
script.onerror = () => reject(new TypeError('script onerror'));
script.src = url;
document.head.appendChild(script);
}));
} else if (api === 'xhr') {
mainPromise = new Promise(t.step_func((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onload = resolve;
xhr.onerror = () => reject(new TypeError('XHR onerror'));
xhr.onabort = t.unreached_func('XHR onabort');
xhr.send();
}));
} else if (api === 'fetch') {
mainPromise = fetch(url)
.then(r => {
messageOnTimeout = 'fetch() completed but text() timeout';
return r.text();
});
} else {
throw new Error('Unexpected api: ' + api);
}
if (shouldMainLoadSucceed) {
await mainPromise;
} else {
await promise_rejects_js(t, TypeError, mainPromise);
}
// Wait also for <link rel=preload> events.
// This deflakes `verifyLoadedAndNoDoubleDownload` results.
await preloadPromise;
verifyLoadedAndNoDoubleDownload(url);
}, description + ': main');
}
const tests = {
'image': {
url: '/preload/resources/square.png',
as: 'image',
mainLoadWillFailIf404Returned: false
},
'style': {
url: '/preload/resources/dummy.css',
as: 'style',
// https://html.spec.whatwg.org/multipage/semantics.html#default-fetch-and-process-the-linked-resource
mainLoadWillFailIf404Returned: true
},
'script': {
url: '/preload/resources/dummy.js',
as: 'script',
// https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-classic-script
mainLoadWillFailIf404Returned: true
},
'xhr': {
url: '/preload/resources/dummy.xml',
as: 'fetch',
mainLoadWillFailIf404Returned: false
},
'fetch': {
url: '/preload/resources/dummy.xml',
as: 'fetch',
mainLoadWillFailIf404Returned: false
}
};
for (const api in tests) {
const url = tests[api].url;
const as = tests[api].as;
// Successful response.
runTest(api, as, 'success', true, true, url);
// Successful response: non-ok status is not considered as a network error,
// but can fire error events on main requests.
runTest(api, as, '404', true, !tests[api].mainLoadWillFailIf404Returned,
url + '?pipe=status(404)');
// Successful response: Successful CORS check.
runTest(api, as, 'CORS', true, true,
'http://{{hosts[alt][]}}:{{ports[http][0]}}' + url +
'?pipe=header(Access-Control-Allow-Origin,*)');
// A network error: Failed CORS check.
runTest(api, as, 'CORS-error', false, false,
'http://{{hosts[alt][]}}:{{ports[http][0]}}' + url);
// A network error: Failed CSP check on redirect.
runTest(api, as, 'CSP-error', false, false,
'/common/redirect.py?location=http://{{hosts[alt][]}}:{{ports[http][1]}}' +
url + '?pipe=header(Access-Control-Allow-Origin,*)');
}
// --------------------------------
// Content error.
// Successful response with corrupted image data.
// Not a network error, but can fire error events for images:
// https://html.spec.whatwg.org/multipage/images.html#update-the-image-data
runTest('image', 'image', 'Decode-error', true, false,
'/preload/resources/dummy.css?pipe=header(Content-Type,image/png)');
runTest('style', 'style', 'Decode-error', true, true,
'/preload/resources/dummy.xml?pipe=header(Content-Type,text/css)');
runTest('script', 'script', 'Decode-error', true, true,
'/preload/resources/dummy.xml?pipe=header(Content-Type,text/javascript)');
// --------------------------------
// MIME Type error.
// Some MIME type mismatches are not network errors.
runTest('image', 'image', 'MIME-error', true, true,
'/preload/resources/square.png?pipe=header(Content-Type,text/notimage)');
runTest('script', 'script', 'MIME-error', true, true,
'/preload/resources/dummy.css?pipe=header(Content-Type,text/notjavascript)');
// But they fire error events for <link rel=stylesheet>s.
// https://html.spec.whatwg.org/multipage/links.html#link-type-stylesheet:process-the-linked-resource
runTest('style', 'style', 'MIME-error', true, false,
'/preload/resources/dummy.js?pipe=header(Content-Type,not/css)');
// Other MIME type mismatches are network errors, due to:
// https://fetch.spec.whatwg.org/#should-response-to-request-be-blocked-due-to-mime-type?
runTest('script', 'script', 'MIME-blocked', false, false,
'/preload/resources/dummy.css?pipe=header(Content-Type,image/not-javascript)');
// https://fetch.spec.whatwg.org/#should-response-to-request-be-blocked-due-to-nosniff?
runTest('style', 'style', 'MIME-blocked-nosniff', false, false,
'/preload/resources/dummy.js?pipe=header(Content-Type,not/css)|header(X-Content-Type-Options,nosniff)');
runTest('script', 'script', 'MIME-blocked-nosniff', false, false,
'/preload/resources/dummy.css?pipe=header(Content-Type,text/notjavascript)|header(X-Content-Type-Options,nosniff)');
</script>