follow-redirects 1.2.5 → 1.4.1

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 +21 -1
  2. package/http.js +1 -1
  3. package/https.js +1 -1
  4. package/index.js +240 -211
  5. package/package.json +12 -15
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,9 +75,27 @@ 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
 
78
82
 
83
+ ### Advanced usage
84
+ By default, `follow-redirects` will use the Node.js default implementations
85
+ of [`http`](https://nodejs.org/api/http.html)
86
+ and [`https`](https://nodejs.org/api/https.html).
87
+ To enable features such as caching and/or intermediate request tracking,
88
+ you might instead want to wrap `follow-redirects` around custom protocol implementations:
89
+
90
+ ```javascript
91
+ var followRedirects = require('follow-redirects').wrap({
92
+ http: require('your-custom-http'),
93
+ https: require('your-custom-https'),
94
+ });
95
+ ```
96
+
97
+ Such custom protocols only need an implementation of the `request` method.
98
+
79
99
  ## Browserify Usage
80
100
 
81
101
  Due to the way `XMLHttpRequest` works, the `browserify` versions of `http` and `https` already follow redirects.
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,267 @@
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._requestBodyLength = 0;
28
+ this._requestBodyBuffers = [];
29
+
30
+ // Attach a callback if passed
31
+ if (responseCallback) {
32
+ this.on("response", responseCallback);
33
+ }
34
+
35
+ // React to responses of native requests
36
+ var self = this;
37
+ this._onNativeResponse = function (response) {
38
+ self._processResponse(response);
39
+ };
40
+
41
+ // Complete the URL object when necessary
42
+ if (!options.pathname && options.path) {
43
+ var searchPos = options.path.indexOf("?");
44
+ if (searchPos < 0) {
45
+ options.pathname = options.path;
46
+ }
47
+ else {
48
+ options.pathname = options.path.substring(0, searchPos);
49
+ options.search = options.path.substring(searchPos);
50
+ }
51
+ }
52
+
53
+ // Perform the first request
54
+ this._performRequest();
58
55
  }
59
56
  RedirectableRequest.prototype = Object.create(Writable.prototype);
60
57
 
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
- }
58
+ // Writes buffered data to the current native request
59
+ RedirectableRequest.prototype.write = function (data, encoding, callback) {
60
+ if (this._requestBodyLength + data.length <= this._options.maxBodyLength) {
61
+ this._requestBodyLength += data.length;
62
+ this._requestBodyBuffers.push({ data: data, encoding: encoding });
63
+ this._currentRequest.write(data, encoding, callback);
64
+ }
65
+ else {
66
+ this.emit("error", new Error("Request body larger than maxBodyLength limit"));
67
+ this.abort();
68
+ }
105
69
  };
106
70
 
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
- }
71
+ // Ends the current native request
72
+ RedirectableRequest.prototype.end = function (data, encoding, callback) {
73
+ var currentRequest = this._currentRequest;
74
+ if (!data) {
75
+ currentRequest.end(null, null, callback);
76
+ }
77
+ else {
78
+ this.write(data, encoding, function () {
79
+ currentRequest.end(null, null, callback);
80
+ });
81
+ }
168
82
  };
169
83
 
