index.js 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. /**
  2. * index.js
  3. *
  4. * a request API compatible with window.fetch
  5. */
  6. var parse_url = require('url').parse;
  7. var resolve_url = require('url').resolve;
  8. var http = require('http');
  9. var https = require('https');
  10. var zlib = require('zlib');
  11. var stream = require('stream');
  12. var Body = require('./lib/body');
  13. var Response = require('./lib/response');
  14. var Headers = require('./lib/headers');
  15. var Request = require('./lib/request');
  16. var FetchError = require('./lib/fetch-error');
  17. // commonjs
  18. module.exports = Fetch;
  19. // es6 default export compatibility
  20. module.exports.default = module.exports;
  21. /**
  22. * Fetch class
  23. *
  24. * @param Mixed url Absolute url or Request instance
  25. * @param Object opts Fetch options
  26. * @return Promise
  27. */
  28. function Fetch(url, opts) {
  29. // allow call as function
  30. if (!(this instanceof Fetch))
  31. return new Fetch(url, opts);
  32. // allow custom promise
  33. if (!Fetch.Promise) {
  34. throw new Error('native promise missing, set Fetch.Promise to your favorite alternative');
  35. }
  36. Body.Promise = Fetch.Promise;
  37. var self = this;
  38. // wrap http.request into fetch
  39. return new Fetch.Promise(function(resolve, reject) {
  40. // build request object
  41. var options = new Request(url, opts);
  42. if (!options.protocol || !options.hostname) {
  43. throw new Error('only absolute urls are supported');
  44. }
  45. if (options.protocol !== 'http:' && options.protocol !== 'https:') {
  46. throw new Error('only http(s) protocols are supported');
  47. }
  48. var send;
  49. if (options.protocol === 'https:') {
  50. send = https.request;
  51. } else {
  52. send = http.request;
  53. }
  54. // normalize headers
  55. var headers = new Headers(options.headers);
  56. if (options.compress) {
  57. headers.set('accept-encoding', 'gzip,deflate');
  58. }
  59. if (!headers.has('user-agent')) {
  60. headers.set('user-agent', 'node-fetch/1.0 (+https://github.com/bitinn/node-fetch)');
  61. }
  62. if (!headers.has('connection') && !options.agent) {
  63. headers.set('connection', 'close');
  64. }
  65. if (!headers.has('accept')) {
  66. headers.set('accept', '*/*');
  67. }
  68. // detect form data input from form-data module, this hack avoid the need to pass multipart header manually
  69. if (!headers.has('content-type') && options.body && typeof options.body.getBoundary === 'function') {
  70. headers.set('content-type', 'multipart/form-data; boundary=' + options.body.getBoundary());
  71. }
  72. // bring node-fetch closer to browser behavior by setting content-length automatically
  73. if (!headers.has('content-length') && /post|put|patch|delete/i.test(options.method)) {
  74. if (typeof options.body === 'string') {
  75. headers.set('content-length', Buffer.byteLength(options.body));
  76. // detect form data input from form-data module, this hack avoid the need to add content-length header manually
  77. } else if (options.body && typeof options.body.getLengthSync === 'function') {
  78. // for form-data 1.x
  79. if (options.body._lengthRetrievers && options.body._lengthRetrievers.length == 0) {
  80. headers.set('content-length', options.body.getLengthSync().toString());
  81. // for form-data 2.x
  82. } else if (options.body.hasKnownLength && options.body.hasKnownLength()) {
  83. headers.set('content-length', options.body.getLengthSync().toString());
  84. }
  85. // this is only necessary for older nodejs releases (before iojs merge)
  86. } else if (options.body === undefined || options.body === null) {
  87. headers.set('content-length', '0');
  88. }
  89. }
  90. options.headers = headers.raw();
  91. // http.request only support string as host header, this hack make custom host header possible
  92. if (options.headers.host) {
  93. options.headers.host = options.headers.host[0];
  94. }
  95. // send request
  96. var req = send(options);
  97. var reqTimeout;
  98. if (options.timeout) {
  99. req.once('socket', function(socket) {
  100. reqTimeout = setTimeout(function() {
  101. req.abort();
  102. reject(new FetchError('network timeout at: ' + options.url, 'request-timeout'));
  103. }, options.timeout);
  104. });
  105. }
  106. req.on('error', function(err) {
  107. clearTimeout(reqTimeout);
  108. reject(new FetchError('request to ' + options.url + ' failed, reason: ' + err.message, 'system', err));
  109. });
  110. req.on('response', function(res) {
  111. clearTimeout(reqTimeout);
  112. // handle redirect
  113. if (self.isRedirect(res.statusCode) && options.redirect !== 'manual') {
  114. if (options.redirect === 'error') {
  115. reject(new FetchError('redirect mode is set to error: ' + options.url, 'no-redirect'));
  116. return;
  117. }
  118. if (options.counter >= options.follow) {
  119. reject(new FetchError('maximum redirect reached at: ' + options.url, 'max-redirect'));
  120. return;
  121. }
  122. if (!res.headers.location) {
  123. reject(new FetchError('redirect location header missing at: ' + options.url, 'invalid-redirect'));
  124. return;
  125. }
  126. // per fetch spec, for POST request with 301/302 response, or any request with 303 response, use GET when following redirect
  127. if (res.statusCode === 303
  128. || ((res.statusCode === 301 || res.statusCode === 302) && options.method === 'POST'))
  129. {
  130. options.method = 'GET';
  131. delete options.body;
  132. delete options.headers['content-length'];
  133. }
  134. options.counter++;
  135. resolve(Fetch(resolve_url(options.url, res.headers.location), options));
  136. return;
  137. }
  138. // normalize location header for manual redirect mode
  139. var headers = new Headers(res.headers);
  140. if (options.redirect === 'manual' && headers.has('location')) {
  141. headers.set('location', resolve_url(options.url, headers.get('location')));
  142. }
  143. // prepare response
  144. var body = res.pipe(new stream.PassThrough());
  145. var response_options = {
  146. url: options.url
  147. , status: res.statusCode
  148. , statusText: res.statusMessage
  149. , headers: headers
  150. , size: options.size
  151. , timeout: options.timeout
  152. };
  153. // response object
  154. var output;
  155. // in following scenarios we ignore compression support
  156. // 1. compression support is disabled
  157. // 2. HEAD request
  158. // 3. no content-encoding header
  159. // 4. no content response (204)
  160. // 5. content not modified response (304)
  161. if (!options.compress || options.method === 'HEAD' || !headers.has('content-encoding') || res.statusCode === 204 || res.statusCode === 304) {
  162. output = new Response(body, response_options);
  163. resolve(output);
  164. return;
  165. }
  166. // otherwise, check for gzip or deflate
  167. var name = headers.get('content-encoding');
  168. // for gzip
  169. if (name == 'gzip' || name == 'x-gzip') {
  170. body = body.pipe(zlib.createGunzip());
  171. output = new Response(body, response_options);
  172. resolve(output);
  173. return;
  174. // for deflate
  175. } else if (name == 'deflate' || name == 'x-deflate') {
  176. // handle the infamous raw deflate response from old servers
  177. // a hack for old IIS and Apache servers
  178. var raw = res.pipe(new stream.PassThrough());
  179. raw.once('data', function(chunk) {
  180. // see http://stackoverflow.com/questions/37519828
  181. if ((chunk[0] & 0x0F) === 0x08) {
  182. body = body.pipe(zlib.createInflate());
  183. } else {
  184. body = body.pipe(zlib.createInflateRaw());
  185. }
  186. output = new Response(body, response_options);
  187. resolve(output);
  188. });
  189. return;
  190. }
  191. // otherwise, use response as-is
  192. output = new Response(body, response_options);
  193. resolve(output);
  194. return;
  195. });
  196. // accept string, buffer or readable stream as body
  197. // per spec we will call tostring on non-stream objects
  198. if (typeof options.body === 'string') {
  199. req.write(options.body);
  200. req.end();
  201. } else if (options.body instanceof Buffer) {
  202. req.write(options.body);
  203. req.end();
  204. } else if (typeof options.body === 'object' && options.body.pipe) {
  205. options.body.pipe(req);
  206. } else if (typeof options.body === 'object') {
  207. req.write(options.body.toString());
  208. req.end();
  209. } else {
  210. req.end();
  211. }
  212. });
  213. };
  214. /**
  215. * Redirect code matching
  216. *
  217. * @param Number code Status code
  218. * @return Boolean
  219. */
  220. Fetch.prototype.isRedirect = function(code) {
  221. return code === 301 || code === 302 || code === 303 || code === 307 || code === 308;
  222. }
  223. // expose Promise
  224. Fetch.Promise = global.Promise;
  225. Fetch.Response = Response;
  226. Fetch.Headers = Headers;
  227. Fetch.Request = Request;