Code coverage report for lib/bagofrequest.js

Statements: 100% (80 / 80)      Branches: 100% (80 / 80)      Functions: 100% (14 / 14)      Lines: 100% (80 / 80)      Ignored: none     

All files » lib/ » bagofrequest.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 2091                                                 1 27 27   27   27                 27 25 6 19 2         27 6       27 135 2         27 1 1       1 27 27 1   27     27 27 5 4   1     22 22 14 8 2   6           1 27               27                       27   1 78   78     78 78   78   78 51 51   27         1 73     1 27 27       27     27       1 93 93 180 180 69     93                               1 52   52   52 7 45 38 3     38 4   34       52     1 72 72     1 1  
var request = require('request'),
  _url = require('url'),
  async = require('async');
 
/**
 * Sends a HTTP request to a specified URL with optional proxy, query strings, and handlers.
 * Convenient handling of request error and unexpected status code.
 * GET redirection is handled by default by request module.
 * Non-GET redirection is handled by params followAllRedirects: true.
 *
 * @param {String} method: http method
 * @param {String} url: URL without query string
 * @param {Object} opts: optional
 *   - proxy: proxy server URL with format http://user:pass@host:port
 *   - noProxyHosts: an array of host names which will ignore any proxy settings, defaults to: localhost, 127.0.0.1
 *   - queryStrings: object containing URL query strings with format { name: value }
 *   - handlers: response handlers with format { error: function(err, result, cb), statuscode: function(result, cb) }
 *   - headers: object containing http headers, exact map to mikeal/request headers opt
 *   - body, form, json, multipart: request payload, exact map to mikeal/request module body, form, json, multipart opts
 *   - retry: request retry configuration, false for disabled, true for sensible defaults, otherwise
 *            { errorCodes: ['ENOTFOUND'] || true/false, statusCodes: [ 503 ], scale: 0.5, delay: 1000, maxRetries: 10 }
 *   - timeout: request timeout in milliseconds, default to 30000 milliseconds
 *   - requestOpts: mikeal/request module opts, these opts will override any params set by http#req
 * @param {Function} cb: standard cb(err, result) callback
 */
function req(method, url, opts, cb) {
  opts.handlers = opts.handlers || {};
  opts.retry = opts.retry || false;
 
  const TIMEOUT = 30000;
  // init default settings with follow redirections on all methods, and 30 seconds timeout
  var params = {
      url: url,
      followAllRedirects: true,
      rejectUnauthorized: false,
      timeout: opts.timeout || TIMEOUT
    },
    envProxy = proxy(url);
 
  // set proxy setting based on environment variable
  if (!_proxyIgnored(url, opts)) {
    if (opts.proxy) {
      params.proxy = opts.proxy;
    } else if (envProxy) {
      params.proxy = envProxy;
    }
  }
 
  // just a more readable opt name for query strings
  if (opts.queryStrings) {
    params.qs = opts.queryStrings;
  }
 
  // headers and payload handling
  ['headers', 'body', 'form', 'json', 'multipart'].forEach(function (opt) {
    if (opts[opt]) {
      params[opt] = opts[opt];
    }
  });
 
  // override defaults with mikeal/request-specific opts
  if (opts.requestOpts) {
    Object.keys(opts.requestOpts).forEach(function (opt) {
      params[opt] = opts.requestOpts[opt];
    });
  }
 
  function _requestMethod(method) {
    var requestMethod = method.toLowerCase();
    if (requestMethod === 'delete') {
      requestMethod = 'del';
    }
    return requestMethod;
  }
 
  _execRequest(opts, _requestMethod(method), params, function (err, result) {
    if (err) {
      if (opts.handlers.error) {
        opts.handlers.error(err, result, cb);
      } else {
        cb(err);
      }
    } else {
      var wildcardMatch = _wildcardMatch(result.statusCode, Object.keys(opts.handlers));
      if (opts.handlers[result.statusCode]) {
        opts.handlers[result.statusCode](result, cb);
      } else if (wildcardMatch) {
        opts.handlers[wildcardMatch](result, cb);
      } else {
        cb(new Error('Unexpected status code: ' + result.statusCode + '\nResponse body:\n' + result.body));
      }
    }
  });
}
 
