Source: patchers/http_p.js

/**
 * @module http_p
 */

/**
 * This module patches the HTTP and HTTPS node built-in libraries and returns a copy of the module with tracing enabled.
 */

var url = require('url');

var contextUtils = require('../context_utils');
var Utils = require('../utils');

var logger = require('../logger');
var events = require('events');

/**
 * Wraps the http/https.request() and .get() calls to automatically capture information for the segment.
 * This patches the built-in HTTP and HTTPS modules globally. If using a 3rd party HTTP library,
 * it should still use HTTP under the hood. Be sure to patch globally before requiring the 3rd party library.
 * 3rd party library compatibility is best effort. Some incompatibility issues may arise.
 * @param {http|https} module - The built in Node.js HTTP or HTTPS module.
 * @param {boolean} downstreamXRayEnabled - when true, adds a "traced:true" property to the subsegment
 *   so the AWS X-Ray service expects a corresponding segment from the downstream service.
 * @param {function} subsegmentCallback - a callback that is called with the subsegment, the Node.js
 *   http.ClientRequest, the Node.js http.IncomingMessage (if a response was received) and any error issued,
 *   allowing custom annotations and metadata to be added.
 *   to be added to the subsegment.
 * @alias module:http_p.captureHTTPsGlobal
 */

var captureHTTPsGlobal = function captureHTTPsGlobal(module, downstreamXRayEnabled, subsegmentCallback) {
  if (!module.__request) {
    enableCapture(module, downstreamXRayEnabled, subsegmentCallback);
  }
};

/**
 * Wraps the http/https.request() and .get() calls to automatically capture information for the segment.
 * Returns an instance of the HTTP or HTTPS module that is patched.
 * @param {http|https} module - The built in Node.js HTTP or HTTPS module.
 * @param {boolean} downstreamXRayEnabled - when true, adds a "traced:true" property to the subsegment
 *   so the AWS X-Ray service expects a corresponding segment from the downstream service.
 * @param {function} subsegmentCallback - a callback that is called with the subsegment, the Node.js
 *   http.ClientRequest, and the Node.js http.IncomingMessage to allow custom annotations and metadata
 *   to be added to the subsegment.
 * @alias module:http_p.captureHTTPs
 * @returns {http|https}
 */

var captureHTTPs = function captureHTTPs(module, downstreamXRayEnabled, subsegmentCallback) {
  if (module.__request) {
    return module;
  }

  var tracedModule = {};

  Object.keys(module).forEach(function(val) {
    tracedModule[val] = module[val];
  });

  enableCapture(tracedModule, downstreamXRayEnabled, subsegmentCallback);
  return tracedModule;
};

