follow-redirects
Advanced tools
Comparing version 0.3.0 to 1.0.0
272
index.js
@@ -10,161 +10,177 @@ 'use strict'; | ||
var nativeProtocols = {'http:': http, 'https:': https}; | ||
var publicApi = module.exports = { | ||
var schemes = {}; | ||
var exports = module.exports = { | ||
maxRedirects: 21 | ||
}; | ||
// RFC7231§4.2.1: Of the request methods defined by this specification, | ||
// the GET, HEAD, OPTIONS, and TRACE methods are defined to be safe. | ||
var safeMethods = {GET: true, HEAD: true, OPTIONS: true, TRACE: true}; | ||
// Wrapper around the native request | ||
function RequestProxy() { | ||
// Create handlers that pass events from native requests | ||
var eventHandlers = Object.create(null); | ||
['abort', 'aborted', 'error'].forEach(function (event) { | ||
eventHandlers[event] = function (arg) { | ||
this._redirectable.emit(event, arg); | ||
}; | ||
}); | ||
// An HTTP(S) request that can be redirected | ||
function RedirectableRequest(options, responseCallback) { | ||
// Initialize the request | ||
Writable.call(this); | ||
} | ||
RequestProxy.prototype = Object.create(Writable.prototype); | ||
this._options = options; | ||
this._redirectCount = 0; | ||
RequestProxy.prototype.abort = function () { | ||
this._request.abort(); | ||
}; | ||
// Attach a callback if passed | ||
if (responseCallback) { | ||
this.on('response', responseCallback); | ||
} | ||
RequestProxy.prototype.end = function (data, encoding, callback) { | ||
this._request.end(data, encoding, callback); | ||
}; | ||
// React to responses of native requests | ||
var self = this; | ||
this._onNativeResponse = function (response) { | ||
self._processResponse(response); | ||
}; | ||
RequestProxy.prototype.flushHeaders = function () { | ||
this._request.flushHeaders(); | ||
}; | ||
// Perform the first request | ||
this._performRequest(); | ||
} | ||
RedirectableRequest.prototype = Object.create(Writable.prototype); | ||
RequestProxy.prototype.setNoDelay = function (noDelay) { | ||
this._request.setNoDelay(noDelay); | ||
}; | ||
// Executes the next native request (initial or redirect) | ||
RedirectableRequest.prototype._performRequest = function () { | ||
// If specified, use the agent corresponding to the protocol | ||
// (HTTP and HTTPS use different types of agents) | ||
var protocol = this._options.protocol; | ||
if (this._options.agents) { | ||
this._options.agent = this._options.agents[schemes[protocol]]; | ||
} | ||
RequestProxy.prototype.setSocketKeepAlive = function (enable, initialDelay) { | ||
this._request.setSocketKeepAlive(enable, initialDelay); | ||
}; | ||
// Create the native request | ||
var nativeProtocol = nativeProtocols[this._options.protocol]; | ||
var request = this._currentRequest = | ||
nativeProtocol.request(this._options, this._onNativeResponse); | ||
this._currentUrl = url.format(this._options); | ||
RequestProxy.prototype.setTimeout = function (timeout, callback) { | ||
this._request.setSocketKeepAlive(timeout, callback); | ||
}; | ||
// Set up event handlers | ||
request._redirectable = this; | ||
for (var event in eventHandlers) { | ||
if (event) { | ||
request.on(event, eventHandlers[event]); | ||
} | ||
} | ||
RequestProxy.prototype._write = function (chunk, encoding, callback) { | ||
this._request.write(chunk, encoding, callback); | ||
// The first request is explicitly ended in RedirectableRequest#end | ||
if (this._currentResponse) { | ||
request.end(); | ||
} | ||
}; | ||
function execute(options, callback) { | ||
var fetchedUrls = []; | ||
var requestProxy = new RequestProxy(); | ||
if (callback) { | ||
requestProxy.on('response', callback); | ||
} | ||
cb(); | ||
return requestProxy; | ||
// Processes a response from the current native request | ||
RedirectableRequest.prototype._processResponse = function (response) { | ||
// RFC7231§6.4: The 3xx (Redirection) class of status code indicates | ||
// that further action needs to be taken by the user agent in order to | ||
// fulfill the request. If a Location header field is provided, | ||
// the user agent MAY automatically redirect its request to the URI | ||
// referenced by the Location field value, | ||
// even if the specific status code is not understood. | ||
var location = response.headers.location; | ||
if (location && this._options.followRedirects !== false && | ||
response.statusCode >= 300 && response.statusCode < 400) { | ||
// RFC7231§6.4: A client SHOULD detect and intervene | ||
// in cyclical redirections (i.e., "infinite" redirection loops). | ||
if (++this._redirectCount > this._options.maxRedirects) { | ||
return this.emit('error', new Error('Max redirects exceeded.')); | ||
} | ||
function cb(res) { | ||
// skip the redirection logic on the first call. | ||
if (res) { | ||
var fetchedUrl = url.format(options); | ||
fetchedUrls.unshift(fetchedUrl); | ||
if (!isRedirect(res)) { | ||
res.fetchedUrls = fetchedUrls; | ||
requestProxy.emit('response', res); | ||
return; | ||
// RFC7231§6.4.7: The 307 (Temporary Redirect) status code indicates | ||
// that the target resource resides temporarily under a different URI | ||
// and the user agent MUST NOT change the request method | ||
// if it performs an automatic redirection to that URI. | ||
if (response.statusCode !== 307) { | ||
// RFC7231§6.4: Automatic redirection needs to done with | ||
// care for methods not known to be safe […], | ||
// since the user might not wish to redirect an unsafe request. | ||
if (!(this._options.method in safeMethods)) { | ||
this._options.method = 'GET'; | ||
} | ||
// need to use url.resolve() in case location is a relative URL | ||
var redirectUrl = url.resolve(fetchedUrl, res.headers.location); | ||
debug('redirecting to', redirectUrl); | ||
extend(options, url.parse(redirectUrl)); | ||
} | ||
if (fetchedUrls.length > options.maxRedirects) { | ||
var err = new Error('Max redirects exceeded.'); | ||
requestProxy.emit('error', err); | ||
return; | ||
} | ||
options.nativeProtocol = nativeProtocols[options.protocol]; | ||
options.defaultRequest = defaultMakeRequest; | ||
var req = (options.makeRequest || defaultMakeRequest)(options, cb, res); | ||
requestProxy._request = req; | ||
mirrorEvent(req, 'abort'); | ||
mirrorEvent(req, 'aborted'); | ||
mirrorEvent(req, 'error'); | ||
return req; | ||
// Perform the redirected request | ||
var redirectUrl = url.resolve(this._currentUrl, location); | ||
debug('redirecting to', redirectUrl); | ||
Object.assign(this._options, url.parse(redirectUrl)); | ||
this._currentResponse = response; | ||
this._performRequest(); | ||
} else { | ||
// The response is not a redirect; return it as-is | ||
response.responseUrl = this._currentUrl; | ||
return this.emit('response', response); | ||
} | ||
}; | ||
function defaultMakeRequest(options, cb, res) { | ||
if (res && res.statusCode !== 307) { | ||
// This is a redirect, so use only GET methods, except for status 307, | ||
// which must honor the previous request method. | ||
options.method = 'GET'; | ||
} | ||
// Aborts the current native request | ||
RedirectableRequest.prototype.abort = function () { | ||
this._currentRequest.abort(); | ||
}; | ||
var req = options.nativeProtocol.request(options, cb); | ||
// Ends the current native request | ||
RedirectableRequest.prototype.end = function (data, encoding, callback) { | ||
this._currentRequest.end(data, encoding, callback); | ||
}; | ||
if (res) { | ||
// We leave the user to call `end` on the first request | ||
req.end(); | ||
} | ||
// Flushes the headers of the current native request | ||
RedirectableRequest.prototype.flushHeaders = function () { | ||
this._currentRequest.flushHeaders(); | ||
}; | ||
return req; | ||
} | ||
// Sets the noDelay option of the current native request | ||
RedirectableRequest.prototype.setNoDelay = function (noDelay) { | ||
this._currentRequest.setNoDelay(noDelay); | ||
}; | ||
// send events through the proxy | ||
function mirrorEvent(req, event) { | ||
req.on(event, function (arg) { | ||
requestProxy.emit(event, arg); | ||
}); | ||
} | ||
} | ||
// Sets the socketKeepAlive option of the current native request | ||
RedirectableRequest.prototype.setSocketKeepAlive = function (enable, initialDelay) { | ||
this._currentRequest.setSocketKeepAlive(enable, initialDelay); | ||
}; | ||
// returns a safe copy of options (or a parsed url object if options was a string). | ||
// validates that the supplied callback is a function | ||
function parseOptions(options, wrappedProtocol) { | ||
if (typeof options === 'string') { | ||
options = url.parse(options); | ||
options.maxRedirects = publicApi.maxRedirects; | ||
} else { | ||
options = extend({ | ||
maxRedirects: publicApi.maxRedirects, | ||
protocol: wrappedProtocol | ||
}, options); | ||
} | ||
assert.equal(options.protocol, wrappedProtocol, 'protocol mismatch'); | ||
// Sets the timeout option of the current native request | ||
RedirectableRequest.prototype.setTimeout = function (timeout, callback) { | ||
this._currentRequest.setTimeout(timeout, callback); | ||
}; | ||
debug('options', options); | ||
return options; | ||
} | ||
// Writes buffered data to the current native request | ||
RedirectableRequest.prototype._write = function (chunk, encoding, callback) { | ||
this._currentRequest.write(chunk, encoding, callback); | ||
}; | ||
// copies source's own properties onto destination and returns destination | ||
function extend(destination, source) { | ||
var keys = Object.keys(source); | ||
for (var i = 0; i < keys.length; i++) { | ||
var key = keys[i]; | ||
destination[key] = source[key]; | ||
} | ||
return destination; | ||
} | ||
// Export a redirecting wrapper for each native protocol | ||
Object.keys(nativeProtocols).forEach(function (protocol) { | ||
var scheme = schemes[protocol] = protocol.substr(0, protocol.length - 1); | ||
var nativeProtocol = nativeProtocols[protocol]; | ||
var wrappedProtocol = exports[scheme] = Object.create(nativeProtocol); | ||
// to redirect the result must have | ||
// a statusCode between 300-399 | ||
// and a `Location` header | ||
function isRedirect(res) { | ||
return (res.statusCode >= 300 && res.statusCode <= 399 && | ||
'location' in res.headers); | ||
} | ||
// Executes an HTTP request, following redirects | ||
wrappedProtocol.request = function (options, callback) { | ||
if (typeof options === 'string') { | ||
options = url.parse(options); | ||
options.maxRedirects = exports.maxRedirects; | ||
} else { | ||
options = Object.assign({ | ||
maxRedirects: exports.maxRedirects, | ||
protocol: protocol | ||
}, options); | ||
} | ||
assert.equal(options.protocol, protocol, 'protocol mismatch'); | ||
debug('options', options); | ||
Object.keys(nativeProtocols).forEach(function (wrappedProtocol) { | ||
var scheme = wrappedProtocol.substr(0, wrappedProtocol.length - 1); | ||
var nativeProtocol = nativeProtocols[wrappedProtocol]; | ||
var protocol = publicApi[scheme] = Object.create(nativeProtocol); | ||
protocol.request = function (options, callback) { | ||
return execute(parseOptions(options, wrappedProtocol), callback); | ||
return new RedirectableRequest(options, callback); | ||
}; | ||
// see https://github.com/joyent/node/blob/master/lib/http.js#L1623 | ||
protocol.get = function (options, callback) { | ||
var req = execute(parseOptions(options, wrappedProtocol), callback); | ||
req.end(); | ||
return req; | ||
// Executes a GET request, following redirects | ||
wrappedProtocol.get = function (options, callback) { | ||
var request = wrappedProtocol.request(options, callback); | ||
request.end(); | ||
return request; | ||
}; | ||
}); |
{ | ||
"name": "follow-redirects", | ||
"version": "0.3.0", | ||
"version": "1.0.0", | ||
"description": "HTTP and HTTPS modules that follow redirects.", | ||
@@ -32,3 +32,4 @@ "main": "index.js", | ||
"contributors": [ | ||
"James Talmage <james@talmage.io>" | ||
"James Talmage <james@talmage.io>", | ||
"Ruben Verborgh <ruben@verborgh.org> (https://ruben.verborgh.org/)" | ||
], | ||
@@ -51,3 +52,2 @@ "files": [ | ||
"nyc": "^8.3.1", | ||
"semver": "^5.3.0", | ||
"xo": "^0.17.0" | ||
@@ -54,0 +54,0 @@ }, |
## Follow Redirects | ||
Drop in replacement for Nodes `http` and `https` that automatically follows redirects. | ||
Drop-in replacement for Nodes `http` and `https` that automatically follows redirects. | ||
@@ -22,4 +22,4 @@ [![npm version](https://badge.fury.io/js/follow-redirects.svg)](https://www.npmjs.com/package/follow-redirects) | ||
http.get('http://bit.ly/900913', function (res) { | ||
res.on('data', function (chunk) { | ||
http.get('http://bit.ly/900913', function (response) { | ||
response.on('data', function (chunk) { | ||
console.log(chunk); | ||
@@ -32,32 +32,54 @@ }); | ||
By default the number of redirects is limited to 5, but you can modify that globally or per request. | ||
You can inspect the final redirected URL through the `responseUrl` property on the `response`. | ||
If no redirection happened, `responseUrl` is the original request URL. | ||
```javascript | ||
require('follow-redirects').maxRedirects = 10; // Has global affect (be careful!) | ||
https.request({ | ||
host: 'bitly.com', | ||
path: '/UHfDGO', | ||
maxRedirects: 3 // per request setting | ||
}, function (res) {/* ... */}); | ||
}, function (response) { | ||
console.log(response.responseUrl); | ||
// 'http://duckduckgo.com/robots.txt' | ||
}); | ||
``` | ||
You can inspect the redirection chain from the `fetchedUrls` array on the `response`. | ||
The array is populated in reverse order, so the original url you requested will be the | ||
last element, while the final redirection point will be at index 0. | ||
## Options | ||
### Global options | ||
Global options are set directly on the `follow-redirects` module: | ||
```javascript | ||
https.request({ | ||
host: 'bitly.com', | ||
path: '/UHfDGO', | ||
}, function (res) { | ||
console.log(res.fetchedUrls); | ||
// [ 'http://duckduckgo.com/robots.txt', 'http://bitly.com/UHfDGO' ] | ||
}); | ||
var followRedirects = require('follow-redirects'); | ||
followRedirects.maxRedirects = 10; | ||
``` | ||
The following global options are supported: | ||
- `maxRedirects` (default: `21`) – sets the maximum number of allowed redirects; if exceeded, an error will be emitted. | ||
### Per-request options | ||
Per-request options are set by passing an `options` object: | ||
```javascript | ||
var url = require('url'); | ||
var followRedirects = require('follow-redirects'); | ||
var options = url.parse('http://bit.ly/900913'); | ||
options.maxRedirects = 10; | ||
http.request(options); | ||
``` | ||
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), | ||
the following per-request options are supported: | ||
- `followRedirects` (default: `true`) – whether redirects should be followed. | ||
- `maxRedirects` (default: `21`) – sets the maximum number of allowed redirects; if exceeded, an error will be emitted. | ||
- `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() }` | ||
## Browserify Usage | ||
Due to the way `XMLHttpRequest` works, the `browserify` versions of `http` and `https` already follow redirects. | ||
If you are *only* targetting the browser, then this library has little value for you. If you want to write cross | ||
If you are *only* targeting the browser, then this library has little value for you. If you want to write cross | ||
platform code for node and the browser, `follow-redirects` provides a great solution for making the native node | ||
@@ -109,8 +131,8 @@ modules behave the same as they do in browserified builds in the browser. To avoid bundling unnecessary code | ||
Olivier Lalonde (olalonde@gmail.com) | ||
- Olivier Lalonde (olalonde@gmail.com) | ||
- James Talmage (james@talmage.io) | ||
- [Ruben Verborgh](https://ruben.verborgh.org/) | ||
James Talmage (james@talmage.io) | ||
## License | ||
MIT: [http://olalonde.mit-license.org](http://olalonde.mit-license.org) |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
13713
7
163
0
136
3