request-base.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694
  1. 'use strict';
  2. /**
  3. * Module of mixed-in functions shared between node and client code
  4. */
  5. var isObject = require('./is-object');
  6. /**
  7. * Expose `RequestBase`.
  8. */
  9. module.exports = RequestBase;
  10. /**
  11. * Initialize a new `RequestBase`.
  12. *
  13. * @api public
  14. */
  15. function RequestBase(obj) {
  16. if (obj) return mixin(obj);
  17. }
  18. /**
  19. * Mixin the prototype properties.
  20. *
  21. * @param {Object} obj
  22. * @return {Object}
  23. * @api private
  24. */
  25. function mixin(obj) {
  26. for (var key in RequestBase.prototype) {
  27. obj[key] = RequestBase.prototype[key];
  28. }
  29. return obj;
  30. }
  31. /**
  32. * Clear previous timeout.
  33. *
  34. * @return {Request} for chaining
  35. * @api public
  36. */
  37. RequestBase.prototype.clearTimeout = function _clearTimeout(){
  38. clearTimeout(this._timer);
  39. clearTimeout(this._responseTimeoutTimer);
  40. delete this._timer;
  41. delete this._responseTimeoutTimer;
  42. return this;
  43. };
  44. /**
  45. * Override default response body parser
  46. *
  47. * This function will be called to convert incoming data into request.body
  48. *
  49. * @param {Function}
  50. * @api public
  51. */
  52. RequestBase.prototype.parse = function parse(fn){
  53. this._parser = fn;
  54. return this;
  55. };
  56. /**
  57. * Set format of binary response body.
  58. * In browser valid formats are 'blob' and 'arraybuffer',
  59. * which return Blob and ArrayBuffer, respectively.
  60. *
  61. * In Node all values result in Buffer.
  62. *
  63. * Examples:
  64. *
  65. * req.get('/')
  66. * .responseType('blob')
  67. * .end(callback);
  68. *
  69. * @param {String} val
  70. * @return {Request} for chaining
  71. * @api public
  72. */
  73. RequestBase.prototype.responseType = function(val){
  74. this._responseType = val;
  75. return this;
  76. };
  77. /**
  78. * Override default request body serializer
  79. *
  80. * This function will be called to convert data set via .send or .attach into payload to send
  81. *
  82. * @param {Function}
  83. * @api public
  84. */
  85. RequestBase.prototype.serialize = function serialize(fn){
  86. this._serializer = fn;
  87. return this;
  88. };
  89. /**
  90. * Set timeouts.
  91. *
  92. * - response timeout is time between sending request and receiving the first byte of the response. Includes DNS and connection time.
  93. * - deadline is the time from start of the request to receiving response body in full. If the deadline is too short large files may not load at all on slow connections.
  94. *
  95. * Value of 0 or false means no timeout.
  96. *
  97. * @param {Number|Object} ms or {response, deadline}
  98. * @return {Request} for chaining
  99. * @api public
  100. */
  101. RequestBase.prototype.timeout = function timeout(options){
  102. if (!options || 'object' !== typeof options) {
  103. this._timeout = options;
  104. this._responseTimeout = 0;
  105. return this;
  106. }
  107. for(var option in options) {
  108. switch(option) {
  109. case 'deadline':
  110. this._timeout = options.deadline;
  111. break;
  112. case 'response':
  113. this._responseTimeout = options.response;
  114. break;
  115. default:
  116. console.warn("Unknown timeout option", option);
  117. }
  118. }
  119. return this;
  120. };
  121. /**
  122. * Set number of retry attempts on error.
  123. *
  124. * Failed requests will be retried 'count' times if timeout or err.code >= 500.
  125. *
  126. * @param {Number} count
  127. * @param {Function} [fn]
  128. * @return {Request} for chaining
  129. * @api public
  130. */
  131. RequestBase.prototype.retry = function retry(count, fn){
  132. // Default to 1 if no count passed or true
  133. if (arguments.length === 0 || count === true) count = 1;
  134. if (count <= 0) count = 0;
  135. this._maxRetries = count;
  136. this._retries = 0;
  137. this._retryCallback = fn;
  138. return this;
  139. };
  140. var ERROR_CODES = [
  141. 'ECONNRESET',
  142. 'ETIMEDOUT',
  143. 'EADDRINFO',
  144. 'ESOCKETTIMEDOUT'
  145. ];
  146. /**
  147. * Determine if a request should be retried.
  148. * (Borrowed from segmentio/superagent-retry)
  149. *
  150. * @param {Error} err
  151. * @param {Response} [res]
  152. * @returns {Boolean}
  153. */
  154. RequestBase.prototype._shouldRetry = function(err, res) {
  155. if (!this._maxRetries || this._retries++ >= this._maxRetries) {
  156. return false;
  157. }
  158. if (this._retryCallback) {
  159. try {
  160. var override = this._retryCallback(err, res);
  161. if (override === true) return true;
  162. if (override === false) return false;
  163. // undefined falls back to defaults
  164. } catch(e) {
  165. console.error(e);
  166. }
  167. }
  168. if (res && res.status && res.status >= 500 && res.status != 501) return true;
  169. if (err) {
  170. if (err.code && ~ERROR_CODES.indexOf(err.code)) return true;
  171. // Superagent timeout
  172. if (err.timeout && err.code == 'ECONNABORTED') return true;
  173. if (err.crossDomain) return true;
  174. }
  175. return false;
  176. };
  177. /**
  178. * Retry request
  179. *
  180. * @return {Request} for chaining
  181. * @api private
  182. */
  183. RequestBase.prototype._retry = function() {
  184. this.clearTimeout();
  185. // node
  186. if (this.req) {
  187. this.req = null;
  188. this.req = this.request();
  189. }
  190. this._aborted = false;
  191. this.timedout = false;
  192. return this._end();
  193. };
  194. /**
  195. * Promise support
  196. *
  197. * @param {Function} resolve
  198. * @param {Function} [reject]
  199. * @return {Request}
  200. */
  201. RequestBase.prototype.then = function then(resolve, reject) {
  202. if (!this._fullfilledPromise) {
  203. var self = this;
  204. if (this._endCalled) {
  205. console.warn("Warning: superagent request was sent twice, because both .end() and .then() were called. Never call .end() if you use promises");
  206. }
  207. this._fullfilledPromise = new Promise(function(innerResolve, innerReject) {
  208. self.end(function(err, res) {
  209. if (err) innerReject(err);
  210. else innerResolve(res);
  211. });
  212. });
  213. }
  214. return this._fullfilledPromise.then(resolve, reject);
  215. };
  216. RequestBase.prototype['catch'] = function(cb) {
  217. return this.then(undefined, cb);
  218. };
  219. /**
  220. * Allow for extension
  221. */
  222. RequestBase.prototype.use = function use(fn) {
  223. fn(this);
  224. return this;
  225. };
  226. RequestBase.prototype.ok = function(cb) {
  227. if ('function' !== typeof cb) throw Error("Callback required");
  228. this._okCallback = cb;
  229. return this;
  230. };
  231. RequestBase.prototype._isResponseOK = function(res) {
  232. if (!res) {
  233. return false;
  234. }
  235. if (this._okCallback) {
  236. return this._okCallback(res);
  237. }
  238. return res.status >= 200 && res.status < 300;
  239. };
  240. /**
  241. * Get request header `field`.
  242. * Case-insensitive.
  243. *
  244. * @param {String} field
  245. * @return {String}
  246. * @api public
  247. */
  248. RequestBase.prototype.get = function(field){
  249. return this._header[field.toLowerCase()];
  250. };
  251. /**
  252. * Get case-insensitive header `field` value.
  253. * This is a deprecated internal API. Use `.get(field)` instead.
  254. *
  255. * (getHeader is no longer used internally by the superagent code base)
  256. *
  257. * @param {String} field
  258. * @return {String}
  259. * @api private
  260. * @deprecated
  261. */
  262. RequestBase.prototype.getHeader = RequestBase.prototype.get;
  263. /**
  264. * Set header `field` to `val`, or multiple fields with one object.
  265. * Case-insensitive.
  266. *
  267. * Examples:
  268. *
  269. * req.get('/')
  270. * .set('Accept', 'application/json')
  271. * .set('X-API-Key', 'foobar')
  272. * .end(callback);
  273. *
  274. * req.get('/')
  275. * .set({ Accept: 'application/json', 'X-API-Key': 'foobar' })
  276. * .end(callback);
  277. *
  278. * @param {String|Object} field
  279. * @param {String} val
  280. * @return {Request} for chaining
  281. * @api public
  282. */
  283. RequestBase.prototype.set = function(field, val){
  284. if (isObject(field)) {
  285. for (var key in field) {
  286. this.set(key, field[key]);
  287. }
  288. return this;
  289. }
  290. this._header[field.toLowerCase()] = val;
  291. this.header[field] = val;
  292. return this;
  293. };
  294. /**
  295. * Remove header `field`.
  296. * Case-insensitive.
  297. *
  298. * Example:
  299. *
  300. * req.get('/')
  301. * .unset('User-Agent')
  302. * .end(callback);
  303. *
  304. * @param {String} field
  305. */
  306. RequestBase.prototype.unset = function(field){
  307. delete this._header[field.toLowerCase()];
  308. delete this.header[field];
  309. return this;
  310. };
  311. /**
  312. * Write the field `name` and `val`, or multiple fields with one object
  313. * for "multipart/form-data" request bodies.
  314. *
  315. * ``` js
  316. * request.post('/upload')
  317. * .field('foo', 'bar')
  318. * .end(callback);
  319. *
  320. * request.post('/upload')
  321. * .field({ foo: 'bar', baz: 'qux' })
  322. * .end(callback);
  323. * ```
  324. *
  325. * @param {String|Object} name
  326. * @param {String|Blob|File|Buffer|fs.ReadStream} val
  327. * @return {Request} for chaining
  328. * @api public
  329. */
  330. RequestBase.prototype.field = function(name, val) {
  331. // name should be either a string or an object.
  332. if (null === name || undefined === name) {
  333. throw new Error('.field(name, val) name can not be empty');
  334. }
  335. if (this._data) {
  336. console.error(".field() can't be used if .send() is used. Please use only .send() or only .field() & .attach()");
  337. }
  338. if (isObject(name)) {
  339. for (var key in name) {
  340. this.field(key, name[key]);
  341. }
  342. return this;
  343. }
  344. if (Array.isArray(val)) {
  345. for (var i in val) {
  346. this.field(name, val[i]);
  347. }
  348. return this;
  349. }
  350. // val should be defined now
  351. if (null === val || undefined === val) {
  352. throw new Error('.field(name, val) val can not be empty');
  353. }
  354. if ('boolean' === typeof val) {
  355. val = '' + val;
  356. }
  357. this._getFormData().append(name, val);
  358. return this;
  359. };
  360. /**
  361. * Abort the request, and clear potential timeout.
  362. *
  363. * @return {Request}
  364. * @api public
  365. */
  366. RequestBase.prototype.abort = function(){
  367. if (this._aborted) {
  368. return this;
  369. }
  370. this._aborted = true;
  371. this.xhr && this.xhr.abort(); // browser
  372. this.req && this.req.abort(); // node
  373. this.clearTimeout();
  374. this.emit('abort');
  375. return this;
  376. };
  377. RequestBase.prototype._auth = function(user, pass, options, base64Encoder) {
  378. switch (options.type) {
  379. case 'basic':
  380. this.set('Authorization', 'Basic ' + base64Encoder(user + ':' + pass));
  381. break;
  382. case 'auto':
  383. this.username = user;
  384. this.password = pass;
  385. break;
  386. case 'bearer': // usage would be .auth(accessToken, { type: 'bearer' })
  387. this.set('Authorization', 'Bearer ' + user);
  388. break;
  389. }
  390. return this;
  391. };
  392. /**
  393. * Enable transmission of cookies with x-domain requests.
  394. *
  395. * Note that for this to work the origin must not be
  396. * using "Access-Control-Allow-Origin" with a wildcard,
  397. * and also must set "Access-Control-Allow-Credentials"
  398. * to "true".
  399. *
  400. * @api public
  401. */
  402. RequestBase.prototype.withCredentials = function(on) {
  403. // This is browser-only functionality. Node side is no-op.
  404. if (on == undefined) on = true;
  405. this._withCredentials = on;
  406. return this;
  407. };
  408. /**
  409. * Set the max redirects to `n`. Does noting in browser XHR implementation.
  410. *
  411. * @param {Number} n
  412. * @return {Request} for chaining
  413. * @api public
  414. */
  415. RequestBase.prototype.redirects = function(n){
  416. this._maxRedirects = n;
  417. return this;
  418. };
  419. /**
  420. * Maximum size of buffered response body, in bytes. Counts uncompressed size.
  421. * Default 200MB.
  422. *
  423. * @param {Number} n
  424. * @return {Request} for chaining
  425. */
  426. RequestBase.prototype.maxResponseSize = function(n){
  427. if ('number' !== typeof n) {
  428. throw TypeError("Invalid argument");
  429. }
  430. this._maxResponseSize = n;
  431. return this;
  432. };
  433. /**
  434. * Convert to a plain javascript object (not JSON string) of scalar properties.
  435. * Note as this method is designed to return a useful non-this value,
  436. * it cannot be chained.
  437. *
  438. * @return {Object} describing method, url, and data of this request
  439. * @api public
  440. */
  441. RequestBase.prototype.toJSON = function() {
  442. return {
  443. method: this.method,
  444. url: this.url,
  445. data: this._data,
  446. headers: this._header,
  447. };
  448. };
  449. /**
  450. * Send `data` as the request body, defaulting the `.type()` to "json" when
  451. * an object is given.
  452. *
  453. * Examples:
  454. *
  455. * // manual json
  456. * request.post('/user')
  457. * .type('json')
  458. * .send('{"name":"tj"}')
  459. * .end(callback)
  460. *
  461. * // auto json
  462. * request.post('/user')
  463. * .send({ name: 'tj' })
  464. * .end(callback)
  465. *
  466. * // manual x-www-form-urlencoded
  467. * request.post('/user')
  468. * .type('form')
  469. * .send('name=tj')
  470. * .end(callback)
  471. *
  472. * // auto x-www-form-urlencoded
  473. * request.post('/user')
  474. * .type('form')
  475. * .send({ name: 'tj' })
  476. * .end(callback)
  477. *
  478. * // defaults to x-www-form-urlencoded
  479. * request.post('/user')
  480. * .send('name=tobi')
  481. * .send('species=ferret')
  482. * .end(callback)
  483. *
  484. * @param {String|Object} data
  485. * @return {Request} for chaining
  486. * @api public
  487. */
  488. RequestBase.prototype.send = function(data){
  489. var isObj = isObject(data);
  490. var type = this._header['content-type'];
  491. if (this._formData) {
  492. console.error(".send() can't be used if .attach() or .field() is used. Please use only .send() or only .field() & .attach()");
  493. }
  494. if (isObj && !this._data) {
  495. if (Array.isArray(data)) {
  496. this._data = [];
  497. } else if (!this._isHost(data)) {
  498. this._data = {};
  499. }
  500. } else if (data && this._data && this._isHost(this._data)) {
  501. throw Error("Can't merge these send calls");
  502. }
  503. // merge
  504. if (isObj && isObject(this._data)) {
  505. for (var key in data) {
  506. this._data[key] = data[key];
  507. }
  508. } else if ('string' == typeof data) {
  509. // default to x-www-form-urlencoded
  510. if (!type) this.type('form');
  511. type = this._header['content-type'];
  512. if ('application/x-www-form-urlencoded' == type) {
  513. this._data = this._data
  514. ? this._data + '&' + data
  515. : data;
  516. } else {
  517. this._data = (this._data || '') + data;
  518. }
  519. } else {
  520. this._data = data;
  521. }
  522. if (!isObj || this._isHost(data)) {
  523. return this;
  524. }
  525. // default to json
  526. if (!type) this.type('json');
  527. return this;
  528. };
  529. /**
  530. * Sort `querystring` by the sort function
  531. *
  532. *
  533. * Examples:
  534. *
  535. * // default order
  536. * request.get('/user')
  537. * .query('name=Nick')
  538. * .query('search=Manny')
  539. * .sortQuery()
  540. * .end(callback)
  541. *
  542. * // customized sort function
  543. * request.get('/user')
  544. * .query('name=Nick')
  545. * .query('search=Manny')
  546. * .sortQuery(function(a, b){
  547. * return a.length - b.length;
  548. * })
  549. * .end(callback)
  550. *
  551. *
  552. * @param {Function} sort
  553. * @return {Request} for chaining
  554. * @api public
  555. */
  556. RequestBase.prototype.sortQuery = function(sort) {
  557. // _sort default to true but otherwise can be a function or boolean
  558. this._sort = typeof sort === 'undefined' ? true : sort;
  559. return this;
  560. };
  561. /**
  562. * Compose querystring to append to req.url
  563. *
  564. * @api private
  565. */
  566. RequestBase.prototype._finalizeQueryString = function(){
  567. var query = this._query.join('&');
  568. if (query) {
  569. this.url += (this.url.indexOf('?') >= 0 ? '&' : '?') + query;
  570. }
  571. this._query.length = 0; // Makes the call idempotent
  572. if (this._sort) {
  573. var index = this.url.indexOf('?');
  574. if (index >= 0) {
  575. var queryArr = this.url.substring(index + 1).split('&');
  576. if ('function' === typeof this._sort) {
  577. queryArr.sort(this._sort);
  578. } else {
  579. queryArr.sort();
  580. }
  581. this.url = this.url.substring(0, index) + '?' + queryArr.join('&');
  582. }
  583. }
  584. };
  585. // For backwards compat only
  586. RequestBase.prototype._appendQueryString = function() {console.trace("Unsupported");}
  587. /**
  588. * Invoke callback with timeout error.
  589. *
  590. * @api private
  591. */
  592. RequestBase.prototype._timeoutError = function(reason, timeout, errno){
  593. if (this._aborted) {
  594. return;
  595. }
  596. var err = new Error(reason + timeout + 'ms exceeded');
  597. err.timeout = timeout;
  598. err.code = 'ECONNABORTED';
  599. err.errno = errno;
  600. this.timedout = true;
  601. this.abort();
  602. this.callback(err);
  603. };
  604. RequestBase.prototype._setTimeouts = function() {
  605. var self = this;
  606. // deadline
  607. if (this._timeout && !this._timer) {
  608. this._timer = setTimeout(function(){
  609. self._timeoutError('Timeout of ', self._timeout, 'ETIME');
  610. }, this._timeout);
  611. }
  612. // response timeout
  613. if (this._responseTimeout && !this._responseTimeoutTimer) {
  614. this._responseTimeoutTimer = setTimeout(function(){
  615. self._timeoutError('Response timeout of ', self._responseTimeout, 'ETIMEDOUT');
  616. }, this._responseTimeout);
  617. }
  618. };