170
- // Aborts the current native request
171
- RedirectableRequest.prototype.abort = function () {
172
- this._currentRequest.abort();
84
+ // Sets a header value on the current native request
85
+ RedirectableRequest.prototype.setHeader = function (name, value) {
86
+ this._options.headers[name] = value;
87
+ this._currentRequest.setHeader(name, value);
173
88
  };
174
89
 
175
- // Flushes the headers of the current native request
176
- RedirectableRequest.prototype.flushHeaders = function () {
177
- this._currentRequest.flushHeaders();
90
+ // Clears a header value on the current native request
91
+ RedirectableRequest.prototype.removeHeader = function (name) {
92
+ delete this._options.headers[name];
93
+ this._currentRequest.removeHeader(name);
178
94
  };
179
95
 
180
- // Sets the noDelay option of the current native request
181
- RedirectableRequest.prototype.setNoDelay = function (noDelay) {
182
- this._currentRequest.setNoDelay(noDelay);
183
- };
96
+ // Proxy all other public ClientRequest methods
97
+ [
98
+ "abort", "flushHeaders", "getHeader",
99
+ "setNoDelay", "setSocketKeepAlive", "setTimeout",
100
+ ].forEach(function (method) {
101
+ RedirectableRequest.prototype[method] = function (a, b) {
102
+ return this._currentRequest[method](a, b);
103
+ };
104
+ });
184
105
 
185
- // Sets the socketKeepAlive option of the current native request
186
- RedirectableRequest.prototype.setSocketKeepAlive = function (enable, initialDelay) {
187
- this._currentRequest.setSocketKeepAlive(enable, initialDelay);
188
- };
106
+ // Proxy all public ClientRequest properties
107
+ ["aborted", "connection", "socket"].forEach(function (property) {
108
+ Object.defineProperty(RedirectableRequest.prototype, property, {
109
+ get: function () { return this._currentRequest[property]; },
110
+ });
111
+ });
189
112
 
190
- // Sets the timeout option of the current native request
191
- RedirectableRequest.prototype.setTimeout = function (timeout, callback) {
192
- this._currentRequest.setTimeout(timeout, callback);
193
- };
113
+ // Executes the next native request (initial or redirect)
114
+ RedirectableRequest.prototype._performRequest = function () {
115
+ // Load the native protocol
116
+ var protocol = this._options.protocol;
117
+ var nativeProtocol = this._options.nativeProtocols[protocol];
194
118
 
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});
119
+ // If specified, use the agent corresponding to the protocol
120
+ // (HTTP and HTTPS use different types of agents)
121
+ if (this._options.agents) {
122
+ var scheme = protocol.substr(0, protocol.length - 1);
123
+ this._options.agent = this._options.agents[scheme];
124
+ }
125
+
126
+ // Create the native request
127
+ var request = this._currentRequest =
128
+ nativeProtocol.request(this._options, this._onNativeResponse);
129
+ this._currentUrl = url.format(this._options);
130
+
131
+ // Set up event handlers
132
+ request._redirectable = this;
133
+ for (var event in eventHandlers) {
134
+ /* istanbul ignore else */
135
+ if (event) {
136
+ request.on(event, eventHandlers[event]);
137
+ }
138
+ }
139
+
140
+ // End a redirected request
141
+ // (The first request must be ended explicitly with RedirectableRequest#end)
142
+ if (this._isRedirect) {
143
+ // Write the request entity and end.
144
+ var requestBodyBuffers = this._requestBodyBuffers;
145
+ (function writeNext() {
146
+ if (requestBodyBuffers.length !== 0) {
147
+ var buffer = requestBodyBuffers.pop();
148
+ request.write(buffer.data, buffer.encoding, writeNext);
149
+ }
150
+ else {
151
+ request.end();
152
+ }
153
+ }());
154
+ }
199
155
  };
200
156
 
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
- }
157
+ // Processes a response from the current native request
158
+ RedirectableRequest.prototype._processResponse = function (response) {
159
+ // RFC7231§6.4: The 3xx (Redirection) class of status code indicates
160
+ // that further action needs to be taken by the user agent in order to
161
+ // fulfill the request. If a Location header field is provided,
162
+ // the user agent MAY automatically redirect its request to the URI
163
+ // referenced by the Location field value,
164
+ // even if the specific status code is not understood.
165
+ var location = response.headers.location;
166
+ if (location && this._options.followRedirects !== false &&
167
+ response.statusCode >= 300 && response.statusCode < 400) {
168
+ // RFC7231§6.4: A client SHOULD detect and intervene
169
+ // in cyclical redirections (i.e., "infinite" redirection loops).
170
+ if (++this._redirectCount > this._options.maxRedirects) {
171
+ this.emit("error", new Error("Max redirects exceeded."));
172
+ return;
173
+ }
174
+
175
+ // RFC7231§6.4: Automatic redirection needs to done with
176
+ // care for methods not known to be safe […],
177
+ // since the user might not wish to redirect an unsafe request.
178
+ // RFC7231§6.4.7: The 307 (Temporary Redirect) status code indicates
179
+ // that the target resource resides temporarily under a different URI
180
+ // and the user agent MUST NOT change the request method
181
+ // if it performs an automatic redirection to that URI.
182
+ var header;
183
+ var headers = this._options.headers;
184
+ if (response.statusCode !== 307 && !(this._options.method in SAFE_METHODS)) {
185
+ this._options.method = "GET";
186
+ // Drop a possible entity and headers related to it
187
+ this._requestBodyBuffers = [];
188
+ for (header in headers) {
189
+ if (/^content-/i.test(header)) {
190
+ delete headers[header];
191
+ }
192
+ }
193
+ }
194
+
195
+ // Drop the Host header, as the redirect might lead to a different host
196
+ if (!this._isRedirect) {
197
+ for (header in headers) {
198
+ if (/^host$/i.test(header)) {
199
+ delete headers[header];
200
+ }
201
+ }
202
+ }
203
+
204
+ // Perform the redirected request
205
+ var redirectUrl = url.resolve(this._currentUrl, location);
206
+ debug("redirecting to", redirectUrl);
207
+ Object.assign(this._options, url.parse(redirectUrl));
208
+ this._isRedirect = true;
209
+ this._performRequest();
210
+ }
211
+ else {
212
+ // The response is not a redirect; return it as-is
213
+ response.responseUrl = this._currentUrl;
214
+ this.emit("response", response);
215
+
216
+ // Clean up
217
+ this._requestBodyBuffers = [];
218
+ }
207
219
  };