function _execRequest(opts, method, params, cb) {
  const DEFAULTS = {
    errorCodes: [ 'ECONNABORTED', 'ECONNREFUSED', 'ECONNRESET', 'EHOSTUNREACH', 'EIO', 'ENETDOWN', 'ENETRESET', 'ENETUNREACH', 'ETIMEDOUT' ],
    statusCodes: [ 408, 502, 503, 504 ],
    scale: 0.5,
    delay: 500,
    maxRetries: 10
  };
 
  var tryCount = 0,
    currDelay, lastResult, retryable,
    retryOpts = (opts.retry === true) ?
      DEFAULTS :
      {
        errorCodes: opts.retry.errorCodes || DEFAULTS.errorCodes,
        statusCodes: opts.retry.statusCodes || DEFAULTS.statusCodes,
        scale: (opts.retry.scale !== undefined) ? opts.retry.scale : DEFAULTS.scale,
        delay: (opts.retry.delay !== undefined) ? opts.retry.delay : DEFAULTS.delay,
        maxRetries: (opts.retry.maxRetries !== undefined) ? opts.retry.maxRetries : DEFAULTS.maxRetries
      };
 
  currDelay = retryOpts.delay;
 
  function does(cb) {
    request[method](params, function (err, result) {
 
      var retryableError = err && (retryOpts.errorCodes === true || (Array.isArray(retryOpts.errorCodes) && retryOpts.errorCodes.indexOf(err.code) !== -1)),
        retryableResult = result && _wildcardMatch(result.statusCode, retryOpts.statusCodes) !== undefined;
 
      tryCount++;
      lastResult = result;
 
      retryable = opts.retry && tryCount <= retryOpts.maxRetries && (retryableError || retryableResult);
 
      if (retryable) {
        setTimeout(cb, currDelay); // retry after delay
        currDelay += currDelay * retryOpts.scale;
      } else {
        cb(err); // no retry needed, finish off
      }
    });
  }
 
  function check() {
    return retryable;
  }
 
  function done(err) {
    var result = lastResult || {};
    result._retry = {
      retryCount: tryCount - 1,
      retryLimitHit: (tryCount - 1 === retryOpts.maxRetries)
    };
    cb(err, result);
  }
 
  async.doWhilst(does, check, done);
}
 
// check for any wildcard match (e.g. 201 matches 2xx)
function _wildcardMatch(statusCode, wildcards) {
  var match;
  wildcards.forEach(function (wildcard) {
    var regex = new RegExp(wildcard.toString().replace(/x/g, '.'));
    if (!match && statusCode.toString().match(regex)) {
      match = wildcard;
    }
  });
  return match;
}
 
/**
 * Determines proxy value based on URL and process environment variable (http_proxy, https_proxy).
 * This allows library clients to control which proxy to use by setting environment variable.
 * - if url starts with http, use http_proxy when available
 * - if url starts with https, use https_proxy when available, otherwise fallback to http_proxy
 * - if url does not have protocol, assume http protocol
 * - if url is not specified, http_proxy takes precedence over https_proxy
 *
 * @param {String} url: URL used to determine which proxy environment variable to use
 * @param {Object} opts: optional
 *   - noProxyHosts: an array of host names which will ignore any proxy settings, defaults to: localhost, 127.0.0.1
 * @return proxy
 */
function proxy(url, opts) {
  opts = opts || {};
 
  var _proxy;
 
  if (!url) {
    _proxy = process.env.http_proxy || process.env.HTTP_PROXY || process.env.https_proxy || process.env.HTTPS_PROXY;
  } else if (!_proxyIgnored(url, opts)) {
    if (!url.match(/^https?:\/\//)) {
      url += 'http://' + url;
    }
 
    if (url.match(/^https:\/\//)) {
      _proxy = process.env.https_proxy || process.env.HTTPS_PROXY || process.env.http_proxy || process.env.HTTP_PROXY;
    } else {
      _proxy = process.env.http_proxy || process.env.HTTP_PROXY;
    }
  }
 
  return _proxy;
}
 
function _proxyIgnored(url, opts) {
  const NO_PROXY_HOSTS = ['localhost', '127.0.0.1'];
  return (opts.noProxyHosts || NO_PROXY_HOSTS).indexOf(_url.parse(url).hostname) !== -1;
}
 
exports.request = req;
exports.proxy = proxy;