preload-error.sub.html (8604B)
1 <!DOCTYPE html> 2 <meta charset="utf-8"> 3 <head> 4 <title>link rel=preload with various errors/non-errors</title> 5 <meta name="timeout" content="long"> 6 <script src="/resources/testharness.js"></script> 7 <script src="/resources/testharnessreport.js"></script> 8 <script src="resources/preload_helper.js"></script> 9 <meta http-equiv="Content-Security-Policy" 10 content="default-src 'self' http://{{hosts[alt][]}}:{{ports[http][0]}} 'unsafe-inline'"> 11 <script> 12 // For various error/non-error network responses,, this test checks 13 // - load/error events fired on <link rel=preload>, 14 // - load/error events on main requests (e.g. <img>), and 15 // - preloads are reused for main requests 16 // (by verifyLoadedAndNoDoubleDownload()). 17 // 18 // While this test expects <link rel=preload> error events only for network errors 19 // as specified in 20 // https://html.spec.whatwg.org/multipage/links.html#link-type-preload:fetch-and-process-the-linked-resource 21 // https://github.com/whatwg/html/pull/7799 22 // the actual browsers' behavior is different, and the feasibility of changing 23 // the behavior has not yet been investigated. 24 // https://github.com/whatwg/html/issues/1142. 25 26 setup({allow_uncaught_exception: true}); 27 28 function preload(t, as, url, shouldPreloadSucceed) { 29 return new Promise(resolve => { 30 const link = document.createElement('link'); 31 link.setAttribute('rel', 'preload'); 32 link.setAttribute('as', as); 33 link.setAttribute('crossorigin', 'anonymous'); 34 link.setAttribute('href', url); 35 link.onload = t.step_func_done(() => { 36 resolve(); 37 if (!shouldPreloadSucceed) { 38 assert_unreached('preload onload'); 39 } 40 }); 41 link.onerror = t.step_func_done(() => { 42 resolve(); 43 if (shouldPreloadSucceed) { 44 assert_unreached('preload onerror'); 45 } 46 }); 47 document.head.appendChild(link); 48 }); 49 } 50 51 function runTest(api, as, description, shouldPreloadSucceed, shouldMainLoadSucceed, 52 urlWithoutLabel) { 53 description += ' (' + api + ')'; 54 55 const url = new URL(urlWithoutLabel, location.href); 56 url.searchParams.set('label', api); 57 58 const tPreload = async_test(description + ': preload events'); 59 60 promise_test(async t => { 61 let messageOnTimeout = 'timeout'; 62 t.step_timeout(() => t.unreached_func(messageOnTimeout)(), 3000); 63 64 const preloadPromise = preload(tPreload, as, url, shouldPreloadSucceed); 65 66 // The main request is started just after preloading is started and thus 67 // HTTP response headers and errors are not observed yet. 68 let mainPromise; 69 if (api === 'image') { 70 mainPromise = new Promise(t.step_func((resolve, reject) => { 71 const img = document.createElement('img'); 72 img.setAttribute('crossorigin', 'anonymous'); 73 img.onload = resolve; 74 img.onerror = () => reject(new TypeError('img onerror')); 75 img.src = url; 76 document.head.appendChild(img); 77 })); 78 } else if (api === 'style') { 79 mainPromise = new Promise(t.step_func((resolve, reject) => { 80 const link = document.createElement('link'); 81 link.setAttribute('rel', 'stylesheet'); 82 link.setAttribute('crossorigin', 'anonymous'); 83 link.onload = resolve; 84 link.onerror = () => reject(new TypeError('link rel=stylesheet onerror')); 85 link.href = url; 86 document.head.appendChild(link); 87 })); 88 } else if (api === 'script') { 89 mainPromise = new Promise(t.step_func((resolve, reject) => { 90 const script = document.createElement('script'); 91 script.setAttribute('crossorigin', 'anonymous'); 92 script.onload = resolve; 93 script.onerror = () => reject(new TypeError('script onerror')); 94 script.src = url; 95 document.head.appendChild(script); 96 })); 97 } else if (api === 'xhr') { 98 mainPromise = new Promise(t.step_func((resolve, reject) => { 99 const xhr = new XMLHttpRequest(); 100 xhr.open('GET', url, true); 101 xhr.onload = resolve; 102 xhr.onerror = () => reject(new TypeError('XHR onerror')); 103 xhr.onabort = t.unreached_func('XHR onabort'); 104 xhr.send(); 105 })); 106 } else if (api === 'fetch') { 107 mainPromise = fetch(url) 108 .then(r => { 109 messageOnTimeout = 'fetch() completed but text() timeout'; 110 return r.text(); 111 }); 112 } else { 113 throw new Error('Unexpected api: ' + api); 114 } 115 116 if (shouldMainLoadSucceed) { 117 await mainPromise; 118 } else { 119 await promise_rejects_js(t, TypeError, mainPromise); 120 } 121 122 // Wait also for <link rel=preload> events. 123 // This deflakes `verifyLoadedAndNoDoubleDownload` results. 124 await preloadPromise; 125 126 verifyLoadedAndNoDoubleDownload(url); 127 }, description + ': main'); 128 } 129 130 const tests = { 131 'image': { 132 url: '/preload/resources/square.png', 133 as: 'image', 134 mainLoadWillFailIf404Returned: false 135 }, 136 'style': { 137 url: '/preload/resources/dummy.css', 138 as: 'style', 139 140 // https://html.spec.whatwg.org/multipage/semantics.html#default-fetch-and-process-the-linked-resource 141 mainLoadWillFailIf404Returned: true 142 }, 143 'script': { 144 url: '/preload/resources/dummy.js', 145 as: 'script', 146 147 // https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-classic-script 148 mainLoadWillFailIf404Returned: true 149 }, 150 'xhr': { 151 url: '/preload/resources/dummy.xml', 152 as: 'fetch', 153 mainLoadWillFailIf404Returned: false 154 }, 155 'fetch': { 156 url: '/preload/resources/dummy.xml', 157 as: 'fetch', 158 mainLoadWillFailIf404Returned: false 159 } 160 }; 161 162 for (const api in tests) { 163 const url = tests[api].url; 164 const as = tests[api].as; 165 166 // Successful response. 167 runTest(api, as, 'success', true, true, url); 168 169 // Successful response: non-ok status is not considered as a network error, 170 // but can fire error events on main requests. 171 runTest(api, as, '404', true, !tests[api].mainLoadWillFailIf404Returned, 172 url + '?pipe=status(404)'); 173 174 // Successful response: Successful CORS check. 175 runTest(api, as, 'CORS', true, true, 176 'http://{{hosts[alt][]}}:{{ports[http][0]}}' + url + 177 '?pipe=header(Access-Control-Allow-Origin,*)'); 178 179 // A network error: Failed CORS check. 180 runTest(api, as, 'CORS-error', false, false, 181 'http://{{hosts[alt][]}}:{{ports[http][0]}}' + url); 182 183 // A network error: Failed CSP check on redirect. 184 runTest(api, as, 'CSP-error', false, false, 185 '/common/redirect.py?location=http://{{hosts[alt][]}}:{{ports[http][1]}}' + 186 url + '?pipe=header(Access-Control-Allow-Origin,*)'); 187 } 188 189 // -------------------------------- 190 // Content error. 191 192 // Successful response with corrupted image data. 193 // Not a network error, but can fire error events for images: 194 // https://html.spec.whatwg.org/multipage/images.html#update-the-image-data 195 runTest('image', 'image', 'Decode-error', true, false, 196 '/preload/resources/dummy.css?pipe=header(Content-Type,image/png)'); 197 runTest('style', 'style', 'Decode-error', true, true, 198 '/preload/resources/dummy.xml?pipe=header(Content-Type,text/css)'); 199 runTest('script', 'script', 'Decode-error', true, true, 200 '/preload/resources/dummy.xml?pipe=header(Content-Type,text/javascript)'); 201 202 // -------------------------------- 203 // MIME Type error. 204 // Some MIME type mismatches are not network errors. 205 runTest('image', 'image', 'MIME-error', true, true, 206 '/preload/resources/square.png?pipe=header(Content-Type,text/notimage)'); 207 runTest('script', 'script', 'MIME-error', true, true, 208 '/preload/resources/dummy.css?pipe=header(Content-Type,text/notjavascript)'); 209 // But they fire error events for <link rel=stylesheet>s. 210 // https://html.spec.whatwg.org/multipage/links.html#link-type-stylesheet:process-the-linked-resource 211 runTest('style', 'style', 'MIME-error', true, false, 212 '/preload/resources/dummy.js?pipe=header(Content-Type,not/css)'); 213 214 // Other MIME type mismatches are network errors, due to: 215 // https://fetch.spec.whatwg.org/#should-response-to-request-be-blocked-due-to-mime-type? 216 runTest('script', 'script', 'MIME-blocked', false, false, 217 '/preload/resources/dummy.css?pipe=header(Content-Type,image/not-javascript)'); 218 // https://fetch.spec.whatwg.org/#should-response-to-request-be-blocked-due-to-nosniff? 219 runTest('style', 'style', 'MIME-blocked-nosniff', false, false, 220 '/preload/resources/dummy.js?pipe=header(Content-Type,not/css)|header(X-Content-Type-Options,nosniff)'); 221 runTest('script', 'script', 'MIME-blocked-nosniff', false, false, 222 '/preload/resources/dummy.css?pipe=header(Content-Type,text/notjavascript)|header(X-Content-Type-Options,nosniff)'); 223 </script>