tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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");