http.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. /**
  2. * Module dependencies.
  3. */
  4. var url = require('url');
  5. var http = require('http');
  6. var extend = require('extend');
  7. var NotFoundError = require('./notfound');
  8. var NotModifiedError = require('./notmodified');
  9. var debug = require('debug')('get-uri:http');
  10. /**
  11. * Module exports.
  12. */
  13. module.exports = get;
  14. /**
  15. * Returns a Readable stream from an "http:" URI.
  16. *
  17. * @api protected
  18. */
  19. function get (parsed, opts, fn) {
  20. debug('GET %o', parsed.href);
  21. var cache = getCache(parsed, opts.cache);
  22. // 5 redirects allowed by default
  23. var maxRedirects = opts.hasOwnProperty('maxRedirects') ? opts.maxRedirects : 5;
  24. debug('allowing %o max redirects', maxRedirects);
  25. // first check the previous Expires and/or Cache-Control headers
  26. // of a previous response if a `cache` was provided
  27. if (cache && isFresh(cache)) {
  28. // check for a 3xx "redirect" status code on the previous cache
  29. var location = cache.headers.location;
  30. var type = (cache.statusCode / 100 | 0);
  31. if (3 == type && location) {
  32. debug('cached redirect');
  33. fn(new Error('TODO: implement cached redirects!'));
  34. } else {
  35. // otherwise we assume that it's the destination endpoint,
  36. // since there's nowhere else to redirect to
  37. fn(new NotModifiedError());
  38. }
  39. return;
  40. }
  41. var mod;
  42. if (opts.http) {
  43. // the `https` module passed in from the "http.js" file
  44. mod = opts.http;
  45. debug('using secure `https` core module');
  46. } else {
  47. mod = http;
  48. debug('using `http` core module');
  49. }
  50. var options = extend({}, opts, parsed);
  51. // add "cache validation" headers if a `cache` was provided
  52. if (cache) {
  53. if (!options.headers) options.headers = {};
  54. var lastModified = cache.headers['last-modified'];
  55. if (lastModified != null) {
  56. options.headers['If-Modified-Since'] = lastModified;
  57. debug('added "If-Modified-Since" request header: %o', lastModified);
  58. }
  59. var etag = cache.headers.etag;
  60. if (etag != null) {
  61. options.headers['If-None-Match'] = etag;
  62. debug('added "If-None-Match" request header: %o', etag);
  63. }
  64. }
  65. var req = mod.get(options);
  66. req.once('error', onerror);
  67. req.once('response', onresponse);
  68. // http.ClientRequest "error" event handler
  69. function onerror (err) {
  70. debug('http.ClientRequest "error" event: %o', err.stack || err);
  71. fn(err);
  72. }
  73. // http.ClientRequest "response" event handler
  74. function onresponse (res) {
  75. var code = res.statusCode;
  76. // assign a Date to this response for the "Cache-Control" delta calculation
  77. res.date = new Date();
  78. res.parsed = parsed;
  79. debug('got %o response status code', code);
  80. // any 2xx response is a "success" code
  81. var type = (code / 100 | 0);
  82. // check for a 3xx "redirect" status code
  83. var location = res.headers.location;
  84. if (3 == type && location) {
  85. if (!opts.redirects) opts.redirects = [];
  86. var redirects = opts.redirects;
  87. if (redirects.length < maxRedirects) {
  88. debug('got a "redirect" status code with Location: %o', location);
  89. // flush this response - we're not going to use it
  90. res.resume();
  91. // hang on to this Response object for the "redirects" Array
  92. redirects.push(res);
  93. var newUri = url.resolve(parsed, location);
  94. debug('resolved redirect URL: %o', newUri);
  95. var left = maxRedirects - redirects.length;
  96. debug('%o more redirects allowed after this one', left);
  97. return get(url.parse(newUri), opts, fn);
  98. }
  99. }
  100. // if we didn't get a 2xx "success" status code, then create an Error object
  101. if (2 != type) {
  102. var err;
  103. if (304 == code) {
  104. err = new NotModifiedError();
  105. } else if (404 == code) {
  106. err = new NotFoundError();
  107. } else {
  108. // other HTTP-level error
  109. var message = http.STATUS_CODES[code];
  110. err = new Error(message);
  111. err.statusCode = code;
  112. err.code = code;
  113. }
  114. res.resume();
  115. return fn(err);
  116. }
  117. if (opts.redirects) {
  118. // store a reference to the "redirects" Array on the Response object so that
  119. // they can be inspected during a subsequent call to GET the same URI
  120. res.redirects = opts.redirects;
  121. }
  122. fn(null, res);
  123. }
  124. }
  125. /**
  126. * Returns `true` if the provided cache's "freshness" is valid. That is, either
  127. * the Cache-Control header or Expires header values are still within the allowed
  128. * time period.
  129. *
  130. * @return {Boolean}
  131. * @api private
  132. */
  133. function isFresh (cache) {
  134. var cacheControl = cache.headers['cache-control'];
  135. var expires = cache.headers.expires;
  136. var fresh;
  137. if (cacheControl) {
  138. // for Cache-Control rules, see: http://www.mnot.net/cache_docs/#CACHE-CONTROL
  139. debug('Cache-Control: %o', cacheControl);
  140. var parts = cacheControl.split(/,\s*?\b/);
  141. for (var i = 0; i < parts.length; i++) {
  142. var part = parts[i];
  143. var subparts = part.split('=');
  144. var name = subparts[0];
  145. switch (name) {
  146. case 'max-age':
  147. var val = +subparts[1];
  148. expires = new Date(+cache.date + (val * 1000));
  149. fresh = new Date() < expires;
  150. if (fresh) debug('cache is "fresh" due to previous %o Cache-Control param', part);
  151. return fresh;
  152. case 'must-revalidate':
  153. // XXX: what we supposed to do here?
  154. break;
  155. case 'no-cache':
  156. case 'no-store':
  157. debug('cache is "stale" due to explicit %o Cache-Control param', name);
  158. return false;
  159. }
  160. }
  161. } else if (expires) {
  162. // for Expires rules, see: http://www.mnot.net/cache_docs/#EXPIRES
  163. debug('Expires: %o', expires);
  164. fresh = new Date() < new Date(expires);
  165. if (fresh) debug('cache is "fresh" due to previous Expires response header');
  166. return fresh;
  167. }
  168. return false;
  169. }
  170. /**
  171. * Attempts to return a previous Response object from a previous GET call to the
  172. * same URI.
  173. *
  174. * @api private
  175. */
  176. function getCache (parsed, cache) {
  177. if (!cache) return;
  178. var href = parsed.href;
  179. if (cache.parsed.href == href) {
  180. return cache;
  181. }
  182. var redirects = cache.redirects;
  183. if (redirects) {
  184. for (var i = 0; i < redirects.length; i++) {
  185. var c = getCache(parsed, redirects[i]);
  186. if (c) return c;
  187. }
  188. }
  189. }