208
220
 
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
- });
221
+ // Wraps the key/value object of protocols with redirect functionality
222
+ function wrap(protocols) {
223
+ // Default settings
224
+ var exports = {
225
+ maxRedirects: 21,
226
+ maxBodyLength: 10 * 1024 * 1024,
227
+ };
228
+
229
+ // Wrap each protocol
230
+ var nativeProtocols = {};
231
+ Object.keys(protocols).forEach(function (scheme) {
232
+ var protocol = scheme + ":";
233
+ var nativeProtocol = nativeProtocols[protocol] = protocols[scheme];
234
+ var wrappedProtocol = exports[scheme] = Object.create(nativeProtocol);
235
+
236
+ // Executes a request, following redirects
237
+ wrappedProtocol.request = function (options, callback) {
238
+ if (typeof options === "string") {
239
+ options = url.parse(options);
240
+ options.maxRedirects = exports.maxRedirects;
241
+ }
242
+ else {
243
+ options = Object.assign({
244
+ protocol: protocol,
245
+ maxRedirects: exports.maxRedirects,
246
+ maxBodyLength: exports.maxBodyLength,
247
+ }, options);
248
+ }
249
+ options.nativeProtocols = nativeProtocols;
250
+ assert.equal(options.protocol, protocol, "protocol mismatch");
251
+ debug("options", options);
252
+ return new RedirectableRequest(options, callback);
253
+ };
254
+
255
+ // Executes a GET request, following redirects
256
+ wrappedProtocol.get = function (options, callback) {
257
+ var request = wrappedProtocol.request(options, callback);
258
+ request.end();
259
+ return request;
260
+ };
261
+ });
262
+ return exports;
263
+ }
264
+
265
+ // Exports
266
+ module.exports = wrap({ http: http, https: https });
267
+ module.exports.wrap = wrap;
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "follow-redirects",
3
- "version": "1.2.5",
3
+ "version": "1.4.1",
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",
@@ -42,16 +44,16 @@
42
44
  "https.js"
43
45
  ],
44
46
  "dependencies": {
45
- "debug": "^2.6.9"
47
+ "debug": "^3.1.0"
46
48
  },
47
49
  "devDependencies": {
48
- "bluebird": "^3.4.0",
49
- "concat-stream": "^1.5.2",
50
- "coveralls": "^2.11.15",
51
- "express": "^4.13.0",
52
- "mocha": "^3.2.0",
53
- "nyc": "^10.0.0",
54
- "xo": "^0.17.1"
50
+ "bluebird": "^3.5.1",
51
+ "concat-stream": "^1.6.0",
52
+ "coveralls": "^3.0.0",
53
+ "eslint": "^4.16.0",
54
+ "express": "^4.16.2",
55
+ "mocha": "^5.0.0",
56
+ "nyc": "^11.4.1"
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
  }