superagent 3.8.1 → 4.0.0-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/node/index.js CHANGED
@@ -15,7 +15,6 @@ let methods = require('methods');
15
15
  const Stream = require('stream');
16
16
  const utils = require('../utils');
17
17
  const unzip = require('./unzip').unzip;
18
- const extend = require('extend');
19
18
  const mime = require('mime');
20
19
  const https = require('https');
21
20
  const http = require('http');
@@ -25,6 +24,13 @@ const zlib = require('zlib');
25
24
  const util = require('util');
26
25
  const pkg = require('../../package.json');
27
26
  const RequestBase = require('../request-base');
27
+ const CookieJar = require('cookiejar');
28
+ let http2;
29
+ try {
30
+ http2 = require('./http2wrapper');
31
+ console.warn('superagent: Enable experimental feature http2');
32
+ } catch (_) {
33
+ }
28
34
 
29
35
  function request(method, url) {
30
36
  // callback
@@ -80,6 +86,7 @@ mime.define({
80
86
  exports.protocols = {
81
87
  'http:': http,
82
88
  'https:': https,
89
+ 'http2:': http2,
83
90
  };
84
91
 
85
92
  /**
@@ -107,6 +114,15 @@ exports.serialize = {
107
114
 
108
115
  exports.parse = require('./parsers');
109
116
 
117
+
118
+ /**
119
+ * Default buffering map. Can be used to set certain
120
+ * response types to buffer/not buffer.
121
+ *
122
+ * superagent.buffer['application/xml'] = true;
123
+ */
124
+ exports.buffer = {};
125
+
110
126
  /**
111
127
  * Initialize internal header tracking properties on a request instance.
112
128
  *
@@ -134,6 +150,7 @@ function _initHeaders(req) {
134
150
  function Request(method, url) {
135
151
  Stream.call(this);
136
152
  if ('string' != typeof url) url = format(url);
153
+ this._enableHttp2 = !!process.env.HTTP2_TEST; // internal only
137
154
  this._agent = false;
138
155
  this._formData = null;
139
156
  this.method = method;
@@ -158,13 +175,50 @@ function Request(method, url) {
158
175
  util.inherits(Request, Stream);
159
176
  RequestBase(Request.prototype);
160
177
 
178
+ /**
179
+ * Enable or Disable http2.
180
+ *
181
+ * Enable http2.
182
+ *
183
+ * ``` js
184
+ * request.get('http://localhost/')
185
+ * .http2()
186
+ * .end(callback);
187
+ *
188
+ * request.get('http://localhost/')
189
+ * .http2(true)
190
+ * .end(callback);
191
+ * ```
192
+ *
193
+ * Disable http2.
194
+ *
195
+ * ``` js
196
+ * request = request.http2();
197
+ * request.get('http://localhost/')
198
+ * .http2(false)
199
+ * .end(callback);
200
+ * ```
201
+ *
202
+ * @param {Boolean} enable
203
+ * @return {Request} for chaining
204
+ * @api public
205
+ */
206
+
207
+ Request.prototype.http2 = function(bool){
208
+ if (exports.protocols['http2:'] === undefined){
209
+ throw new Error('superagent: does not support http2');
210
+ }
211
+ this._enableHttp2 = bool === undefined ? true : bool;
212
+ return this;
213
+ }
214
+
161
215
  /**
162
216
  * Queue the given `file` as an attachment to the specified `field`,
163
217
  * with optional `options` (or filename).
164
218
  *
165
219
  * ``` js
166
220
  * request.post('http://localhost/upload')
167
- * .attach(new Buffer('<b>Hello world</b>'), 'hello.html')
221
+ * .attach('field', Buffer.from('<b>Hello world</b>'), 'hello.html')
168
222
  * .end(callback);
169
223
  * ```
170
224
  *
@@ -234,7 +288,7 @@ Request.prototype.agent = function(agent){
234
288
  };
235
289
 
236
290
  /**
237
- * Set _Content-Type_ response header passed through `mime.lookup()`.
291
+ * Set _Content-Type_ response header passed through `mime.getType()`.
238
292
  *
239
293
  * Examples:
240
294
  *
@@ -261,12 +315,12 @@ Request.prototype.agent = function(agent){
261
315
  Request.prototype.type = function(type) {
262
316
  return this.set(
263
317
  'Content-Type',
264
- ~type.indexOf('/') ? type : mime.lookup(type)
318
+ ~type.indexOf('/') ? type : mime.getType(type)
265
319
  );
266
320
  };
267
321
 
268
322
  /**
269
- * Set _Accept_ response header passed through `mime.lookup()`.
323
+ * Set _Accept_ response header passed through `mime.getType()`.
270
324
  *
271
325
  * Examples:
272
326
  *
@@ -288,7 +342,7 @@ Request.prototype.type = function(type) {
288
342
  Request.prototype.accept = function(type){
289
343
  return this.set('Accept', ~type.indexOf('/')
290
344
  ? type
291
- : mime.lookup(type));
345
+ : mime.getType(type));
292
346
  };
293
347
 
294
348
  /**
@@ -309,7 +363,7 @@ Request.prototype.query = function(val){
309
363
  if ('string' == typeof val) {
310
364
  this._query.push(val);
311
365
  } else {
312
- extend(this.qs, val);
366
+ Object.assign(this.qs, val);
313
367
  }
314
368
  return this;
315
369
  };
@@ -494,9 +548,7 @@ Request.prototype.auth = function(user, pass, options){
494
548
  options = { type: 'basic' };
495
549
  }
496
550
 
497
- var encoder = function(string) {
498
- return new Buffer(string).toString('base64');
499
- };
551
+ const encoder = string => new Buffer.from(string).toString('base64');
500
552
 
501
553
  return this._auth(user, pass, options, encoder);
502
554
  };
@@ -587,10 +639,31 @@ Request.prototype.request = function(){
587
639
  let url = this.url;
588
640
  const retries = this._retries;
589
641
 
642
+ // Capture backticks as-is from the final query string built above.
643
+ // Note: this'll only find backticks entered in req.query(String)
644
+ // calls, because qs.stringify unconditionally encodes backticks.
645
+ let queryStringBackticks;
646
+ if(url.indexOf('`') > -1) {
647
+ const queryStartIndex = url.indexOf("?");
648
+
649
+ if(queryStartIndex !== -1) {
650
+ const queryString = url.substr(queryStartIndex + 1);
651
+ queryStringBackticks = queryString.match(/`|\%60/g);
652
+ }
653
+ }
654
+
590
655
  // default to http://
591
656
  if (0 != url.indexOf('http')) url = `http://${url}`;
592
657
  url = parse(url);
593
658
 
659
+ // See https://github.com/visionmedia/superagent/issues/1367
660
+ if(queryStringBackticks) {
661
+ let i = 0;
662
+ url.query = url.query.replace(/\%60/g, () => queryStringBackticks[i++]);
663
+ url.search = `?${url.query}`;
664
+ url.path = url.pathname + url.search;
665
+ }
666
+
594
667
  // support unix sockets
595
668
  if (/^https?\+unix:/.test(url.protocol) === true) {
596
669
  // get the protocol
@@ -614,8 +687,13 @@ Request.prototype.request = function(){
614
687
  options.passphrase = this._passphrase;
615
688
  options.agent = this._agent;
616
689
 
690
+ // Allows request.get('https://1.2.3.4/').set('Host', 'example.com')
691
+ if (this._header['host']) {
692
+ options.servername = this._header['host'].replace(/:[0-9]+$/,'');
693
+ }
694
+
617
695
  // initiate request
618
- const mod = exports.protocols[url.protocol];
696
+ const mod = this._enableHttp2 ? exports.protocols['http2:'].setProtocol(url.protocol) : exports.protocols[url.protocol];
619
697
 
620
698
  // request
621
699
  const req = (this.req = mod.request(options));
@@ -632,7 +710,7 @@ Request.prototype.request = function(){
632
710
  // expose events
633
711
  req.once('drain', () => { this.emit('drain'); });
634
712
 
635
- req.once('error', err => {
713
+ req.on('error', err => {
636
714
  // flag abortion here for out timeouts
637
715
  // because node will emit a faux-error "socket hang up"
638
716
  // when request is aborted before a connection is made
@@ -654,15 +732,24 @@ Request.prototype.request = function(){
654
732
  if (this.username && this.password) {
655
733
  this.auth(this.username, this.password);
656
734
  }
657
-
658
- // add cookies
659
- if (this.cookies) req.setHeader('Cookie', this.cookies);
660
-
661
735
  for (const key in this.header) {
662
736
  if (this.header.hasOwnProperty(key))
663
737
  req.setHeader(key, this.header[key]);
664
738
  }
665
739
 
740
+ // add cookies
741
+ if (this.cookies) {
742
+ if(this._header.hasOwnProperty('cookie')) {
743
+ // merge
744
+ const tmpJar = new CookieJar.CookieJar();
745
+ tmpJar.setCookies(this._header.cookie.split(';'));
746
+ tmpJar.setCookies(this.cookies.split(';'));
747
+ req.setHeader('Cookie',tmpJar.getCookies(CookieJar.CookieAccessInfo.All).toValueString());
748
+ } else {
749
+ req.setHeader('Cookie', this.cookies);
750
+ }
751
+ }
752
+
666
753
  return req;
667
754
  };
668
755
 
@@ -688,20 +775,23 @@ Request.prototype.callback = function(err, res){
688
775
 
689
776
  if (!err) {
690
777
  try {
691
- if (this._isResponseOK(res)) {
692
- return fn(err, res);
693
- }
694
-
695
- let msg = 'Unsuccessful HTTP response';
696
- if (res) {
697
- msg = http.STATUS_CODES[res.status] || msg;
778
+ if (!this._isResponseOK(res)) {
779
+ let msg = 'Unsuccessful HTTP response';
780
+ if (res) {
781
+ msg = http.STATUS_CODES[res.status] || msg;
782
+ }
783
+ err = new Error(msg);
784
+ err.status = res ? res.status : undefined;
698
785
  }
699
- err = new Error(msg);
700
- err.status = res ? res.status : undefined;
701
786
  } catch (new_err) {
702
787
  err = new_err;
703
788
  }
704
789
  }
790
+ // It's important that the callback is called outside try/catch
791
+ // to avoid double callback
792
+ if (!err) {
793
+ return fn(null, res);
794
+ }
705
795
 
706
796
  err.response = res;
707
797
  if (this._maxRetries) err.retries = this._retries - 1;
@@ -743,6 +833,11 @@ Request.prototype._emitResponse = function(body, files) {
743
833
  response.body = body;
744
834
  }
745
835
  response.files = files;
836
+ if (this._endCalled) {
837
+ response.pipe = function() {
838
+ throw Error("end() has already been called, so it's too late to start piping");
839
+ }
840
+ }
746
841
  this.emit('response', response);
747
842
  return response;
748
843
  };
@@ -752,8 +847,8 @@ Request.prototype.end = function(fn) {
752
847
  debug('%s %s', this.method, this.url);
753
848
 
754
849
  if (this._endCalled) {
755
- console.warn(
756
- 'Warning: .end() was called twice. This is not supported in superagent'
850
+ throw Error(
851
+ '.end() was called twice. This is not supported in superagent'
757
852
  );
758
853
  }
759
854
  this._endCalled = true;
@@ -761,13 +856,14 @@ Request.prototype.end = function(fn) {
761
856
  // store callback
762
857
  this._callback = fn || noop;
763
858
 
764
- return this._end();
859
+ this._end();
765
860
  };
766
861
 
767
862
  Request.prototype._end = function() {
863
+ if (this._aborted) return this.callback(Error("The request has been aborted even before .end() was called"));
864
+
768
865
  let data = this._data;
769
866
  const req = this.req;
770
- let buffer = this._buffer;
771
867
  const method = this.method;
772
868
 
773
869
  this._setTimeouts();
@@ -779,7 +875,7 @@ Request.prototype._end = function() {
779
875
  let contentType = req.getHeader('Content-Type');
780
876
  // Parse out just the content type from the header (ignore the charset)
781
877
  if (contentType) contentType = contentType.split(';')[0];
782
- let serialize = exports.serialize[contentType];
878
+ let serialize = this._serializer || exports.serialize[contentType];
783
879
  if (!serialize && isJSON(contentType)) {
784
880
  serialize = exports.serialize['application/json'];
785
881
  }
@@ -809,7 +905,6 @@ Request.prototype._end = function() {
809
905
  const type = mime.split('/')[0];
810
906
  const multipart = 'multipart' == type;
811
907
  const redirect = isRedirect(res.statusCode);
812
- let parser = this._parser;
813
908
  const responseType = this._responseType;
814
909
 
815
910
  this.res = res;
@@ -830,6 +925,19 @@ Request.prototype._end = function() {
830
925
  unzip(req, res);
831
926
  }
832
927
 
928
+ let buffer = this._buffer;
929
+ if (buffer === undefined && mime in exports.buffer){
930
+ buffer = !!exports.buffer[mime];
931
+ }
932
+
933
+ let parser = this._parser;
934
+ if (undefined === buffer) {
935
+ if (parser) {
936
+ console.warn("A custom superagent parser has been set, but buffering strategy for the parser hasn't been configured. Call `req.buffer(true or false)` or set `superagent.buffer[mime] = true or false`");
937
+ buffer = true;
938
+ }
939
+ }
940
+
833
941
  if (!parser) {
834
942
  if (responseType) {
835
943
  parser = exports.parse.image; // It's actually a generic Buffer
@@ -853,6 +961,9 @@ Request.prototype._end = function() {
853
961
  buffer = (buffer !== false);
854
962
  } else if (buffer) {
855
963
  parser = exports.parse.text;
964
+ } else if (undefined === buffer) {
965
+ parser = exports.parse.image; // It's actually a generic Buffer
966
+ buffer = true;
856
967
  }
857
968
  }
858
969
 
@@ -861,6 +972,7 @@ Request.prototype._end = function() {
861
972
  buffer = true;
862
973
  }
863
974
 
975
+ this._resBuffered = buffer;
864
976
  let parserHandlesEnd = false;
865
977
  if (buffer) {
866
978
  // Protectiona against zip bombs and other nuisance
@@ -939,6 +1051,47 @@ Request.prototype._end = function() {
939
1051
 
940
1052
  this.emit('request', this);
941
1053
 
1054
+ const getProgressMonitor = () => {
1055
+ const lengthComputable = true;
1056
+ const total = req.getHeader('Content-Length');
1057
+ let loaded = 0;
1058
+
1059
+ const progress = new Stream.Transform();
1060
+ progress._transform = (chunk, encoding, cb) => {
1061
+ loaded += chunk.length;
1062
+ this.emit('progress', {
1063
+ direction: 'upload',
1064
+ lengthComputable,
1065
+ loaded,
1066
+ total,
1067
+ });
1068
+ cb(null, chunk);
1069
+ };
1070
+ return progress;
1071
+ };
1072
+
1073
+ const bufferToChunks = (buffer) => {
1074
+ const chunkSize = 16 * 1024; // default highWaterMark value
1075
+ const chunking = new Stream.Readable();
1076
+ const totalLength = buffer.length;
1077
+ const remainder = totalLength % chunkSize;
1078
+ const cutoff = totalLength - remainder;
1079
+
1080
+ for (let i = 0; i < cutoff; i += chunkSize) {
1081
+ const chunk = buffer.slice(i, i + chunkSize);
1082
+ chunking.push(chunk);
1083
+ }
1084
+
1085
+ if (remainder > 0) {
1086
+ const remainderBuffer = buffer.slice(-remainder);
1087
+ chunking.push(remainderBuffer);
1088
+ }
1089
+
1090
+ chunking.push(null); // no more data
1091
+
1092
+ return chunking;
1093
+ }
1094
+
942
1095
  // if a FormData instance got created, then we send that as the request body
943
1096
  const formData = this._formData;
944
1097
  if (formData) {
@@ -959,31 +1112,13 @@ Request.prototype._end = function() {
959
1112
  req.setHeader('Content-Length', length);
960
1113
  }
961
1114
 
962
- const getProgressMonitor = () => {
963
- const lengthComputable = true;
964
- const total = req.getHeader('Content-Length');
965
- let loaded = 0;
966
-
967
- const progress = new Stream.Transform();
968
- progress._transform = (chunk, encoding, cb) => {
969
- loaded += chunk.length;
970
- this.emit('progress', {
971
- direction: 'upload',
972
- lengthComputable,
973
- loaded,
974
- total,
975
- });
976
- cb(null, chunk);
977
- };
978
- return progress;
979
- };
980
1115
  formData.pipe(getProgressMonitor()).pipe(req);
981
1116
  });
1117
+ } else if (Buffer.isBuffer(data)) {
1118
+ bufferToChunks(data).pipe(getProgressMonitor()).pipe(req);
982
1119
  } else {
983
1120
  req.end(data);
984
1121
  }
985
-
986
- return this;
987
1122
  };
988
1123
 
989
1124
  /**
@@ -1064,7 +1199,9 @@ function isImageOrVideo(mime) {
1064
1199
  */
1065
1200
 
1066
1201
  function isJSON(mime) {
1067
- return /[\/+]json\b/.test(mime);
1202
+ // should match /json or +json
1203
+ // but not /json-seq
1204
+ return /[\/+]json($|[^-\w])/.test(mime);
1068
1205
  }
1069
1206
 
1070
1207
  /**
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- module.exports = function(res, fn){
3
+ module.exports = (res, fn) => {
4
4
  res.text = '';
5
5
  res.setEncoding('utf8');
6
6
  res.on('data', chunk => {
@@ -6,7 +6,7 @@
6
6
 
7
7
  const qs = require('qs');
8
8
 
9
- module.exports = function(res, fn){
9
+ module.exports = (res, fn) => {
10
10
  res.text = '';
11
11
  res.setEncoding('ascii');
12
12
  res.on('data', chunk => {
@@ -36,7 +36,7 @@ function Response(req) {
36
36
  this.text = res.text;
37
37
  this.body = res.body !== undefined ? res.body : {};
38
38
  this.files = res.files || {};
39
- this.buffered = 'string' == typeof this.text;
39
+ this.buffered = req._resBuffered;
40
40
  this.header = this.headers = res.headers;
41
41
  this._setStatusProperties(res.statusCode);
42
42
  this._setHeaderProperties(this.header);
package/lib/node/unzip.js CHANGED
@@ -59,9 +59,9 @@ exports.unzip = (req, res) => {
59
59
  const _on = res.on;
60
60
  res.on = function(type, fn) {
61
61
  if ('data' == type || 'end' == type) {
62
- stream.on(type, fn);
62
+ stream.on(type, fn.bind(res));
63
63
  } else if ('error' == type) {
64
- stream.on(type, fn);
64
+ stream.on(type, fn.bind(res));
65
65
  _on.call(res, type, fn);
66
66
  } else {
67
67
  _on.call(res, type, fn);