async function httpRequest(opts, events, src, cb) {
  const { tab } = src;
  const { incognito } = tab;
  const { anonymous, id, overrideMimeType, [kXhrType]: xhrType } = opts;
  const url = vetUrl(opts.url, src.url, true);
  const req = requests[id];
  if (!req || req.cb) return;
  req.cb = cb;
  req[kFileName] = opts[kFileName];
  const { xhr } = req;
  const vmHeaders = {};
  // Firefox can send Blob/ArrayBuffer directly
  const willStringifyBinaries = xhrType && !IS_FIREFOX;
  // Chrome can't fetch Blob URL in incognito so we use chunks
  const chunked = willStringifyBinaries && incognito;
  const blobbed = willStringifyBinaries && !incognito;
  const [body, contentType] = decodeBody(opts.data);
  // Firefox doesn't send cookies, https://github.com/violentmonkey/violentmonkey/issues/606
  // Both Chrome & FF need explicit routing of cookies in containers or incognito
  const shouldSendCookies = !anonymous && (incognito || IS_FIREFOX);
  const uaHeaders = [];
  req[kCookie] = !anonymous && !shouldSendCookies;
  req[kSetCookie] = !anonymous;
  xhr.open(opts.method || 'GET', url, true, opts.user || '', opts.password || '');
  xhr.setRequestHeader(VM_VERIFY, id);
  if (contentType) xhr.setRequestHeader('Content-Type', contentType);
  opts.headers::forEachEntry(([name, value]) => {
    const nameLow = name.toLowerCase();
    const i = UA_HEADERS.indexOf(nameLow);
    if (i >= 0 && (uaHeaders[i] = true) || FORBIDDEN_HEADER_RE.test(name)) {
      pushWebRequestHeader(vmHeaders, name, value, nameLow);
    } else {
      xhr.setRequestHeader(name, value);
    }
  });
  opts.ua.forEach((val, i) => {
    if (!uaHeaders[i] && !deepEqual(val, !i ? navUA : navUAD[UA_PROPS[i]])) {
      const name = UA_HEADERS[i];
      pushWebRequestHeader(vmHeaders, name, UA_GETTERS[name](val), name);
    }
  });
  xhr[kResponseType] = willStringifyBinaries && 'blob' || xhrType || 'text';
  xhr.timeout = Math.max(0, Math.min(0x7FFF_FFFF, opts.timeout)) || 0;
  if (overrideMimeType) xhr.overrideMimeType(overrideMimeType);
  if (shouldSendCookies) {
    for (const store of await browser.cookies.getAllCookieStores()) {
      if (store.tabIds.includes(tab.id)) {
        if (IS_FIREFOX ? !store.id.endsWith('-default') : store.id !== '0') {
          /* Cookie routing. For the main store we rely on the browser.
           * The ids are hard-coded as `stores` may omit the main store if no such tabs are open. */
          req.storeId = store.id;
        }
        break;
      }
    }
    const now = Date.now() / 1000;
    const cookies = (await browser.cookies.getAll({
      url,
      storeId: req.storeId,
      ...FIREFOX >= 59 && { firstPartyDomain: null },
    })).filter(c => c.session || c.expirationDate > now); // FF reports expired cookies!
    if (cookies.length) {
      pushWebRequestHeader(vmHeaders, kCookie,
        cookies.map(c => `${c.name}=${c.value};`).join(' '));
    }
  }
  toggleHeaderInjector(id, vmHeaders);
  // Sending as params to avoid storing one-time init data in `requests`
  const callback = xhrCallbackWrapper(req, events, blobbed, chunked, opts[kResponseType] === 'json');
  events.forEach(evt => { xhr[`on${evt}`] = callback; });
  xhr.onloadend = callback; // always send it for the internal cleanup
  xhr.send(body);
}
@param {GMReq.Message.Web} opts
@param {GMReq.EventType[]} events
@param {VMMessageSender} src
@param {function} cb
@returns {Promise<void>}