general.any.js (18428B)
1 // META: timeout=long 2 // META: global=window,worker 3 // META: script=/common/utils.js 4 // META: script=/common/get-host-info.sub.js 5 // META: script=../request/request-error.js 6 7 const BODY_METHODS = ['arrayBuffer', 'blob', 'bytes', 'formData', 'json', 'text']; 8 9 const error1 = new Error('error1'); 10 error1.name = 'error1'; 11 12 // This is used to close connections that weren't correctly closed during the tests, 13 // otherwise you can end up running out of HTTP connections. 14 let requestAbortKeys = []; 15 16 function abortRequests() { 17 const keys = requestAbortKeys; 18 requestAbortKeys = []; 19 return Promise.all( 20 keys.map(key => fetch(`../resources/stash-put.py?key=${key}&value=close`)) 21 ); 22 } 23 24 const hostInfo = get_host_info(); 25 const urlHostname = hostInfo.REMOTE_HOST; 26 27 promise_test(async t => { 28 const controller = new AbortController(); 29 const signal = controller.signal; 30 controller.abort(); 31 32 const fetchPromise = fetch('../resources/data.json', { signal }); 33 34 await promise_rejects_dom(t, "AbortError", fetchPromise); 35 }, "Aborting rejects with AbortError"); 36 37 promise_test(async t => { 38 const controller = new AbortController(); 39 const signal = controller.signal; 40 controller.abort(error1); 41 42 const fetchPromise = fetch('../resources/data.json', { signal }); 43 44 await promise_rejects_exactly(t, error1, fetchPromise, 'fetch() should reject with abort reason'); 45 }, "Aborting rejects with abort reason"); 46 47 promise_test(async t => { 48 const controller = new AbortController(); 49 const signal = controller.signal; 50 controller.abort(); 51 52 const url = new URL('../resources/data.json', location); 53 url.hostname = urlHostname; 54 55 const fetchPromise = fetch(url, { 56 signal, 57 mode: 'no-cors' 58 }); 59 60 await promise_rejects_dom(t, "AbortError", fetchPromise); 61 }, "Aborting rejects with AbortError - no-cors"); 62 63 // Test that errors thrown from the request constructor take priority over abort errors. 64 // badRequestArgTests is from response-error.js 65 for (const { args, testName } of badRequestArgTests) { 66 promise_test(async t => { 67 try { 68 // If this doesn't throw, we'll effectively skip the test. 69 // It'll fail properly in ../request/request-error.html 70 new Request(...args); 71 } 72 catch (err) { 73 const controller = new AbortController(); 74 controller.abort(); 75 76 // Add signal to 2nd arg 77 args[1] = args[1] || {}; 78 args[1].signal = controller.signal; 79 await promise_rejects_js(t, TypeError, fetch(...args)); 80 } 81 }, `TypeError from request constructor takes priority - ${testName}`); 82 } 83 84 test(() => { 85 const request = new Request(''); 86 assert_true(Boolean(request.signal), "Signal member is present & truthy"); 87 assert_equals(request.signal.constructor, AbortSignal); 88 }, "Request objects have a signal property"); 89 90 promise_test(async t => { 91 const controller = new AbortController(); 92 const signal = controller.signal; 93 controller.abort(); 94 95 const request = new Request('../resources/data.json', { signal }); 96 97 assert_true(Boolean(request.signal), "Signal member is present & truthy"); 98 assert_equals(request.signal.constructor, AbortSignal); 99 assert_not_equals(request.signal, signal, 'Request has a new signal, not a reference'); 100 assert_true(request.signal.aborted, `Request's signal has aborted`); 101 102 const fetchPromise = fetch(request); 103 104 await promise_rejects_dom(t, "AbortError", fetchPromise); 105 }, "Signal on request object"); 106 107 promise_test(async t => { 108 const controller = new AbortController(); 109 const signal = controller.signal; 110 controller.abort(error1); 111 112 const request = new Request('../resources/data.json', { signal }); 113 114 assert_not_equals(request.signal, signal, 'Request has a new signal, not a reference'); 115 assert_true(request.signal.aborted, `Request's signal has aborted`); 116 assert_equals(request.signal.reason, error1, `Request's signal's abort reason is error1`); 117 118 const fetchPromise = fetch(request); 119 120 await promise_rejects_exactly(t, error1, fetchPromise, "fetch() should reject with abort reason"); 121 }, "Signal on request object should also have abort reason"); 122 123 promise_test(async t => { 124 const controller = new AbortController(); 125 const signal = controller.signal; 126 controller.abort(); 127 128 const request = new Request('../resources/data.json', { signal }); 129 const requestFromRequest = new Request(request); 130 131 const fetchPromise = fetch(requestFromRequest); 132 133 await promise_rejects_dom(t, "AbortError", fetchPromise); 134 }, "Signal on request object created from request object"); 135 136 promise_test(async t => { 137 const controller = new AbortController(); 138 const signal = controller.signal; 139 controller.abort(); 140 141 const request = new Request('../resources/data.json'); 142 const requestFromRequest = new Request(request, { signal }); 143 144 const fetchPromise = fetch(requestFromRequest); 145 146 await promise_rejects_dom(t, "AbortError", fetchPromise); 147 }, "Signal on request object created from request object, with signal on second request"); 148 149 promise_test(async t => { 150 const controller = new AbortController(); 151 const signal = controller.signal; 152 controller.abort(); 153 154 const request = new Request('../resources/data.json', { signal: new AbortController().signal }); 155 const requestFromRequest = new Request(request, { signal }); 156 157 const fetchPromise = fetch(requestFromRequest); 158 159 await promise_rejects_dom(t, "AbortError", fetchPromise); 160 }, "Signal on request object created from request object, with signal on second request overriding another"); 161 162 promise_test(async t => { 163 const controller = new AbortController(); 164 const signal = controller.signal; 165 controller.abort(); 166 167 const request = new Request('../resources/data.json', { signal }); 168 169 const fetchPromise = fetch(request, {method: 'POST'}); 170 171 await promise_rejects_dom(t, "AbortError", fetchPromise); 172 }, "Signal retained after unrelated properties are overridden by fetch"); 173 174 promise_test(async t => { 175 const controller = new AbortController(); 176 const signal = controller.signal; 177 controller.abort(); 178 179 const request = new Request('../resources/data.json', { signal }); 180 181 const data = await fetch(request, { signal: null }).then(r => r.json()); 182 assert_equals(data.key, 'value', 'Fetch fully completes'); 183 }, "Signal removed by setting to null"); 184 185 promise_test(async t => { 186 const controller = new AbortController(); 187 const signal = controller.signal; 188 controller.abort(); 189 190 const log = []; 191 192 await Promise.all([ 193 fetch('../resources/data.json', { signal }).then( 194 () => assert_unreached("Fetch must not resolve"), 195 () => log.push('fetch-reject') 196 ), 197 Promise.resolve().then(() => log.push('next-microtask')) 198 ]); 199 200 assert_array_equals(log, ['fetch-reject', 'next-microtask']); 201 }, "Already aborted signal rejects immediately"); 202 203 promise_test(async t => { 204 const controller = new AbortController(); 205 const signal = controller.signal; 206 controller.abort(); 207 208 const request = new Request('../resources/data.json', { 209 signal, 210 method: 'POST', 211 body: 'foo', 212 headers: { 'Content-Type': 'text/plain' } 213 }); 214 215 await fetch(request).catch(() => {}); 216 217 assert_true(request.bodyUsed, "Body has been used"); 218 }, "Request is still 'used' if signal is aborted before fetching"); 219 220 for (const bodyMethod of BODY_METHODS) { 221 promise_test(async t => { 222 const controller = new AbortController(); 223 const signal = controller.signal; 224 225 const log = []; 226 const response = await fetch('../resources/data.json', { signal }); 227 228 controller.abort(); 229 230 const bodyPromise = response[bodyMethod](); 231 232 await Promise.all([ 233 bodyPromise.catch(() => log.push(`${bodyMethod}-reject`)), 234 Promise.resolve().then(() => log.push('next-microtask')) 235 ]); 236 237 await promise_rejects_dom(t, "AbortError", bodyPromise); 238 239 assert_array_equals(log, [`${bodyMethod}-reject`, 'next-microtask']); 240 }, `response.${bodyMethod}() rejects if already aborted`); 241 } 242 243 promise_test(async (t) => { 244 const controller = new AbortController(); 245 const signal = controller.signal; 246 247 const res = await fetch('../resources/data.json', { signal }); 248 controller.abort(); 249 250 await promise_rejects_dom(t, 'AbortError', res.text()); 251 await promise_rejects_dom(t, 'AbortError', res.text()); 252 }, 'Call text() twice on aborted response'); 253 254 promise_test(async t => { 255 await abortRequests(); 256 257 const controller = new AbortController(); 258 const signal = controller.signal; 259 const stateKey = token(); 260 const abortKey = token(); 261 requestAbortKeys.push(abortKey); 262 controller.abort(); 263 264 await fetch(`../resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, { signal }).catch(() => {}); 265 266 // I'm hoping this will give the browser enough time to (incorrectly) make the request 267 // above, if it intends to. 268 await fetch('../resources/data.json').then(r => r.json()); 269 270 const response = await fetch(`../resources/stash-take.py?key=${stateKey}`); 271 const data = await response.json(); 272 273 assert_equals(data, null, "Request hasn't been made to the server"); 274 }, "Already aborted signal does not make request"); 275 276 promise_test(async t => { 277 await abortRequests(); 278 279 const controller = new AbortController(); 280 const signal = controller.signal; 281 controller.abort(); 282 283 const fetches = []; 284 285 for (let i = 0; i < 3; i++) { 286 const abortKey = token(); 287 requestAbortKeys.push(abortKey); 288 289 fetches.push( 290 fetch(`../resources/infinite-slow-response.py?${i}&abortKey=${abortKey}`, { signal }) 291 ); 292 } 293 294 for (const fetchPromise of fetches) { 295 await promise_rejects_dom(t, "AbortError", fetchPromise); 296 } 297 }, "Already aborted signal can be used for many fetches"); 298 299 promise_test(async t => { 300 await abortRequests(); 301 302 const controller = new AbortController(); 303 const signal = controller.signal; 304 305 await fetch('../resources/data.json', { signal }).then(r => r.json()); 306 307 controller.abort(); 308 309 const fetches = []; 310 311 for (let i = 0; i < 3; i++) { 312 const abortKey = token(); 313 requestAbortKeys.push(abortKey); 314 315 fetches.push( 316 fetch(`../resources/infinite-slow-response.py?${i}&abortKey=${abortKey}`, { signal }) 317 ); 318 } 319 320 for (const fetchPromise of fetches) { 321 await promise_rejects_dom(t, "AbortError", fetchPromise); 322 } 323 }, "Signal can be used to abort other fetches, even if another fetch succeeded before aborting"); 324 325 promise_test(async t => { 326 await abortRequests(); 327 328 const controller = new AbortController(); 329 const signal = controller.signal; 330 const stateKey = token(); 331 const abortKey = token(); 332 requestAbortKeys.push(abortKey); 333 334 await fetch(`../resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, { signal }); 335 336 const beforeAbortResult = await fetch(`../resources/stash-take.py?key=${stateKey}`).then(r => r.json()); 337 assert_equals(beforeAbortResult, "open", "Connection is open"); 338 339 controller.abort(); 340 341 // The connection won't close immediately, but it should close at some point: 342 const start = Date.now(); 343 344 while (true) { 345 // Stop spinning if 10 seconds have passed 346 if (Date.now() - start > 10000) throw Error('Timed out'); 347 348 const afterAbortResult = await fetch(`../resources/stash-take.py?key=${stateKey}`).then(r => r.json()); 349 if (afterAbortResult == 'closed') break; 350 } 351 }, "Underlying connection is closed when aborting after receiving response"); 352 353 promise_test(async t => { 354 await abortRequests(); 355 356 const controller = new AbortController(); 357 const signal = controller.signal; 358 const stateKey = token(); 359 const abortKey = token(); 360 requestAbortKeys.push(abortKey); 361 362 const url = new URL(`../resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, location); 363 url.hostname = urlHostname; 364 365 await fetch(url, { 366 signal, 367 mode: 'no-cors' 368 }); 369 370 const stashTakeURL = new URL(`../resources/stash-take.py?key=${stateKey}`, location); 371 stashTakeURL.hostname = urlHostname; 372 373 const beforeAbortResult = await fetch(stashTakeURL).then(r => r.json()); 374 assert_equals(beforeAbortResult, "open", "Connection is open"); 375 376 controller.abort(); 377 378 // The connection won't close immediately, but it should close at some point: 379 const start = Date.now(); 380 381 while (true) { 382 // Stop spinning if 10 seconds have passed 383 if (Date.now() - start > 10000) throw Error('Timed out'); 384 385 const afterAbortResult = await fetch(stashTakeURL).then(r => r.json()); 386 if (afterAbortResult == 'closed') break; 387 } 388 }, "Underlying connection is closed when aborting after receiving response - no-cors"); 389 390 for (const bodyMethod of BODY_METHODS) { 391 promise_test(async t => { 392 await abortRequests(); 393 394 const controller = new AbortController(); 395 const signal = controller.signal; 396 const stateKey = token(); 397 const abortKey = token(); 398 requestAbortKeys.push(abortKey); 399 400 const response = await fetch(`../resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, { signal }); 401 402 const beforeAbortResult = await fetch(`../resources/stash-take.py?key=${stateKey}`).then(r => r.json()); 403 assert_equals(beforeAbortResult, "open", "Connection is open"); 404 405 const bodyPromise = response[bodyMethod](); 406 407 controller.abort(); 408 409 await promise_rejects_dom(t, "AbortError", bodyPromise); 410 411 const start = Date.now(); 412 413 while (true) { 414 // Stop spinning if 10 seconds have passed 415 if (Date.now() - start > 10000) throw Error('Timed out'); 416 417 const afterAbortResult = await fetch(`../resources/stash-take.py?key=${stateKey}`).then(r => r.json()); 418 if (afterAbortResult == 'closed') break; 419 } 420 }, `Fetch aborted & connection closed when aborted after calling response.${bodyMethod}()`); 421 } 422 423 promise_test(async t => { 424 await abortRequests(); 425 426 const controller = new AbortController(); 427 const signal = controller.signal; 428 const stateKey = token(); 429 const abortKey = token(); 430 requestAbortKeys.push(abortKey); 431 432 const response = await fetch(`../resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, { signal }); 433 const reader = response.body.getReader(); 434 435 controller.abort(); 436 437 await promise_rejects_dom(t, "AbortError", reader.read()); 438 await promise_rejects_dom(t, "AbortError", reader.closed); 439 440 // The connection won't close immediately, but it should close at some point: 441 const start = Date.now(); 442 443 while (true) { 444 // Stop spinning if 10 seconds have passed 445 if (Date.now() - start > 10000) throw Error('Timed out'); 446 447 const afterAbortResult = await fetch(`../resources/stash-take.py?key=${stateKey}`).then(r => r.json()); 448 if (afterAbortResult == 'closed') break; 449 } 450 }, "Stream errors once aborted. Underlying connection closed."); 451 452 promise_test(async t => { 453 await abortRequests(); 454 455 const controller = new AbortController(); 456 const signal = controller.signal; 457 const stateKey = token(); 458 const abortKey = token(); 459 requestAbortKeys.push(abortKey); 460 461 const response = await fetch(`../resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, { signal }); 462 const reader = response.body.getReader(); 463 464 await reader.read(); 465 466 controller.abort(); 467 468 await promise_rejects_dom(t, "AbortError", reader.read()); 469 await promise_rejects_dom(t, "AbortError", reader.closed); 470 471 // The connection won't close immediately, but it should close at some point: 472 const start = Date.now(); 473 474 while (true) { 475 // Stop spinning if 10 seconds have passed 476 if (Date.now() - start > 10000) throw Error('Timed out'); 477 478 const afterAbortResult = await fetch(`../resources/stash-take.py?key=${stateKey}`).then(r => r.json()); 479 if (afterAbortResult == 'closed') break; 480 } 481 }, "Stream errors once aborted, after reading. Underlying connection closed."); 482 483 promise_test(async t => { 484 await abortRequests(); 485 486 const controller = new AbortController(); 487 const signal = controller.signal; 488 489 const response = await fetch(`../resources/empty.txt`, { signal }); 490 491 // Read whole response to ensure close signal has sent. 492 await response.clone().text(); 493 494 const reader = response.body.getReader(); 495 496 controller.abort(); 497 498 const item = await reader.read(); 499 500 assert_true(item.done, "Stream is done"); 501 }, "Stream will not error if body is empty. It's closed with an empty queue before it errors."); 502 503 promise_test(async t => { 504 const controller = new AbortController(); 505 const signal = controller.signal; 506 controller.abort(); 507 508 let cancelReason; 509 510 const body = new ReadableStream({ 511 pull(controller) { 512 controller.enqueue(new Uint8Array([42])); 513 }, 514 cancel(reason) { 515 cancelReason = reason; 516 } 517 }); 518 519 const fetchPromise = fetch('../resources/empty.txt', { 520 body, signal, 521 method: 'POST', 522 duplex: 'half', 523 headers: { 524 'Content-Type': 'text/plain' 525 } 526 }); 527 528 assert_true(!!cancelReason, 'Cancel called sync'); 529 assert_equals(cancelReason.constructor, DOMException); 530 assert_equals(cancelReason.name, 'AbortError'); 531 532 await promise_rejects_dom(t, "AbortError", fetchPromise); 533 534 const fetchErr = await fetchPromise.catch(e => e); 535 536 assert_equals(cancelReason, fetchErr, "Fetch rejects with same error instance"); 537 }, "Readable stream synchronously cancels with AbortError if aborted before reading"); 538 539 test(() => { 540 const controller = new AbortController(); 541 const signal = controller.signal; 542 controller.abort(); 543 544 const request = new Request('.', { signal }); 545 const requestSignal = request.signal; 546 547 const clonedRequest = request.clone(); 548 549 assert_equals(requestSignal, request.signal, "Original request signal the same after cloning"); 550 assert_true(request.signal.aborted, "Original request signal aborted"); 551 assert_not_equals(clonedRequest.signal, request.signal, "Cloned request has different signal"); 552 assert_true(clonedRequest.signal.aborted, "Cloned request signal aborted"); 553 }, "Signal state is cloned"); 554 555 test(() => { 556 const controller = new AbortController(); 557 const signal = controller.signal; 558 559 const request = new Request('.', { signal }); 560 const clonedRequest = request.clone(); 561 562 const log = []; 563 564 request.signal.addEventListener('abort', () => log.push('original-aborted')); 565 clonedRequest.signal.addEventListener('abort', () => log.push('clone-aborted')); 566 567 controller.abort(); 568 569 assert_array_equals(log, ['original-aborted', 'clone-aborted'], "Abort events fired in correct order"); 570 assert_true(request.signal.aborted, 'Signal aborted'); 571 assert_true(clonedRequest.signal.aborted, 'Signal aborted'); 572 }, "Clone aborts with original controller");