function enableCapture(module, downstreamXRayEnabled, subsegmentCallback) {
  function captureOutgoingHTTPs(baseFunc, ...args) {
    let options;
    let callback;
    let hasUrl;
    let urlObj;

    let arg0 = args[0];
    if (typeof args[1] === 'object') {
      hasUrl = true;
      urlObj = typeof arg0 === 'string' ? new url.URL(arg0) : arg0;
      options = args[1],
      callback = args[2];
    } else {
      hasUrl = false;
      options = arg0;
      callback = args[1];
    }

    // Short circuit if the HTTP request has no options or is already being captured
    if (!options || (options.headers && (options.headers['X-Amzn-Trace-Id']))) {
      return baseFunc(...args);
    }

    // Case of calling a string URL without options, e.g.: http.request('http://amazon.com', callback)
    if (typeof options === 'string') {
      options = new url.URL(options);
    }

    if (!hasUrl) {
      urlObj = options;
    }

    const parent = contextUtils.resolveSegment(contextUtils.resolveManualSegmentParams(options));
    const hostname = options.hostname || options.host || urlObj.hostname || urlObj.host || 'Unknown host';

    if (!parent) {
      let output = '[ host: ' + hostname;
      output = options.method ? (output + ', method: ' + options.method) : output;
      output += ', path: ' + (urlObj.pathname || Utils.stripQueryStringFromPath(options.path)) + ' ]';

      if (!contextUtils.isAutomaticMode()) {
        logger.getLogger().info('Options for request ' + output +
          ' requires a segment object on the options params as "XRaySegment" for tracing in manual mode. Ignoring.');
      } else {
        logger.getLogger().info('Options for request ' + output +
          ' is missing the sub/segment context for automatic mode. Ignoring.');
      }

      // Options are not modified, only parsed for logging. We can pass in the original arguments.
      return baseFunc(...args);
    }

    let subsegment;
    if (parent.notTraced) {
      subsegment = parent.addNewSubsegmentWithoutSampling(hostname);
    } else {
      subsegment = parent.addNewSubsegment(hostname);
    }

    const root = parent.segment ? parent.segment : parent;
    subsegment.namespace = 'remote';

    if (!options.headers) {
      options.headers = {};
    }

    options.headers['X-Amzn-Trace-Id'] = 'Root=' + root.trace_id + ';Parent=' + subsegment.id +
      ';Sampled=' + (subsegment.notTraced ? '0' : '1');

    const errorCapturer = function errorCapturer(e) {
      if (subsegmentCallback) {
        subsegmentCallback(subsegment, this, null, e);
      }

      if (subsegment.http && subsegment.http.response) {
        if (Utils.getCauseTypeFromHttpStatus(subsegment.http.response.status) === 'error') {
          subsegment.addErrorFlag();
        }
        subsegment.close(e, true);
      } else {
        const madeItToDownstream = (e.code !== 'ECONNREFUSED');

        subsegment.addRemoteRequestData(this, null, madeItToDownstream && downstreamXRayEnabled);
        subsegment.close(e);
      }
    };

    const optionsCopy = Utils.objectWithoutProperties(options, ['Segment'], true);

    let req = baseFunc(...(hasUrl ? [arg0, optionsCopy] : [options]), function(res) {
      res.on('end', function() {
        if (subsegmentCallback) {
          subsegmentCallback(subsegment, this.req, res);
        }

        if (res.statusCode === 429) {
          subsegment.addThrottleFlag();
        }

        const cause = Utils.getCauseTypeFromHttpStatus(res.statusCode);

        if (cause) {
          subsegment[cause] = true;
        }

        subsegment.addRemoteRequestData(res.req, res, !!downstreamXRayEnabled);
        subsegment.close();
      });

      if (typeof callback === 'function') {
        if (contextUtils.isAutomaticMode()) {
          const session = contextUtils.getNamespace();

          session.run(function() {
            contextUtils.setSegment(subsegment);
            callback(res);
          });
        } else {
          callback(res);
        }
        // if no callback provided by user application, AND no explicit response listener
        // added by user application, then we consume the response so the 'end' event fires
        // See: https://nodejs.org/api/http.html#http_class_http_clientrequest
      } else if (res.req && res.req.listenerCount('response') === 0) {
        res.resume();
      }
    });

    // Use errorMonitor if available (in Node 12.17+), otherwise fall back to standard error listener
    // See: https://nodejs.org/dist/latest-v12.x/docs/api/events.html#events_eventemitter_errormonitor
    req.on(events.errorMonitor || 'error', errorCapturer);

    return req;
  }

  module.__request = module.request;
  function captureHTTPsRequest(...args) {
    return captureOutgoingHTTPs(module.__request, ...args);
  }

  module.__get = module.get;
  function captureHTTPsGet(...args) {
    return captureOutgoingHTTPs(module.__get, ...args);
  }

  Object.defineProperties(module, {
    request: { value: captureHTTPsRequest, configurable: true, enumerable: true, writable: true },
    get: { value: captureHTTPsGet, configurable: true, enumerable: true, writable: true },
  });
}

module.exports.captureHTTPsGlobal = captureHTTPsGlobal;
module.exports.captureHTTPs = captureHTTPs;