index.js 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  1. 'use strict';
  2. var EventEmitter = require('events').EventEmitter;
  3. var http = require('http');
  4. var https = require('https');
  5. var urlLib = require('url');
  6. var querystring = require('querystring');
  7. var objectAssign = require('object-assign');
  8. var PassThrough = require('readable-stream').PassThrough;
  9. var duplexer2 = require('duplexer2');
  10. var isStream = require('is-stream');
  11. var readAllStream = require('read-all-stream');
  12. var timedOut = require('timed-out');
  13. var urlParseLax = require('url-parse-lax');
  14. var lowercaseKeys = require('lowercase-keys');
  15. var isRedirect = require('is-redirect');
  16. var PinkiePromise = require('pinkie-promise');
  17. var unzipResponse = require('unzip-response');
  18. var createErrorClass = require('create-error-class');
  19. var nodeStatusCodes = require('node-status-codes');
  20. var parseJson = require('parse-json');
  21. var isRetryAllowed = require('is-retry-allowed');
  22. var pkg = require('./package.json');
  23. function requestAsEventEmitter(opts) {
  24. opts = opts || {};
  25. var ee = new EventEmitter();
  26. var requestUrl = opts.href || urlLib.resolve(urlLib.format(opts), opts.path);
  27. var redirectCount = 0;
  28. var retryCount = 0;
  29. var redirectUrl;
  30. var get = function (opts) {
  31. var fn = opts.protocol === 'https:' ? https : http;
  32. var req = fn.request(opts, function (res) {
  33. var statusCode = res.statusCode;
  34. if (isRedirect(statusCode) && opts.followRedirect && 'location' in res.headers && (opts.method === 'GET' || opts.method === 'HEAD')) {
  35. res.resume();
  36. if (++redirectCount > 10) {
  37. ee.emit('error', new got.MaxRedirectsError(statusCode, opts), null, res);
  38. return;
  39. }
  40. redirectUrl = urlLib.resolve(urlLib.format(opts), res.headers.location);
  41. var redirectOpts = objectAssign({}, opts, urlLib.parse(redirectUrl));
  42. ee.emit('redirect', res, redirectOpts);
  43. get(redirectOpts);
  44. return;
  45. }
  46. // do not write ee.bind(...) instead of function - it will break gzip in Node.js 0.10
  47. setImmediate(function () {
  48. var response = typeof unzipResponse === 'function' && req.method !== 'HEAD' ? unzipResponse(res) : res;
  49. response.url = redirectUrl || requestUrl;
  50. response.requestUrl = requestUrl;
  51. ee.emit('response', response);
  52. });
  53. });
  54. req.once('error', function (err) {
  55. var backoff = opts.retries(++retryCount, err);
  56. if (backoff) {
  57. setTimeout(get, backoff, opts);
  58. return;
  59. }
  60. ee.emit('error', new got.RequestError(err, opts));
  61. });
  62. if (opts.timeout) {
  63. timedOut(req, opts.timeout);
  64. }
  65. setImmediate(ee.emit.bind(ee), 'request', req);
  66. };
  67. get(opts);
  68. return ee;
  69. }
  70. function asCallback(opts, cb) {
  71. var ee = requestAsEventEmitter(opts);
  72. ee.on('request', function (req) {
  73. if (isStream(opts.body)) {
  74. opts.body.pipe(req);
  75. opts.body = undefined;
  76. return;
  77. }
  78. req.end(opts.body);
  79. });
  80. ee.on('response', function (res) {
  81. readAllStream(res, opts.encoding, function (error, data) {
  82. var statusCode = res.statusCode;
  83. var limitStatusCode = opts.followRedirect ? 299 : 399;
  84. if (error) {
  85. cb(new got.ReadError(error, opts), null, res);
  86. return;
  87. }
  88. if (statusCode < 200 || statusCode > limitStatusCode) {
  89. error = new got.HTTPError(statusCode, opts);
  90. }
  91. if (opts.json && data) {
  92. try {
  93. data = parseJson(data);
  94. } catch (err) {
  95. err.fileName = urlLib.format(opts);
  96. error = new got.ParseError(err, statusCode, opts);
  97. }
  98. }
  99. cb(error, data, res);
  100. });
  101. });
  102. ee.on('error', cb);
  103. }
  104. function asPromise(opts) {
  105. return new PinkiePromise(function (resolve, reject) {
  106. asCallback(opts, function (err, data, response) {
  107. if (response) {
  108. response.body = data;
  109. }
  110. if (err) {
  111. Object.defineProperty(err, 'response', {
  112. value: response,
  113. enumerable: false
  114. });
  115. reject(err);
  116. return;
  117. }
  118. resolve(response);
  119. });
  120. });
  121. }
  122. function asStream(opts) {
  123. var input = new PassThrough();
  124. var output = new PassThrough();
  125. var proxy = duplexer2(input, output);
  126. if (opts.json) {
  127. throw new Error('got can not be used as stream when options.json is used');
  128. }
  129. if (opts.body) {
  130. proxy.write = function () {
  131. throw new Error('got\'s stream is not writable when options.body is used');
  132. };
  133. }
  134. var ee = requestAsEventEmitter(opts);
  135. ee.on('request', function (req) {
  136. proxy.emit('request', req);
  137. if (isStream(opts.body)) {
  138. opts.body.pipe(req);
  139. return;
  140. }
  141. if (opts.body) {
  142. req.end(opts.body);
  143. return;
  144. }
  145. if (opts.method === 'POST' || opts.method === 'PUT' || opts.method === 'PATCH') {
  146. input.pipe(req);
  147. return;
  148. }
  149. req.end();
  150. });
  151. ee.on('response', function (res) {
  152. var statusCode = res.statusCode;
  153. var limitStatusCode = opts.followRedirect ? 299 : 399;
  154. res.pipe(output);
  155. if (statusCode < 200 || statusCode > limitStatusCode) {
  156. proxy.emit('error', new got.HTTPError(statusCode, opts), null, res);
  157. return;
  158. }
  159. proxy.emit('response', res);
  160. });
  161. ee.on('redirect', proxy.emit.bind(proxy, 'redirect'));
  162. ee.on('error', proxy.emit.bind(proxy, 'error'));
  163. return proxy;
  164. }
  165. function normalizeArguments(url, opts) {
  166. if (typeof url !== 'string' && typeof url !== 'object') {
  167. throw new Error('Parameter `url` must be a string or object, not ' + typeof url);
  168. }
  169. if (typeof url === 'string') {
  170. url = url.replace(/^unix:/, 'http://$&');
  171. url = urlParseLax(url);
  172. if (url.auth) {
  173. throw new Error('Basic authentication must be done with auth option');
  174. }
  175. }
  176. opts = objectAssign(
  177. {protocol: 'http:', path: '', retries: 5},
  178. url,
  179. opts
  180. );
  181. opts.headers = objectAssign({
  182. 'user-agent': pkg.name + '/' + pkg.version + ' (https://github.com/sindresorhus/got)',
  183. 'accept-encoding': 'gzip,deflate'
  184. }, lowercaseKeys(opts.headers));
  185. var query = opts.query;
  186. if (query) {
  187. if (typeof query !== 'string') {
  188. opts.query = querystring.stringify(query);
  189. }
  190. opts.path = opts.path.split('?')[0] + '?' + opts.query;
  191. delete opts.query;
  192. }
  193. if (opts.json && opts.headers.accept === undefined) {
  194. opts.headers.accept = 'application/json';
  195. }
  196. var body = opts.body;
  197. if (body) {
  198. if (typeof body !== 'string' && !(body !== null && typeof body === 'object')) {
  199. throw new Error('options.body must be a ReadableStream, string, Buffer or plain Object');
  200. }
  201. opts.method = opts.method || 'POST';
  202. if (isStream(body) && typeof body.getBoundary === 'function') {
  203. // Special case for https://github.com/form-data/form-data
  204. opts.headers['content-type'] = opts.headers['content-type'] || 'multipart/form-data; boundary=' + body.getBoundary();
  205. } else if (body !== null && typeof body === 'object' && !Buffer.isBuffer(body) && !isStream(body)) {
  206. opts.headers['content-type'] = opts.headers['content-type'] || 'application/x-www-form-urlencoded';
  207. body = opts.body = querystring.stringify(body);
  208. }
  209. if (opts.headers['content-length'] === undefined && opts.headers['transfer-encoding'] === undefined && !isStream(body)) {
  210. var length = typeof body === 'string' ? Buffer.byteLength(body) : body.length;
  211. opts.headers['content-length'] = length;
  212. }
  213. }
  214. opts.method = opts.method || 'GET';
  215. opts.method = opts.method.toUpperCase();
  216. if (opts.hostname === 'unix') {
  217. var matches = /(.+):(.+)/.exec(opts.path);
  218. if (matches) {
  219. opts.socketPath = matches[1];
  220. opts.path = matches[2];
  221. opts.host = null;
  222. }
  223. }
  224. if (typeof opts.retries !== 'function') {
  225. var retries = opts.retries;
  226. opts.retries = function backoff(iter, err) {
  227. if (iter > retries || !isRetryAllowed(err)) {
  228. return 0;
  229. }
  230. var noise = Math.random() * 100;
  231. return ((1 << iter) * 1000) + noise;
  232. };
  233. }
  234. if (opts.followRedirect === undefined) {
  235. opts.followRedirect = true;
  236. }
  237. return opts;
  238. }
  239. function got(url, opts, cb) {
  240. if (typeof opts === 'function') {
  241. cb = opts;
  242. opts = {};
  243. }
  244. if (cb) {
  245. asCallback(normalizeArguments(url, opts), cb);
  246. return null;
  247. }
  248. try {
  249. return asPromise(normalizeArguments(url, opts));
  250. } catch (err) {
  251. return PinkiePromise.reject(err);
  252. }
  253. }
  254. var helpers = [
  255. 'get',
  256. 'post',
  257. 'put',
  258. 'patch',
  259. 'head',
  260. 'delete'
  261. ];
  262. helpers.forEach(function (el) {
  263. got[el] = function (url, opts, cb) {
  264. if (typeof opts === 'function') {
  265. cb = opts;
  266. opts = {};
  267. }
  268. return got(url, objectAssign({}, opts, {method: el}), cb);
  269. };
  270. });
  271. got.stream = function (url, opts, cb) {
  272. if (cb || typeof opts === 'function') {
  273. throw new Error('callback can not be used with stream mode');
  274. }
  275. return asStream(normalizeArguments(url, opts));
  276. };
  277. helpers.forEach(function (el) {
  278. got.stream[el] = function (url, opts, cb) {
  279. if (typeof opts === 'function') {
  280. cb = opts;
  281. opts = {};
  282. }
  283. return got.stream(url, objectAssign({}, opts, {method: el}), cb);
  284. };
  285. });
  286. function stdError(error, opts) {
  287. if (error.code !== undefined) {
  288. this.code = error.code;
  289. }
  290. objectAssign(this, {
  291. message: error.message,
  292. host: opts.host,
  293. hostname: opts.hostname,
  294. method: opts.method,
  295. path: opts.path
  296. });
  297. }
  298. got.RequestError = createErrorClass('RequestError', stdError);
  299. got.ReadError = createErrorClass('ReadError', stdError);
  300. got.ParseError = createErrorClass('ParseError', function (e, statusCode, opts) {
  301. stdError.call(this, e, opts);
  302. this.statusCode = statusCode;
  303. this.statusMessage = nodeStatusCodes[this.statusCode];
  304. });
  305. got.HTTPError = createErrorClass('HTTPError', function (statusCode, opts) {
  306. stdError.call(this, {}, opts);
  307. this.statusCode = statusCode;
  308. this.statusMessage = nodeStatusCodes[this.statusCode];
  309. this.message = 'Response code ' + this.statusCode + ' (' + this.statusMessage + ')';
  310. });
  311. got.MaxRedirectsError = createErrorClass('MaxRedirectsError', function (statusCode, opts) {
  312. stdError.call(this, {}, opts);
  313. this.statusCode = statusCode;
  314. this.statusMessage = nodeStatusCodes[this.statusCode];
  315. this.message = 'Redirected 10 times. Aborting.';
  316. });
  317. module.exports = got;