function onRequestCreate(opts, context, fileName) {
  if (process.env.DEBUG) throwIfProtoPresent(opts);
  let { data, url, [kResponseType]: type = '' } = opts;
  let err, res;
  // XHR spec requires `url` but allows ''/null/non-string
  if (!url && !('url' in opts)) {
    err = new SafeError('Required parameter "url" is missing.');
  } else if (!isString(url)) {
    if (url === location) { url = url.href; } // safe window.location is unforgeable
    else {
      try { url = url::URLToString(); } // safe window.URL getter
      catch (e) {
        try { url = `${url}`; } // unsafe toString may throw e.g. for Symbol or if spoofed
        catch (e2) { err = e2; }
      }
    }
    opts.url = url;
  }
  if (err) {
    onRequestInitError(opts, err);
    return; // not returning the abort controller as there's no request to abort
  }
  if (!(type in XHR_TYPES)) {
    logging.warn(`Unknown ${kResponseType} "${type}"`);
    type = '';
  }
  const scriptId = context.id;
  const id = safeGetUniqId('VMxhr');
  const cb = createNullObj();
  const req = safePickInto({ cb, id, scriptId }, opts, OPTS_TO_KEEP);
  // withCredentials is for GM4 compatibility and used only if `anonymous` is not set,
  // it's true by default per the standard/historical behavior of gmxhr
  const { withCredentials = true, anonymous = !withCredentials } = opts;
  // setting opts.onload and onerror before EVENTS_TO_NOTIFY
  if (context.async) res = new SafePromise((resolve, reject) => {
    const { [kOnload]: onload, [kOnerror]: onerror } = opts;
    opts[kOnload] = onload ? v => { resolve(v); onload(v); } : resolve;
    opts[kOnerror] = onerror ? v => { reject(v); onerror(v); } : reject;
  });
  idMap[id] = req;
  data = data == null && []
    // `binary` is for TM/GM-compatibility + non-objects = must use a string `data`
    || (opts.binary || !isObject(data)) && [`${data}`]
    // No browser can send FormData/URLSearchParams directly across worlds
    || getFormData(data)
    // FF56+ can send any cloneable data directly, FF52-55 can't due to https://bugzil.la/1371246
    || IS_FIREFOX >= 56 && [data]
    || [data, 'bin'];
  /** @type {GMReq.Message.Web} */
  bridge.call('HttpRequest', safePickInto({
    anonymous,
    data,
    id,
    scriptId,
    url,
    [kFileName]: fileName,
    [kResponseType]: type,
    [kXhrType]: req[kXhrType] = XHR_TYPES[type] ? type : '',
    events: EVENTS_TO_NOTIFY::filter(key => isFunction(cb[key] = opts[`on${key}`])),
  }, opts, OPTS_TO_PASS));
  if (!res) res = {};
  else if (IS_FIREFOX) setPrototypeOf(res, SafePromiseConstructor);
  setOwnProp(res, 'abort', () => bridge.post('AbortRequest', id));
  return res;
}
@param {GMReq.UserOpts} opts - must already have a null proto
@param {GMContext} context
@param {string} fileName
@return {VMScriptXHRControl | Promise<VMScriptXHRControl>}