superagent 4.0.0-alpha.1 → 4.1.0

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.
@@ -0,0 +1,188 @@
1
+ 'use strict';
2
+
3
+ const http2 = require('http2');
4
+ const Stream = require('stream');
5
+ const util = require('util');
6
+ const net = require('net');
7
+ const tls = require('tls');
8
+ const parse = require('url').parse;
9
+
10
+ const {
11
+ HTTP2_HEADER_PATH,
12
+ HTTP2_HEADER_STATUS,
13
+ HTTP2_HEADER_METHOD,
14
+ HTTP2_HEADER_AUTHORITY,
15
+ HTTP2_HEADER_HOST,
16
+ HTTP2_HEADER_SET_COOKIE,
17
+ NGHTTP2_CANCEL,
18
+ } = http2.constants;
19
+
20
+
21
+ function setProtocol(protocol) {
22
+ return {
23
+ request: function (options) {
24
+ return new Request(protocol, options);
25
+ }
26
+ }
27
+ }
28
+
29
+ function Request(protocol, options) {
30
+ Stream.call(this);
31
+ const defaultPort = protocol === 'https:' ? 443 : 80;
32
+ const defaultHost = 'localhost'
33
+ const port = options.port || defaultPort;
34
+ const host = options.host || defaultHost;
35
+
36
+ delete options.port
37
+ delete options.host
38
+
39
+ this.method = options.method;
40
+ this.path = options.path;
41
+ this.protocol = protocol;
42
+ this.host = host;
43
+
44
+ delete options.method
45
+ delete options.path
46
+
47
+ const sessionOptions = Object.assign({}, options);
48
+ if (options.socketPath) {
49
+ sessionOptions.socketPath = options.socketPath;
50
+ sessionOptions.createConnection = this.createUnixConnection.bind(this);
51
+ }
52
+
53
+ this._headers = {};
54
+
55
+ const session = http2.connect(`${protocol}//${host}:${port}`, sessionOptions);
56
+ this.setHeader('host', `${host}:${port}`)
57
+
58
+ session.on('error', (err) => this.emit('error', err));
59
+
60
+ this.session = session;
61
+ }
62
+
63
+ /**
64
+ * Inherit from `Stream` (which inherits from `EventEmitter`).
65
+ */
66
+ util.inherits(Request, Stream);
67
+
68
+ Request.prototype.createUnixConnection = function (authority, options) {
69
+ switch (this.protocol) {
70
+ case 'http:':
71
+ return net.connect(options.socketPath);
72
+ case 'https:':
73
+ options.ALPNProtocols = ['h2'];
74
+ options.servername = this.host;
75
+ options.allowHalfOpen = true;
76
+ return tls.connect(options.socketPath, options);
77
+ default:
78
+ throw new Error('Unsupported protocol', this.protocol);
79
+ }
80
+ }
81
+
82
+ Request.prototype.setNoDelay = function (bool) {
83
+ // We can not use setNoDelay with HTTP/2.
84
+ // Node 10 limits http2session.socket methods to ones safe to use with HTTP/2.
85
+ // See also https://nodejs.org/api/http2.html#http2_http2session_socket
86
+ }
87
+
88
+ Request.prototype.getFrame = function () {
89
+ if (this.frame) {
90
+ return this.frame;
91
+ }
92
+
93
+ const method = {
94
+ [HTTP2_HEADER_PATH]: this.path,
95
+ [HTTP2_HEADER_METHOD]: this.method,
96
+ }
97
+
98
+ let headers = this.mapToHttp2Header(this._headers);
99
+
100
+ headers = Object.assign(headers, method);
101
+
102
+ const frame = this.session.request(headers);
103
+ frame.once('response', (headers, flags) => {
104
+ headers = this.mapToHttpHeader(headers);
105
+ frame.headers = headers;
106
+ frame.status = frame.statusCode = headers[HTTP2_HEADER_STATUS];
107
+ this.emit('response', frame);
108
+ });
109
+
110
+ this._headerSent = true;
111
+
112
+ frame.once('drain', () => this.emit('drain'));
113
+ frame.on('error', (err) => this.emit('error', err));
114
+ frame.on('close', () => this.session.close());
115
+
116
+ this.frame = frame;
117
+ return frame;
118
+ }
119
+
120
+ Request.prototype.mapToHttpHeader = function (headers) {
121
+ const keys = Object.keys(headers);
122
+ const http2Headers = {};
123
+ for (var i = 0; i < keys.length; i++) {
124
+ let key = keys[i];
125
+ let value = headers[key];
126
+ key = key.toLowerCase();
127
+ switch (key) {
128
+ case HTTP2_HEADER_SET_COOKIE:
129
+ value = Array.isArray(value) ? value : [value];
130
+ break;
131
+ default:
132
+ break;
133
+ }
134
+ http2Headers[key] = value;
135
+ }
136
+ return http2Headers;
137
+ }
138
+
139
+ Request.prototype.mapToHttp2Header = function (headers) {
140
+ const keys = Object.keys(headers);
141
+ const http2Headers = {};
142
+ for (var i = 0; i < keys.length; i++) {
143
+ let key = keys[i];
144
+ let value = headers[key];
145
+ key = key.toLowerCase();
146
+ switch (key) {
147
+ case HTTP2_HEADER_HOST:
148
+ key = HTTP2_HEADER_AUTHORITY;
149
+ value = /^http\:\/\/|^https\:\/\//.test(value) ? parse(value).host : value;
150
+ break;
151
+ default:
152
+ break;
153
+ }
154
+ http2Headers[key] = value;
155
+ }
156
+ return http2Headers;
157
+ }
158
+
159
+ Request.prototype.setHeader = function (name, value) {
160
+ this._headers[name.toLowerCase()] = value;
161
+ }
162
+
163
+ Request.prototype.getHeader = function (name) {
164
+ return this._headers[name.toLowerCase()];
165
+ }
166
+
167
+ Request.prototype.write = function (data, encoding) {
168
+ const frame = this.getFrame();
169
+ return frame.write(data, encoding);
170
+ };
171
+
172
+ Request.prototype.pipe = function (stream, options) {
173
+ const frame = this.getFrame();
174
+ return frame.pipe(stream, options);
175
+ }
176
+
177
+ Request.prototype.end = function (data) {
178
+ const frame = this.getFrame();
179
+ frame.end(data);
180
+ }
181
+
182
+ Request.prototype.abort = function (data) {
183
+ const frame = this.getFrame();
184
+ frame.close(NGHTTP2_CANCEL);
185
+ this.session.destroy();
186
+ }
187
+
188
+ exports.setProtocol = setProtocol;
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');
@@ -27,6 +26,11 @@ const pkg = require('../../package.json');
27
26
  const RequestBase = require('../request-base');
