follow-redirects 0.3.0 → 1.0.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 (3) hide show
  1. package/README.md +44 -22
  2. package/index.js +147 -131
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ## Follow Redirects
2
2
 
3
- Drop in replacement for Nodes `http` and `https` that automatically follows redirects.
3
+ Drop-in replacement for Nodes `http` and `https` that automatically follows redirects.
4
4
 
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)
@@ -19,8 +19,8 @@ Drop in replacement for Nodes `http` and `https` that automatically follows redi
19
19
  var http = require('follow-redirects').http;
20
20
  var https = require('follow-redirects').https;
21
21
 
22
- http.get('http://bit.ly/900913', function (res) {
23
- res.on('data', function (chunk) {
22
+ http.get('http://bit.ly/900913', function (response) {
23
+ response.on('data', function (chunk) {
24
24
  console.log(chunk);
25
25
  });
26
26
  }).on('error', function (err) {
@@ -28,36 +28,58 @@ http.get('http://bit.ly/900913', function (res) {
28
28
  });
29
29
  ```
30
30
 
31
- By default the number of redirects is limited to 5, but you can modify that globally or per request.
31
+ You can inspect the final redirected URL through the `responseUrl` property on the `response`.
32
+ If no redirection happened, `responseUrl` is the original request URL.
32
33
 
33
34
  ```javascript
34
- require('follow-redirects').maxRedirects = 10; // Has global affect (be careful!)
35
-
36
35
  https.request({
37
36
  host: 'bitly.com',
38
37
  path: '/UHfDGO',
39
- maxRedirects: 3 // per request setting
40
- }, function (res) {/* ... */});
38
+ }, function (response) {
39
+ console.log(response.responseUrl);
40
+ // 'http://duckduckgo.com/robots.txt'
41
+ });
41
42
  ```
42
43
 
43
- You can inspect the redirection chain from the `fetchedUrls` array on the `response`.
44
- The array is populated in reverse order, so the original url you requested will be the
45
- last element, while the final redirection point will be at index 0.
44
+ ## Options
45
+ ### Global options
46
+ Global options are set directly on the `follow-redirects` module:
46
47
 
47
48
  ```javascript
48
- https.request({
49
- host: 'bitly.com',
50
- path: '/UHfDGO',
51
- }, function (res) {
52
- console.log(res.fetchedUrls);
53
- // [ 'http://duckduckgo.com/robots.txt', 'http://bitly.com/UHfDGO' ]
54
- });
49
+ var followRedirects = require('follow-redirects');
50
+ followRedirects.maxRedirects = 10;
55
51
  ```
56
52
 
53
+ The following global options are supported:
54
+
55
+ - `maxRedirects` (default: `21`) – sets the maximum number of allowed redirects; if exceeded, an error will be emitted.
56
+
57
+
58
+ ### Per-request options
59
+ Per-request options are set by passing an `options` object:
60
+
61
+ ```javascript
62
+ var url = require('url');
63
+ var followRedirects = require('follow-redirects');
64
+
65
+ var options = url.parse('http://bit.ly/900913');
66
+ options.maxRedirects = 10;
67
+ http.request(options);
68
+ ```
69
+
70
+ In addition to the [standard HTTP](https://nodejs.org/api/http.html#http_http_request_options_callback) and [HTTPS options](https://nodejs.org/api/https.html#https_https_request_options_callback),
71
+ the following per-request options are supported:
72
+ - `followRedirects` (default: `true`) – whether redirects should be followed.
73
+
74
+ - `maxRedirects` (default: `21`) – sets the maximum number of allowed redirects; if exceeded, an error will be emitted.
75
+
76
+ - `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
+
78
+
57
79
  ## Browserify Usage
58
80
 
59
81
  Due to the way `XMLHttpRequest` works, the `browserify` versions of `http` and `https` already follow redirects.
60
- If you are *only* targetting the browser, then this library has little value for you. If you want to write cross
82
+ If you are *only* targeting the browser, then this library has little value for you. If you want to write cross
61
83
  platform code for node and the browser, `follow-redirects` provides a great solution for making the native node
62
84
  modules behave the same as they do in browserified builds in the browser. To avoid bundling unnecessary code
63
85
  you should tell browserify to swap out `follow-redirects` with the standard modules when bundling.
@@ -104,9 +126,9 @@ Pull Requests are always welcome. Please [file an issue](https://github.com/olal
104
126
 
105
127
  ## Authors
106
128
 
107
- Olivier Lalonde (olalonde@gmail.com)
108
-
109
- James Talmage (james@talmage.io)
129
+ - Olivier Lalonde (olalonde@gmail.com)
130
+ - James Talmage (james@talmage.io)
131
+ - [Ruben Verborgh](https://ruben.verborgh.org/)
110
132
 
111
133
  ## License
112
134
 
package/index.js CHANGED
@@ -7,163 +7,179 @@ var Writable = require('stream').Writable;
7
7
  var debug = require('debug')('follow-redirects');
8
8
 
9
9
  var nativeProtocols = {'http:': http, 'https:': https};
10
-
11
- var publicApi = module.exports = {
10
+ var schemes = {};
11
+ var exports = module.exports = {
12
12
  maxRedirects: 21
13
13
  };
14
+ // RFC7231§4.2.1: Of the request methods defined by this specification,
15
+ // the GET, HEAD, OPTIONS, and TRACE methods are defined to be safe.
16
+ var safeMethods = {GET: true, HEAD: true, OPTIONS: true, TRACE: true};
17
+
18
+ // Create handlers that pass events from native requests
19
+ var eventHandlers = Object.create(null);
20
+ ['abort', 'aborted', 'error'].forEach(function (event) {
21
+ eventHandlers[event] = function (arg) {
22
+ this._redirectable.emit(event, arg);
23
+ };
24
+ });
14
25
 
15
- // Wrapper around the native request
16
- function RequestProxy() {
26
+ // An HTTP(S) request that can be redirected
27
+ function RedirectableRequest(options, responseCallback) {
28
+ // Initialize the request
17
29
  Writable.call(this);
18
- }
19
- RequestProxy.prototype = Object.create(Writable.prototype);
20
-
21
- RequestProxy.prototype.abort = function () {
22
- this._request.abort();
23
- };
24
-
25
- RequestProxy.prototype.end = function (data, encoding, callback) {
26
- this._request.end(data, encoding, callback);
27
- };
28
-
29
- RequestProxy.prototype.flushHeaders = function () {
30
- this._request.flushHeaders();
31
- };
32
-
33
- RequestProxy.prototype.setNoDelay = function (noDelay) {
34
- this._request.setNoDelay(noDelay);
35
- };
30
+ this._options = options;
31
+ this._redirectCount = 0;
36
32
 
37
- RequestProxy.prototype.setSocketKeepAlive = function (enable, initialDelay) {
38
- this._request.setSocketKeepAlive(enable, initialDelay);
39
- };
40
-
41
- RequestProxy.prototype.setTimeout = function (timeout, callback) {
42
- this._request.setSocketKeepAlive(timeout, callback);
43
- };
33
+ // Attach a callback if passed
34
+ if (responseCallback) {
35
+ this.on('response', responseCallback);
36
+ }
44
37
 
45
- RequestProxy.prototype._write = function (chunk, encoding, callback) {
46
- this._request.write(chunk, encoding, callback);
47
- };
38
+ // React to responses of native requests
39
+ var self = this;
40
+ this._onNativeResponse = function (response) {
41
+ self._processResponse(response);
42
+ };
48
43
 
49
- function execute(options, callback) {
50
- var fetchedUrls = [];
51
- var requestProxy = new RequestProxy();
52
- if (callback) {
53
- requestProxy.on('response', callback);
44
+ // Perform the first request
45
+ this._performRequest();
46
+ }
47
+ RedirectableRequest.prototype = Object.create(Writable.prototype);
48
+
49
+ // Executes the next native request (initial or redirect)
50
+ RedirectableRequest.prototype._performRequest = function () {
51
+ // If specified, use the agent corresponding to the protocol
52
+ // (HTTP and HTTPS use different types of agents)
53
+ var protocol = this._options.protocol;
54
+ if (this._options.agents) {
55
+ this._options.agent = this._options.agents[schemes[protocol]];
54
56
  }
55
- cb();
56
- return requestProxy;
57
-
58
- function cb(res) {
59
- // skip the redirection logic on the first call.
60
- if (res) {
61
- var fetchedUrl = url.format(options);
62
- fetchedUrls.unshift(fetchedUrl);
63
-
64
- if (!isRedirect(res)) {
65
- res.fetchedUrls = fetchedUrls;
66
- requestProxy.emit('response', res);
67
- return;
68
- }
69
-
70
- // need to use url.resolve() in case location is a relative URL
71
- var redirectUrl = url.resolve(fetchedUrl, res.headers.location);
72
- debug('redirecting to', redirectUrl);
73
- extend(options, url.parse(redirectUrl));
74
- }
75
57
 
76
- if (fetchedUrls.length > options.maxRedirects) {
77
- var err = new Error('Max redirects exceeded.');
78
- requestProxy.emit('error', err);
79
- return;
58
+ // Create the native request
59
+ var nativeProtocol = nativeProtocols[this._options.protocol];
60
+ var request = this._currentRequest =
61
+ nativeProtocol.request(this._options, this._onNativeResponse);
62
+ this._currentUrl = url.format(this._options);
63
+
64
+ // Set up event handlers
65
+ request._redirectable = this;
66
+ for (var event in eventHandlers) {
67
+ if (event) {
68
+ request.on(event, eventHandlers[event]);
80
69
  }
70
+ }
81
71
 
82
- options.nativeProtocol = nativeProtocols[options.protocol];
83
- options.defaultRequest = defaultMakeRequest;
84
-
85
- var req = (options.makeRequest || defaultMakeRequest)(options, cb, res);
86
- requestProxy._request = req;
87
- mirrorEvent(req, 'abort');
88
- mirrorEvent(req, 'aborted');
89
- mirrorEvent(req, 'error');
90
- return req;
72
+ // The first request is explicitly ended in RedirectableRequest#end
73
+ if (this._currentResponse) {
74
+ request.end();
91
75
  }
76
+ };
92
77
 
93
- function defaultMakeRequest(options, cb, res) {
94
- if (res && res.statusCode !== 307) {
95
- // This is a redirect, so use only GET methods, except for status 307,
96
- // which must honor the previous request method.
97
- options.method = 'GET';
78
+ // Processes a response from the current native request
79
+ RedirectableRequest.prototype._processResponse = function (response) {
80
+ // RFC7231§6.4: The 3xx (Redirection) class of status code indicates
81
+ // that further action needs to be taken by the user agent in order to
82
+ // fulfill the request. If a Location header field is provided,
83
+ // the user agent MAY automatically redirect its request to the URI
84
+ // referenced by the Location field value,
85
+ // even if the specific status code is not understood.
86
+ var location = response.headers.location;
87
+ if (location && this._options.followRedirects !== false &&
88
+ response.statusCode >= 300 && response.statusCode < 400) {
89
+ // RFC7231§6.4: A client SHOULD detect and intervene
90
+ // in cyclical redirections (i.e., "infinite" redirection loops).
91
+ if (++this._redirectCount > this._options.maxRedirects) {
92
+ return this.emit('error', new Error('Max redirects exceeded.'));
98
93
  }
99
94
 
100
- var req = options.nativeProtocol.request(options, cb);
101
-
102
- if (res) {
103
- // We leave the user to call `end` on the first request
104
- req.end();
95
+ // RFC7231§6.4.7: The 307 (Temporary Redirect) status code indicates
96
+ // that the target resource resides temporarily under a different URI
97
+ // and the user agent MUST NOT change the request method
98
+ // if it performs an automatic redirection to that URI.
99
+ if (response.statusCode !== 307) {
100
+ // RFC7231§6.4: Automatic redirection needs to done with
101
+ // care for methods not known to be safe […],
102
+ // since the user might not wish to redirect an unsafe request.
103
+ if (!(this._options.method in safeMethods)) {
104
+ this._options.method = 'GET';
105
+ }
105
106
  }
106
107
 
107
- return req;
108
+ // Perform the redirected request
109
+ var redirectUrl = url.resolve(this._currentUrl, location);
110
+ debug('redirecting to', redirectUrl);
111
+ Object.assign(this._options, url.parse(redirectUrl));
112
+ this._currentResponse = response;
113
+ this._performRequest();
114
+ } else {
115
+ // The response is not a redirect; return it as-is
116
+ response.responseUrl = this._currentUrl;
117
+ return this.emit('response', response);
108
118
  }
119
+ };
109
120
 
110
- // send events through the proxy
111
- function mirrorEvent(req, event) {
112
- req.on(event, function (arg) {
113
- requestProxy.emit(event, arg);
114
- });
115
- }
116
- }
121
+ // Aborts the current native request
122
+ RedirectableRequest.prototype.abort = function () {
123
+ this._currentRequest.abort();
124
+ };
117
125
 
118
- // returns a safe copy of options (or a parsed url object if options was a string).
119
- // validates that the supplied callback is a function
120
- function parseOptions(options, wrappedProtocol) {
121
- if (typeof options === 'string') {
122
- options = url.parse(options);
123
- options.maxRedirects = publicApi.maxRedirects;
124
- } else {
125
- options = extend({
126
- maxRedirects: publicApi.maxRedirects,
127
- protocol: wrappedProtocol
128
- }, options);
129
- }
130
- assert.equal(options.protocol, wrappedProtocol, 'protocol mismatch');
126
+ // Ends the current native request
127
+ RedirectableRequest.prototype.end = function (data, encoding, callback) {
128
+ this._currentRequest.end(data, encoding, callback);
129
+ };
131
130
 
132
- debug('options', options);
133
- return options;
134
- }
131
+ // Flushes the headers of the current native request
132
+ RedirectableRequest.prototype.flushHeaders = function () {
133
+ this._currentRequest.flushHeaders();
134
+ };
135
135
 
136
- // copies source's own properties onto destination and returns destination
137
- function extend(destination, source) {
138
- var keys = Object.keys(source);
139
- for (var i = 0; i < keys.length; i++) {
140
- var key = keys[i];
141
- destination[key] = source[key];
142
- }
143
- return destination;
144
- }
136
+ // Sets the noDelay option of the current native request
137
+ RedirectableRequest.prototype.setNoDelay = function (noDelay) {
138
+ this._currentRequest.setNoDelay(noDelay);
139
+ };
145
140
 
146
- // to redirect the result must have
147
- // a statusCode between 300-399
148
- // and a `Location` header
149
- function isRedirect(res) {
150
- return (res.statusCode >= 300 && res.statusCode <= 399 &&
151
- 'location' in res.headers);
152
- }
141
+ // Sets the socketKeepAlive option of the current native request
142
+ RedirectableRequest.prototype.setSocketKeepAlive = function (enable, initialDelay) {
143
+ this._currentRequest.setSocketKeepAlive(enable, initialDelay);
144
+ };
153
145
 
154
- Object.keys(nativeProtocols).forEach(function (wrappedProtocol) {
155
- var scheme = wrappedProtocol.substr(0, wrappedProtocol.length - 1);
156
- var nativeProtocol = nativeProtocols[wrappedProtocol];
157
- var protocol = publicApi[scheme] = Object.create(nativeProtocol);
146
+ // Sets the timeout option of the current native request
147
+ RedirectableRequest.prototype.setTimeout = function (timeout, callback) {
148
+ this._currentRequest.setTimeout(timeout, callback);
149
+ };
150
+
151
+ // Writes buffered data to the current native request
152
+ RedirectableRequest.prototype._write = function (chunk, encoding, callback) {
153
+ this._currentRequest.write(chunk, encoding, callback);
154
+ };
155
+
156
+ // Export a redirecting wrapper for each native protocol
157
+ Object.keys(nativeProtocols).forEach(function (protocol) {
158
+ var scheme = schemes[protocol] = protocol.substr(0, protocol.length - 1);
159
+ var nativeProtocol = nativeProtocols[protocol];
160
+ var wrappedProtocol = exports[scheme] = Object.create(nativeProtocol);
161
+
162
+ // Executes an HTTP request, following redirects
163
+ wrappedProtocol.request = function (options, callback) {
164
+ if (typeof options === 'string') {
165
+ options = url.parse(options);
166
+ options.maxRedirects = exports.maxRedirects;
167
+ } else {
168
+ options = Object.assign({
169
+ maxRedirects: exports.maxRedirects,
170
+ protocol: protocol
171
+ }, options);
172
+ }
173
+ assert.equal(options.protocol, protocol, 'protocol mismatch');
174
+ debug('options', options);
158
175
 
159
- protocol.request = function (options, callback) {
160
- return execute(parseOptions(options, wrappedProtocol), callback);
176
+ return new RedirectableRequest(options, callback);
161
177
  };
162
178
 
163
- // see https://github.com/joyent/node/blob/master/lib/http.js#L1623
164
- protocol.get = function (options, callback) {
165
- var req = execute(parseOptions(options, wrappedProtocol), callback);
166
- req.end();
167
- return req;
179
+ // Executes a GET request, following redirects
180
+ wrappedProtocol.get = function (options, callback) {
181
+ var request = wrappedProtocol.request(options, callback);
182
+ request.end();
183
+ return request;
168
184
  };
169
185
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "follow-redirects",
3
- "version": "0.3.0",
3
+ "version": "1.0.0",
4
4
  "description": "HTTP and HTTPS modules that follow redirects.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -29,7 +29,8 @@
29
29
  "url": "http://www.syskall.com"
30
30
  },
31
31
  "contributors": [
32
- "James Talmage <james@talmage.io>"
32
+ "James Talmage <james@talmage.io>",
33
+ "Ruben Verborgh <ruben@verborgh.org> (https://ruben.verborgh.org/)"
33
34
  ],
34
35
  "files": [
35
36
  "index.js",
@@ -47,7 +48,6 @@
47
48
  "express": "^4.13.0",
48
49
  "mocha": "^3.1.2",
49
50
  "nyc": "^8.3.1",
50
- "semver": "^5.3.0",
51
51
  "xo": "^0.17.0"
52
52
  },
53
53
  "license": "MIT",