follow-redirects 1.2.6 → 1.5.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.

Potentially problematic release.


This version of follow-redirects might be problematic. Click here for more details.

Files changed (5) hide show
  1. package/README.md +23 -1
  2. package/http.js +1 -1
  3. package/https.js +1 -1
  4. package/index.js +254 -211
  5. package/package.json +10 -13
package/README.md CHANGED
@@ -5,7 +5,6 @@ Drop-in replacement for Nodes `http` and `https` that automatically follows redi
5
5
  [![npm version](https://badge.fury.io/js/follow-redirects.svg)](https://www.npmjs.com/package/follow-redirects)
6
6
  [![Build Status](https://travis-ci.org/olalonde/follow-redirects.svg?branch=master)](https://travis-ci.org/olalonde/follow-redirects)
7
7
  [![Coverage Status](https://coveralls.io/repos/olalonde/follow-redirects/badge.svg?branch=master)](https://coveralls.io/r/olalonde/follow-redirects?branch=master)
8
- [![Code Climate](https://codeclimate.com/github/olalonde/follow-redirects/badges/gpa.svg)](https://codeclimate.com/github/olalonde/follow-redirects)
9
8
  [![Dependency Status](https://david-dm.org/olalonde/follow-redirects.svg)](https://david-dm.org/olalonde/follow-redirects)
10
9
  [![devDependency Status](https://david-dm.org/olalonde/follow-redirects/dev-status.svg)](https://david-dm.org/olalonde/follow-redirects#info=devDependencies)
11
10
 
@@ -48,12 +47,15 @@ Global options are set directly on the `follow-redirects` module:
48
47
  ```javascript
49
48
  var followRedirects = require('follow-redirects');
50
49
  followRedirects.maxRedirects = 10;
50
+ followRedirects.maxBodyLength = 20 * 1024 * 1024; // 20 MB
51
51
  ```
52
52
 
53
53
  The following global options are supported:
54
54
 
55
55
  - `maxRedirects` (default: `21`) – sets the maximum number of allowed redirects; if exceeded, an error will be emitted.
56
56
 
57
+ - `maxBodyLength` (default: 10MB) – sets the maximum size of the request body; if exceeded, an error will be emitted.
58
+
57
59
 
58
60
  ### Per-request options
59
61
  Per-request options are set by passing an `options` object:
@@ -73,8 +75,28 @@ the following per-request options are supported:
73
75
 
74
76
  - `maxRedirects` (default: `21`) – sets the maximum number of allowed redirects; if exceeded, an error will be emitted.
75
77
 
78
+ - `maxBodyLength` (default: 10MB) – sets the maximum size of the request body; if exceeded, an error will be emitted.
79
+
76
80
  - `agents` (default: `undefined`) – sets the `agent` option per protocol, since HTTP and HTTPS use different agents. Example value: `{ http: new http.Agent(), https: new https.Agent() }`
77
81
 
82
+ - `trackRedirects` (default: `false`) – whether to store the redirected response details into the `redirects` array on the response object.
83
+
84
+
85
+ ### Advanced usage
86
+ By default, `follow-redirects` will use the Node.js default implementations
87
+ of [`http`](https://nodejs.org/api/http.html)
88
+ and [`https`](https://nodejs.org/api/https.html).
89
+ To enable features such as caching and/or intermediate request tracking,
90
+ you might instead want to wrap `follow-redirects` around custom protocol implementations:
91
+
92
+ ```javascript
93
+ var followRedirects = require('follow-redirects').wrap({
94
+ http: require('your-custom-http'),
95
+ https: require('your-custom-https'),
96
+ });
97
+ ```
98
+
99
+ Such custom protocols only need an implementation of the `request` method.
78
100
 
79
101
  ## Browserify Usage
80
102
 
package/http.js CHANGED
@@ -1 +1 @@
1
- module.exports = require('./').http;
1
+ module.exports = require("./").http;
package/https.js CHANGED
@@ -1 +1 @@
1
- module.exports = require('./').https;
1
+ module.exports = require("./").https;
package/index.js CHANGED
@@ -1,238 +1,281 @@
1
- 'use strict';
2
- var url = require('url');
3
- var assert = require('assert');
4
- var http = require('http');
5
- var https = require('https');
6
- var Writable = require('stream').Writable;
7
- var debug = require('debug')('follow-redirects');
8
-
9
- var nativeProtocols = {'http:': http, 'https:': https};
10
- var schemes = {};
11
- var exports = module.exports = {
12
- maxRedirects: 21
13
- };
1
+ var url = require("url");
2
+ var http = require("http");
3
+ var https = require("https");
4
+ var assert = require("assert");
5
+ var Writable = require("stream").Writable;
6
+ var debug = require("debug")("follow-redirects");
7
+
14
8
  // RFC7231§4.2.1: Of the request methods defined by this specification,
15
9
  // the GET, HEAD, OPTIONS, and TRACE methods are defined to be safe.
16
- var safeMethods = {GET: true, HEAD: true, OPTIONS: true, TRACE: true};
10
+ var SAFE_METHODS = { GET: true, HEAD: true, OPTIONS: true, TRACE: true };
17
11
 
18
12
  // Create handlers that pass events from native requests
19
13
  var eventHandlers = Object.create(null);
20
- ['abort', 'aborted', 'error', 'socket'].forEach(function (event) {
21
- eventHandlers[event] = function (arg) {
22
- this._redirectable.emit(event, arg);
23
- };
14
+ ["abort", "aborted", "error", "socket", "timeout"].forEach(function (event) {
15
+ eventHandlers[event] = function (arg) {
16
+ this._redirectable.emit(event, arg);
17
+ };
24
18
  });
25
19
 
26
20
  // An HTTP(S) request that can be redirected
27
21
  function RedirectableRequest(options, responseCallback) {
28
- // Initialize the request
29
- Writable.call(this);
30
- this._options = options;
31
- this._redirectCount = 0;
32
- this._bufferedWrites = [];
33
-
34
- // Attach a callback if passed
35
- if (responseCallback) {
36
- this.on('response', responseCallback);
37
- }
38
-
39
- // React to responses of native requests
40
- var self = this;
41
- this._onNativeResponse = function (response) {
42
- self._processResponse(response);
43
- };
44
-
45
- // Complete the URL object when necessary
46
- if (!options.pathname && options.path) {
47
- var searchPos = options.path.indexOf('?');
48
- if (searchPos < 0) {
49
- options.pathname = options.path;
50
- } else {
51
- options.pathname = options.path.substring(0, searchPos);
52
- options.search = options.path.substring(searchPos);
53
- }
54
- }
55
-
56
- // Perform the first request
57
- this._performRequest();
22
+ // Initialize the request
23
+ Writable.call(this);
24
+ options.headers = options.headers || {};
25
+ this._options = options;
26
+ this._redirectCount = 0;
27
+ this._redirects = [];
28
+ this._requestBodyLength = 0;
29
+ this._requestBodyBuffers = [];
30
+
31
+ // Attach a callback if passed
32
+ if (responseCallback) {
33
+ this.on("response", responseCallback);
34
+ }
35
+
36
+ // React to responses of native requests
37
+ var self = this;
38
+ this._onNativeResponse = function (response) {
39
+ self._processResponse(response);
40
+ };
41
+
42
+ // Complete the URL object when necessary
43
+ if (!options.pathname && options.path) {
44
+ var searchPos = options.path.indexOf("?");
45
+ if (searchPos < 0) {
46
+ options.pathname = options.path;
47
+ }
48
+ else {
49
+ options.pathname = options.path.substring(0, searchPos);
50
+ options.search = options.path.substring(searchPos);
51
+ }
52
+ }
53
+
54
+ // Perform the first request
55
+ this._performRequest();
58
56
  }
59
57
  RedirectableRequest.prototype = Object.create(Writable.prototype);
60
58
 
61
- // Executes the next native request (initial or redirect)
62
- RedirectableRequest.prototype._performRequest = function () {
63
- // If specified, use the agent corresponding to the protocol
64
- // (HTTP and HTTPS use different types of agents)
65
- var protocol = this._options.protocol;
66
- if (this._options.agents) {
67
- this._options.agent = this._options.agents[schemes[protocol]];
68
- }
69
-
70
- // Create the native request
71
- var nativeProtocol = nativeProtocols[protocol];
72
- var request = this._currentRequest =
73
- nativeProtocol.request(this._options, this._onNativeResponse);
74
- this._currentUrl = url.format(this._options);
75
-
76
- // Set up event handlers
77
- request._redirectable = this;
78
- for (var event in eventHandlers) {
79
- /* istanbul ignore else */
80
- if (event) {
81
- request.on(event, eventHandlers[event]);
82
- }
83
- }
84
-
85
- // End a redirected request
86
- // (The first request must be ended explicitly with RedirectableRequest#end)
87
- if (this._isRedirect) {
88
- // If the request doesn't have en entity, end directly.
89
- var bufferedWrites = this._bufferedWrites;
90
- if (bufferedWrites.length === 0) {
91
- request.end();
92
- // Otherwise, write the request entity and end afterwards.
93
- } else {
94
- var i = 0;
95
- (function writeNext() {
96
- if (i < bufferedWrites.length) {
97
- var bufferedWrite = bufferedWrites[i++];
98
- request.write(bufferedWrite.data, bufferedWrite.encoding, writeNext);
99
- } else {
100
- request.end();
101
- }
102
- })();
103
- }
104
- }
59
+ // Writes buffered data to the current native request
60
+ RedirectableRequest.prototype.write = function (data, encoding, callback) {
61
+ if (!(typeof data === "string" || typeof data === "object" && ("length" in data))) {
62
+ throw new Error("data should be a string, Buffer or Uint8Array");
63
+ }
64
+ if (this._requestBodyLength + data.length <= this._options.maxBodyLength) {
65
+ this._requestBodyLength += data.length;
66
+ this._requestBodyBuffers.push({ data: data, encoding: encoding });
67
+ this._currentRequest.write(data, encoding, callback);
68
+ }
69
+ else {
70
+ this.emit("error", new Error("Request body larger than maxBodyLength limit"));
71
+ this.abort();
72
+ }
105
73
  };
106
74
 
107
- // Processes a response from the current native request
108
- RedirectableRequest.prototype._processResponse = function (response) {
109
- // RFC7231§6.4: The 3xx (Redirection) class of status code indicates
110
- // that further action needs to be taken by the user agent in order to
111
- // fulfill the request. If a Location header field is provided,
112
- // the user agent MAY automatically redirect its request to the URI
113
- // referenced by the Location field value,
114
- // even if the specific status code is not understood.
115
- var location = response.headers.location;
116
- if (location && this._options.followRedirects !== false &&
117
- response.statusCode >= 300 && response.statusCode < 400) {
118
- // RFC7231§6.4: A client SHOULD detect and intervene
119
- // in cyclical redirections (i.e., "infinite" redirection loops).
120
- if (++this._redirectCount > this._options.maxRedirects) {
121
- return this.emit('error', new Error('Max redirects exceeded.'));
122
- }
123
-
124
- // RFC7231§6.4: Automatic redirection needs to done with
125
- // care for methods not known to be safe […],
126
- // since the user might not wish to redirect an unsafe request.
127
- // RFC7231§6.4.7: The 307 (Temporary Redirect) status code indicates
128
- // that the target resource resides temporarily under a different URI
129
- // and the user agent MUST NOT change the request method
130
- // if it performs an automatic redirection to that URI.
131
- var header;
132
- var headers = this._options.headers;
133
- if (response.statusCode !== 307 && !(this._options.method in safeMethods)) {
134
- this._options.method = 'GET';
135
- // Drop a possible entity and headers related to it
136
- this._bufferedWrites = [];
137
- for (header in headers) {
138
- if (/^content-/i.test(header)) {
139
- delete headers[header];
140
- }
141
- }
142
- }
143
-
144
- // Drop the Host header, as the redirect might lead to a different host
145
- if (!this._isRedirect) {
146
- for (header in headers) {
147
- if (/^host$/i.test(header)) {
148
- delete headers[header];
149
- }
150
- }
151
- }
152
-
153
- // Perform the redirected request
154
- var redirectUrl = url.resolve(this._currentUrl, location);
155
- debug('redirecting to', redirectUrl);
156
- Object.assign(this._options, url.parse(redirectUrl));
157
- this._isRedirect = true;
158
- this._performRequest();
159
- } else {
160
- // The response is not a redirect; return it as-is
161
- response.responseUrl = this._currentUrl;
162
- this.emit('response', response);
163
-
164
- // Clean up
165
- delete this._options;
166
- delete this._bufferedWrites;
167
- }
75
+ // Ends the current native request
76
+ RedirectableRequest.prototype.end = function (data, encoding, callback) {
77
+ var currentRequest = this._currentRequest;
78
+ if (!data) {
79
+ currentRequest.end(null, null, callback);
80
+ }
81
+ else {
82
+ this.write(data, encoding, function () {
83
+ currentRequest.end(null, null, callback);
84
+ });
85
+ }
168
86
  };
169
87
 
170
- // Aborts the current native request
171
- RedirectableRequest.prototype.abort = function () {
172
- this._currentRequest.abort();
88
+ // Sets a header value on the current native request
89
+ RedirectableRequest.prototype.setHeader = function (name, value) {
90
+ this._options.headers[name] = value;
91
+ this._currentRequest.setHeader(name, value);
173
92
  };
174
93
 
175
- // Flushes the headers of the current native request
176
- RedirectableRequest.prototype.flushHeaders = function () {
177
- this._currentRequest.flushHeaders();
94
+ // Clears a header value on the current native request
95
+ RedirectableRequest.prototype.removeHeader = function (name) {
96
+ delete this._options.headers[name];
97
+ this._currentRequest.removeHeader(name);
178
98
  };
179
99
 
180
- // Sets the noDelay option of the current native request
181
- RedirectableRequest.prototype.setNoDelay = function (noDelay) {
182
- this._currentRequest.setNoDelay(noDelay);
183
- };
100
+ // Proxy all other public ClientRequest methods
101
+ [
102
+ "abort", "flushHeaders", "getHeader",
103
+ "setNoDelay", "setSocketKeepAlive", "setTimeout",
104
+ ].forEach(function (method) {
105
+ RedirectableRequest.prototype[method] = function (a, b) {
106
+ return this._currentRequest[method](a, b);
107
+ };
108
+ });
184
109
 
185
- // Sets the socketKeepAlive option of the current native request
186
- RedirectableRequest.prototype.setSocketKeepAlive = function (enable, initialDelay) {
187
- this._currentRequest.setSocketKeepAlive(enable, initialDelay);
188
- };
110
+ // Proxy all public ClientRequest properties
111
+ ["aborted", "connection", "socket"].forEach(function (property) {
112
+ Object.defineProperty(RedirectableRequest.prototype, property, {
113
+ get: function () { return this._currentRequest[property]; },
114
+ });
115
+ });
189
116
 
190
- // Sets the timeout option of the current native request
191
- RedirectableRequest.prototype.setTimeout = function (timeout, callback) {
192
- this._currentRequest.setTimeout(timeout, callback);
193
- };
117
+ // Executes the next native request (initial or redirect)
118
+ RedirectableRequest.prototype._performRequest = function () {
119
+ // Load the native protocol
120
+ var protocol = this._options.protocol;
121
+ var nativeProtocol = this._options.nativeProtocols[protocol];
194
122
 
195
- // Writes buffered data to the current native request
196
- RedirectableRequest.prototype.write = function (data, encoding, callback) {
197
- this._currentRequest.write(data, encoding, callback);
198
- this._bufferedWrites.push({data: data, encoding: encoding});
123
+ // If specified, use the agent corresponding to the protocol
124
+ // (HTTP and HTTPS use different types of agents)
125
+ if (this._options.agents) {
126
+ var scheme = protocol.substr(0, protocol.length - 1);
127
+ this._options.agent = this._options.agents[scheme];
128
+ }
129
+
130
+ // Create the native request
131
+ var request = this._currentRequest =
132
+ nativeProtocol.request(this._options, this._onNativeResponse);
133
+ this._currentUrl = url.format(this._options);
134
+
135
+ // Set up event handlers
136
+ request._redirectable = this;
137
+ for (var event in eventHandlers) {
138
+ /* istanbul ignore else */
139
+ if (event) {
140
+ request.on(event, eventHandlers[event]);
141
+ }
142
+ }
143
+
144
+ // End a redirected request
145
+ // (The first request must be ended explicitly with RedirectableRequest#end)
146
+ if (this._isRedirect) {
147
+ // Write the request entity and end.
148
+ var requestBodyBuffers = this._requestBodyBuffers;
149
+ (function writeNext() {
150
+ if (requestBodyBuffers.length !== 0) {
151
+ var buffer = requestBodyBuffers.pop();
152
+ request.write(buffer.data, buffer.encoding, writeNext);
153
+ }
154
+ else {
155
+ request.end();
156
+ }
157
+ }());
158
+ }
199
159
  };
200
160
 
201
- // Ends the current native request
202
- RedirectableRequest.prototype.end = function (data, encoding, callback) {
203
- this._currentRequest.end(data, encoding, callback);
204
- if (data) {
205
- this._bufferedWrites.push({data: data, encoding: encoding});
206
- }
161
+ // Processes a response from the current native request
162
+ RedirectableRequest.prototype._processResponse = function (response) {
163
+ // Store the redirected response
164
+ if (this._options.trackRedirects) {
165
+ this._redirects.push({
166
+ url: this._currentUrl,
167
+ headers: response.headers,
168
+ statusCode: response.statusCode,
169
+ });
170
+ }
171
+
172
+ // RFC7231§6.4: The 3xx (Redirection) class of status code indicates
173
+ // that further action needs to be taken by the user agent in order to
174
+ // fulfill the request. If a Location header field is provided,
175
+ // the user agent MAY automatically redirect its request to the URI
176
+ // referenced by the Location field value,
177
+ // even if the specific status code is not understood.
178
+ var location = response.headers.location;
179
+ if (location && this._options.followRedirects !== false &&
180
+ response.statusCode >= 300 && response.statusCode < 400) {
181
+ // RFC7231§6.4: A client SHOULD detect and intervene
182
+ // in cyclical redirections (i.e., "infinite" redirection loops).
183
+ if (++this._redirectCount > this._options.maxRedirects) {
184
+ this.emit("error", new Error("Max redirects exceeded."));
185
+ return;
186
+ }
187
+
188
+ // RFC7231§6.4: Automatic redirection needs to done with
189
+ // care for methods not known to be safe […],
190
+ // since the user might not wish to redirect an unsafe request.
191
+ // RFC7231§6.4.7: The 307 (Temporary Redirect) status code indicates
192
+ // that the target resource resides temporarily under a different URI
193
+ // and the user agent MUST NOT change the request method
194
+ // if it performs an automatic redirection to that URI.
195
+ var header;
196
+ var headers = this._options.headers;
197
+ if (response.statusCode !== 307 && !(this._options.method in SAFE_METHODS)) {
198
+ this._options.method = "GET";
199
+ // Drop a possible entity and headers related to it
200
+ this._requestBodyBuffers = [];
201
+ for (header in headers) {
202
+ if (/^content-/i.test(header)) {
203
+ delete headers[header];
204
+ }
205
+ }
206
+ }
207
+
208
+ // Drop the Host header, as the redirect might lead to a different host
209
+ if (!this._isRedirect) {
210
+ for (header in headers) {
211
+ if (/^host$/i.test(header)) {
212
+ delete headers[header];
213
+ }
214
+ }
215
+ }
216
+
217
+ // Perform the redirected request
218
+ var redirectUrl = url.resolve(this._currentUrl, location);
219
+ debug("redirecting to", redirectUrl);
220
+ Object.assign(this._options, url.parse(redirectUrl));
221
+ this._isRedirect = true;
222
+ this._performRequest();
223
+ }
224
+ else {
225
+ // The response is not a redirect; return it as-is
226
+ response.responseUrl = this._currentUrl;
227
+ response.redirects = this._redirects;
228
+ this.emit("response", response);
229
+
230
+ // Clean up
231
+ this._requestBodyBuffers = [];
232
+ }
207
233
  };
208
234
 
209
- // Export a redirecting wrapper for each native protocol
210
- Object.keys(nativeProtocols).forEach(function (protocol) {
211
- var scheme = schemes[protocol] = protocol.substr(0, protocol.length - 1);
212
- var nativeProtocol = nativeProtocols[protocol];
213
- var wrappedProtocol = exports[scheme] = Object.create(nativeProtocol);
214
-
215
- // Executes an HTTP request, following redirects
216
- wrappedProtocol.request = function (options, callback) {
217
- if (typeof options === 'string') {
218
- options = url.parse(options);
219
- options.maxRedirects = exports.maxRedirects;
220
- } else {
221
- options = Object.assign({
222
- maxRedirects: exports.maxRedirects,
223
- protocol: protocol
224
- }, options);
225
- }
226
- assert.equal(options.protocol, protocol, 'protocol mismatch');
227
- debug('options', options);
228
-
229
- return new RedirectableRequest(options, callback);
230
- };
231
-
232
- // Executes a GET request, following redirects
233
- wrappedProtocol.get = function (options, callback) {
234
- var request = wrappedProtocol.request(options, callback);
235
- request.end();
236
- return request;
237
- };
238
- });
235
+ // Wraps the key/value object of protocols with redirect functionality
236
+ function wrap(protocols) {
237
+ // Default settings
238
+ var exports = {
239
+ maxRedirects: 21,
240
+ maxBodyLength: 10 * 1024 * 1024,
241
+ };
242
+
243
+ // Wrap each protocol
244
+ var nativeProtocols = {};
245
+ Object.keys(protocols).forEach(function (scheme) {
246
+ var protocol = scheme + ":";
247
+ var nativeProtocol = nativeProtocols[protocol] = protocols[scheme];
248
+ var wrappedProtocol = exports[scheme] = Object.create(nativeProtocol);
249
+
250
+ // Executes a request, following redirects
251
+ wrappedProtocol.request = function (options, callback) {
252
+ if (typeof options === "string") {
253
+ options = url.parse(options);
254
+ options.maxRedirects = exports.maxRedirects;
255
+ }
256
+ else {
257
+ options = Object.assign({
258
+ protocol: protocol,
259
+ maxRedirects: exports.maxRedirects,
260
+ maxBodyLength: exports.maxBodyLength,
261
+ }, options);
262
+ }
263
+ options.nativeProtocols = nativeProtocols;
264
+ assert.equal(options.protocol, protocol, "protocol mismatch");
265
+ debug("options", options);
266
+ return new RedirectableRequest(options, callback);
267
+ };
268
+
269
+ // Executes a GET request, following redirects
270
+ wrappedProtocol.get = function (options, callback) {
271
+ var request = wrappedProtocol.request(options, callback);
272
+ request.end();
273
+ return request;
274
+ };
275
+ });
276
+ return exports;
277
+ }
278
+
279
+ // Exports
280
+ module.exports = wrap({ http: http, https: https });
281
+ module.exports.wrap = wrap;
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "follow-redirects",
3
- "version": "1.2.6",
3
+ "version": "1.5.0",
4
4
  "description": "HTTP and HTTPS modules that follow redirects.",
5
5
  "main": "index.js",
6
6
  "engines": {
7
7
  "node": ">=4.0"
8
8
  },
9
9
  "scripts": {
10
- "test": "xo && BLUEBIRD_DEBUG=1 nyc mocha"
10
+ "test": "npm run lint && npm run mocha",
11
+ "lint": "eslint *.js test",
12
+ "mocha": "nyc mocha"
11
13
  },
12
14
  "repository": {
13
15
  "type": "git",
@@ -45,13 +47,13 @@
45
47
  "debug": "^3.1.0"
46
48
  },
47
49
  "devDependencies": {
48
- "bluebird": "^3.4.0",
49
- "concat-stream": "^1.5.2",
50
+ "bluebird": "^3.5.1",
51
+ "concat-stream": "^1.6.0",
50
52
  "coveralls": "^3.0.0",
51
- "express": "^4.13.0",
52
- "mocha": "^4.0.1",
53
- "nyc": "^11.3.0",
54
- "xo": "^0.17.1"
53
+ "eslint": "^4.19.1",
54
+ "express": "^4.16.2",
55
+ "mocha": "^5.0.0",
56
+ "nyc": "^11.8.0"
55
57
  },
56
58
  "license": "MIT",
57
59
  "nyc": {
@@ -59,10 +61,5 @@
59
61
  "lcov",
60
62
  "text"
61
63
  ]
62
- },
63
- "xo": {
64
- "envs": [
65
- "mocha"
66
- ]
67
64
  }
68
65
  }