28
27
  const CookieJar = require('cookiejar');
29
28
 
29
+ let http2;
30
+ try {
31
+ http2 = require('./http2wrapper');
32
+ } catch (_) {}
33
+
30
34
  function request(method, url) {
31
35
  // callback
32
36
  if ('function' == typeof url) {
@@ -81,6 +85,7 @@ mime.define({
81
85
  exports.protocols = {
82
86
  'http:': http,
83
87
  'https:': https,
88
+ 'http2:': http2,
84
89
  };
85
90
 
86
91
  /**
@@ -144,6 +149,7 @@ function _initHeaders(req) {
144
149
  function Request(method, url) {
145
150
  Stream.call(this);
146
151
  if ('string' != typeof url) url = format(url);
152
+ this._enableHttp2 = !!process.env.HTTP2_TEST; // internal only
147
153
  this._agent = false;
148
154
  this._formData = null;
149
155
  this.method = method;
@@ -168,6 +174,43 @@ function Request(method, url) {
168
174
  util.inherits(Request, Stream);
169
175
  RequestBase(Request.prototype);
170
176
 
177
+ /**
178
+ * Enable or Disable http2.
179
+ *
180
+ * Enable http2.
181
+ *
182
+ * ``` js
183
+ * request.get('http://localhost/')
184
+ * .http2()
185
+ * .end(callback);
186
+ *
187
+ * request.get('http://localhost/')
188
+ * .http2(true)
189
+ * .end(callback);
190
+ * ```
191
+ *
192
+ * Disable http2.
193
+ *
194
+ * ``` js
195
+ * request = request.http2();
196
+ * request.get('http://localhost/')
197
+ * .http2(false)
198
+ * .end(callback);
199
+ * ```
200
+ *
201
+ * @param {Boolean} enable
202
+ * @return {Request} for chaining
203
+ * @api public
204
+ */
205
+
206
+ Request.prototype.http2 = function(bool){
207
+ if (exports.protocols['http2:'] === undefined) {
208
+ throw new Error('superagent: this version of Node.js does not support http2');
209
+ }
210
+ this._enableHttp2 = bool === undefined ? true : bool;
211
+ return this;
212
+ }
213
+
171
214
  /**
172
215
  * Queue the given `file` as an attachment to the specified `field`,
173
216
  * with optional `options` (or filename).
@@ -319,7 +362,7 @@ Request.prototype.query = function(val){
319
362
  if ('string' == typeof val) {
320
363
  this._query.push(val);
321
364
  } else {
322
- extend(this.qs, val);
365
+ Object.assign(this.qs, val);
323
366
  }
324
367
  return this;
325
368
  };
@@ -504,7 +547,7 @@ Request.prototype.auth = function(user, pass, options){
504
547
  options = { type: 'basic' };
505
548
  }
506
549
 
507
- const encoder = string => new Buffer(string).toString('base64');
550
+ const encoder = string => new Buffer.from(string).toString('base64');
508
551
 
509
552
  return this._auth(user, pass, options, encoder);
510
553
  };
@@ -631,6 +674,24 @@ Request.prototype.request = function(){
631
674
  url.path = unixParts[2];
632
675
  }
633
676
 
677
+ // Override IP address of a hostname
678
+ if (this._connectOverride) {
679
+ const hostname = url.hostname;
680
+ const match = hostname in this._connectOverride ? this._connectOverride[hostname] : this._connectOverride['*'];
681
+ if (match) {
682
+ // backup the real host
683
+ if (!this._header['host']) {
684
+ this.set('host', url.host);
685
+ }
686
+ // wrap [ipv6]
687
+ url.host = /:/.test(match) ? `[${match}]` : match;
688
+ if (url.port) {
689
+ url.host += `:${url.port}`;
690
+ }
691
+ url.hostname = match;
692
+ }
693
+ }
694
+
634
695
  // options
635
696
  options.method = this.method;
636
697
  options.port = url.port;
@@ -648,8 +709,12 @@ Request.prototype.request = function(){
648
709
  options.servername = this._header['host'].replace(/:[0-9]+$/,'');
649
710
  }
650
711
 
712
+ if (this._trustLocalhost && /^(?:localhost|127\.0\.0\.\d+|(0*:)+:0*1)$/.test(url.hostname)) {
713
+ options.rejectUnauthorized = false;
714
+ }
715
+
651
716
  // initiate request
652
- const mod = exports.protocols[url.protocol];
717
+ const mod = this._enableHttp2 ? exports.protocols['http2:'].setProtocol(url.protocol) : exports.protocols[url.protocol];
653
718
 
654
719
  // request
655
720
  const req = (this.req = mod.request(options));
@@ -695,10 +760,10 @@ Request.prototype.request = function(){
695
760
 
696
761
  // add cookies
697
762
  if (this.cookies) {
698
- if(this.header.hasOwnProperty('cookie')) {
763
+ if(this._header.hasOwnProperty('cookie')) {
699
764
  // merge
700
765
  const tmpJar = new CookieJar.CookieJar();
701
- tmpJar.setCookies(this.header.cookie.split(';'));
766
+ tmpJar.setCookies(this._header.cookie.split(';'));
702
767
  tmpJar.setCookies(this.cookies.split(';'));
703
768
  req.setHeader('Cookie',tmpJar.getCookies(CookieJar.CookieAccessInfo.All).toValueString());
704
769
  } else {
@@ -789,6 +854,11 @@ Request.prototype._emitResponse = function(body, files) {
789
854
  response.body = body;
790
855
  }
791
856
  response.files = files;
857
+ if (this._endCalled) {
858
+ response.pipe = function() {
859
+ throw Error("end() has already been called, so it's too late to start piping");
860
+ }
861
+ }
792
862
  this.emit('response', response);
793
863
  return response;
794
864
  };
@@ -798,8 +868,8 @@ Request.prototype.end = function(fn) {
798
868
  debug('%s %s', this.method, this.url);
799
869
 
800
870
  if (this._endCalled) {
801
- console.warn(
802
- 'Warning: .end() was called twice. This is not supported in superagent'
871
+ throw Error(
872
+ '.end() was called twice. This is not supported in superagent'
803
873
  );
804
874
  }
805
875
  this._endCalled = true;
@@ -807,7 +877,7 @@ Request.prototype.end = function(fn) {
807
877
  // store callback
808
878
  this._callback = fn || noop;
809
879
 
810
- return this._end();
880
+ this._end();
811
881
  };
812
882
 
813
883
  Request.prototype._end = function() {
@@ -815,7 +885,6 @@ Request.prototype._end = function() {
815
885
 
816
886
  let data = this._data;
817
887
  const req = this.req;
818
- let buffer = this._buffer;
819
888
  const method = this.method;
820
889
 
821
890
  this._setTimeouts();
@@ -827,7 +896,7 @@ Request.prototype._end = function() {
827
896
  let contentType = req.getHeader('Content-Type');
828
897
  // Parse out just the content type from the header (ignore the charset)
829
898
  if (contentType) contentType = contentType.split(';')[0];
830
- let serialize = exports.serialize[contentType];
899
+ let serialize = this._serializer || exports.serialize[contentType];
831
900
  if (!serialize && isJSON(contentType)) {
832
901
  serialize = exports.serialize['application/json'];
833
902
  }
@@ -857,7 +926,6 @@ Request.prototype._end = function() {
857
926
  const type = mime.split('/')[0];
858
927
  const multipart = 'multipart' == type;
859
928
  const redirect = isRedirect(res.statusCode);
860
- let parser = this._parser;
861
929
  const responseType = this._responseType;
862
930
 
863
931
  this.res = res;
@@ -878,10 +946,19 @@ Request.prototype._end = function() {
878
946
  unzip(req, res);
879
947
  }
880
948
 
949
+ let buffer = this._buffer;
881
950
  if (buffer === undefined && mime in exports.buffer){
882
951
  buffer = !!exports.buffer[mime];
883
952
  }
884
953
 
954
+ let parser = this._parser;
955
+ if (undefined === buffer) {
956
+ if (parser) {
957
+ 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`");
958
+ buffer = true;
959
+ }
960
+ }
961
+
885
962
  if (!parser) {
886
963
  if (responseType) {
887
964
  parser = exports.parse.image; // It's actually a generic Buffer
@@ -905,6 +982,9 @@ Request.prototype._end = function() {
905
982
  buffer = (buffer !== false);
906
983
  } else if (buffer) {
907
984
  parser = exports.parse.text;
985
+ } else if (undefined === buffer) {
986
+ parser = exports.parse.image; // It's actually a generic Buffer
987
+ buffer = true;
908
988
  }
909
989
  }
910
990
 
@@ -913,6 +993,7 @@ Request.prototype._end = function() {
913
993
  buffer = true;
914
994
  }
915
995
 
996
+ this._resBuffered = buffer;
916
997
  let parserHandlesEnd = false;
917
998
  if (buffer) {
918
999
  // Protectiona against zip bombs and other nuisance
@@ -1059,8 +1140,6 @@ Request.prototype._end = function() {
1059
1140
  } else {
1060
1141
  req.end(data);
1061
1142
  }
1062
-
1063
- return this;
1064
1143
  };
1065
1144
 
1066
1145
  /**
@@ -1082,6 +1161,35 @@ Request.prototype._shouldUnzip = res => {
1082
1161
  return /^\s*(?:deflate|gzip)\s*$/.test(res.headers['content-encoding']);
1083
1162
  };
1084
1163
 
1164
+ /**
1165
+ * Overrides DNS for selected hostnames. Takes object mapping hostnames to IP addresses.
1166
+ *
1167
+ * When making a request to a URL with a hostname exactly matching a key in the object,
1168
+ * use the given IP address to connect, instead of using DNS to resolve the hostname.
1169
+ *
1170
+ * A special host `*` matches every hostname (keep redirects in mind!)
1171
+ *
1172
+ * request.connect({
1173
+ * 'test.example.com': '127.0.0.1',
1174
+ * 'ipv6.example.com': '::1',
1175
+ * })
1176
+ */
1177
+ Request.prototype.connect = function(connectOverride) {
1178
+ if ('string' === typeof connectOverride) {
1179
+ this._connectOverride = {'*': connectOverride};
1180
+ } else if ('object' === typeof connectOverride) {
1181
+ this._connectOverride = connectOverride;
1182
+ } else {
1183
+ this._connectOverride = undefined;
1184
+ }
1185
+ return this;
1186
+ };
1187
+
1188
+ Request.prototype.trustLocalhost = function(toggle) {
1189
+ this._trustLocalhost = toggle === undefined ? true : toggle;
1190
+ return this;
1191
+ };
1192
+
1085
1193
  // generate HTTP verb methods
1086
1194
  if (methods.indexOf('del') == -1) {
1087
1195
  // create a copy so we don't cause conflicts with
@@ -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);
@@ -236,6 +236,14 @@ RequestBase.prototype.then = function then(resolve, reject) {
236
236
  }
237
237
  this._fullfilledPromise = new Promise((innerResolve, innerReject) => {
238
238
  self.on('error', innerReject);
239
+ self.on('abort', () => {
240
+ const err = new Error('Aborted');
241
+ err.code = "ABORTED";
242
+ err.status = this.status;
243
+ err.method = this.method;
244
+ err.url = this.url;
245
+ innerReject(err);
246
+ });
239
247
  self.end((err, res) => {
240
248
  if (err) innerReject(err);
241
249
  else innerResolve(res);
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "superagent",
3
- "version": "4.0.0-alpha.1",
3
+ "version": "4.1.0",
4
4
  "description": "elegant & feature rich browser / node HTTP with a fluent API",
5
5
  "scripts": {
6
6
  "prepare": "make all",
7
- "test": "make test"
7
+ "test": "make test",
8
+ "test-http2": "make test-node-http2"
8
9
  },
9
10
  "keywords": [
10
11
  "http",
@@ -25,15 +26,14 @@
25
26
  },
26
27
  "dependencies": {
27
28
  "component-emitter": "^1.2.0",
28
- "cookiejar": "^2.1.0",
29
- "debug": "^3.1.0",
30
- "extend": "^3.0.0",
31
- "form-data": "^2.3.2",
29
+ "cookiejar": "^2.1.2",
30
+ "debug": "^4.1.0",
31
+ "form-data": "^2.3.3",
32
32
  "formidable": "^1.2.0",
33
33
  "methods": "^1.1.1",
34
- "mime": "^2.0.3",
35
- "qs": "^6.5.1",
36
- "readable-stream": "^2.3.5"
34
+ "mime": "^2.4.0",
35
+ "qs": "^6.6.0",
36
+ "readable-stream": "^3.0.6"
37
37
  },
38
38
  "devDependencies": {
39
39
  "Base64": "^1.0.1",
@@ -42,16 +42,16 @@
42
42
  "babelify": "^8.0.0",
43
43
  "basic-auth-connect": "^1.0.0",
44
44
  "body-parser": "^1.18.2",
45
- "browserify": "^16.2.0",
45
+ "browserify": "^16.2.3",
46
46
  "cookie-parser": "^1.4.3",
47
47
  "express": "^4.16.3",
48
48
  "express-session": "^1.15.6",
49
- "marked": "^0.3.19",
49
+ "marked": "^0.5.2",
50
50
  "mocha": "^3.5.3",
51
- "multer": "^1.3.0",
51
+ "multer": "^1.4.1",
52
52
  "should": "^13.2.0",
53
53
  "should-http": "^0.1.1",
54
- "zuul": "^3.11.1"
54
+ "zuul": "^3.12.0"
55
55
  },
56
56
  "browser": {
57
57
  "./lib/node/index.js": "./lib/client.js",
package/superagent.js CHANGED
@@ -274,6 +274,8 @@ RequestBase.prototype._retry = function () {
274
274
  */
275
275
 
276
276
  RequestBase.prototype.then = function then(resolve, reject) {
277
+ var _this = this;
278
+
277
279
  if (!this._fullfilledPromise) {
278
280
  var self = this;
279
281
  if (this._endCalled) {
@@ -281,6 +283,14 @@ RequestBase.prototype.then = function then(resolve, reject) {
281
283
  }
282
284
  this._fullfilledPromise = new Promise(function (innerResolve, innerReject) {
283
285
  self.on('error', innerReject);
286
+ self.on('abort', function () {
287
+ var err = new Error('Aborted');
288
+ err.code = "ABORTED";
289
+ err.status = _this.status;
290
+ err.method = _this.method;
291
+ err.url = _this.url;
292
+ innerReject(err);
293
+ });
284
294
  self.end(function (err, res) {
285
295
  if (err) innerReject(err);else innerResolve(res);
286
296
  });
@@ -1799,7 +1809,7 @@ Request.prototype.end = function (fn) {
1799
1809
  // querystring
1800
1810
  this._finalizeQueryString();
1801
1811
 
1802
- return this._end();
1812
+ this._end();
1803
1813
  };
1804
1814
 
1805
1815
  Request.prototype._end = function () {
@@ -1901,7 +1911,6 @@ Request.prototype._end = function () {
1901
1911
  // IE11 xhr.send(undefined) sends 'undefined' string as POST payload (instead of nothing)
1902
1912
  // We need null here if data is undefined
1903
1913
  xhr.send(typeof data !== 'undefined' ? data : null);
1904
- return this;
1905
1914
  };
1906
1915
 
1907
1916
  request.agent = function () {