function performRequest(uri, transactionReq, options, callback) {
  if (typeof options === 'function') {
    [options, callback] = [{}, options];
  }
  const logger = options.logger || defaultLogger;
  const request = options.request || defaultRequest;

  const httpOptions = Object.assign({}, options.http || {});
  httpOptions.proxy = false;
  httpOptions.followRedirect = false;
  httpOptions.encoding = null;
  httpOptions.method = transactionReq.method;
  httpOptions.uri = uri;

  try {
    httpOptions.body = getBodyAsBuffer(
      transactionReq.body,
      transactionReq.bodyEncoding,
    );
    httpOptions.headers = normalizeContentLengthHeader(
      transactionReq.headers,
      httpOptions.body,
    );

    const protocol = httpOptions.uri.split(':')[0].toUpperCase();
    logger.debug(
      `Performing ${protocol} request to the server under test: ` +
        `${httpOptions.method} ${httpOptions.uri}`,
    );

    request(httpOptions, (error, response, responseBody) => {
      logger.debug(`Handling ${protocol} response from the server under test`);
      if (error) {
        callback(error);
      } else {
        callback(null, createTransactionResponse(response, responseBody));
      }
    });
  } catch (error) {
    process.nextTick(() => callback(error));
  }
}
Performs the HTTP request as described in the 'transaction.request' object

In future we should introduce a 'real' request object as well so user has
access to the modifications made on the way.

@param {string} uri
@param {Object} transactionReq
@param {Object} [options]
@param {Object} [options.logger] Custom logger
@param {Object} [options.request] Custom 'request' library implementation
@param {Object} [options.http] Custom default 'request' library options
@param {Function} callback