follow-redirects
Advanced tools
Comparing version 1.2.6 to 1.3.0
@@ -1,1 +0,1 @@ | ||
module.exports = require('./').http; | ||
module.exports = require("./").http; |
@@ -1,1 +0,1 @@ | ||
module.exports = require('./').https; | ||
module.exports = require("./").https; |
375
index.js
@@ -1,24 +0,18 @@ | ||
'use strict'; | ||
var url = require('url'); | ||
var assert = require('assert'); | ||
var http = require('http'); | ||
var https = require('https'); | ||
var Writable = require('stream').Writable; | ||
var debug = require('debug')('follow-redirects'); | ||
var url = require("url"); | ||
var http = require("http"); | ||
var https = require("https"); | ||
var assert = require("assert"); | ||
var Writable = require("stream").Writable; | ||
var debug = require("debug")("follow-redirects"); | ||
var nativeProtocols = {'http:': http, 'https:': https}; | ||
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}; | ||
var SAFE_METHODS = { GET: true, HEAD: true, OPTIONS: true, TRACE: true }; | ||
// Create handlers that pass events from native requests | ||
var eventHandlers = Object.create(null); | ||
['abort', 'aborted', 'error', 'socket'].forEach(function (event) { | ||
eventHandlers[event] = function (arg) { | ||
this._redirectable.emit(event, arg); | ||
}; | ||
["abort", "aborted", "error", "socket", "timeout"].forEach(function (event) { | ||
eventHandlers[event] = function (arg) { | ||
this._redirectable.emit(event, arg); | ||
}; | ||
}); | ||
@@ -28,32 +22,34 @@ | ||
function RedirectableRequest(options, responseCallback) { | ||
// Initialize the request | ||
Writable.call(this); | ||
this._options = options; | ||
this._redirectCount = 0; | ||
this._bufferedWrites = []; | ||
// Initialize the request | ||
Writable.call(this); | ||
this._options = options; | ||
this._redirectCount = 0; | ||
this._requestBodyLength = 0; | ||
this._requestBodyBuffers = []; | ||
// Attach a callback if passed | ||
if (responseCallback) { | ||
this.on('response', responseCallback); | ||
} | ||
// Attach a callback if passed | ||
if (responseCallback) { | ||
this.on("response", responseCallback); | ||
} | ||
// React to responses of native requests | ||
var self = this; | ||
this._onNativeResponse = function (response) { | ||
self._processResponse(response); | ||
}; | ||
// React to responses of native requests | ||
var self = this; | ||
this._onNativeResponse = function (response) { | ||
self._processResponse(response); | ||
}; | ||
// Complete the URL object when necessary | ||
if (!options.pathname && options.path) { | ||
var searchPos = options.path.indexOf('?'); | ||
if (searchPos < 0) { | ||
options.pathname = options.path; | ||
} else { | ||
options.pathname = options.path.substring(0, searchPos); | ||
options.search = options.path.substring(searchPos); | ||
} | ||
} | ||
// Complete the URL object when necessary | ||
if (!options.pathname && options.path) { | ||
var searchPos = options.path.indexOf("?"); | ||
if (searchPos < 0) { | ||
options.pathname = options.path; | ||
} | ||
else { | ||
options.pathname = options.path.substring(0, searchPos); | ||
options.search = options.path.substring(searchPos); | ||
} | ||
} | ||
// Perform the first request | ||
this._performRequest(); | ||
// Perform the first request | ||
this._performRequest(); | ||
} | ||
@@ -64,44 +60,42 @@ RedirectableRequest.prototype = Object.create(Writable.prototype); | ||
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]]; | ||
} | ||
// Load the native protocol | ||
var protocol = this._options.protocol; | ||
var nativeProtocol = this._options.nativeProtocols[protocol]; | ||
// Create the native request | ||
var nativeProtocol = nativeProtocols[protocol]; | ||
var request = this._currentRequest = | ||
nativeProtocol.request(this._options, this._onNativeResponse); | ||
this._currentUrl = url.format(this._options); | ||
// If specified, use the agent corresponding to the protocol | ||
// (HTTP and HTTPS use different types of agents) | ||
if (this._options.agents) { | ||
var scheme = protocol.substr(0, protocol.length - 1); | ||
this._options.agent = this._options.agents[scheme]; | ||
} | ||
// Set up event handlers | ||
request._redirectable = this; | ||
for (var event in eventHandlers) { | ||
/* istanbul ignore else */ | ||
if (event) { | ||
request.on(event, eventHandlers[event]); | ||
} | ||
} | ||
// Create the native request | ||
var request = this._currentRequest = | ||
nativeProtocol.request(this._options, this._onNativeResponse); | ||
this._currentUrl = url.format(this._options); | ||
// End a redirected request | ||
// (The first request must be ended explicitly with RedirectableRequest#end) | ||
if (this._isRedirect) { | ||
// If the request doesn't have en entity, end directly. | ||
var bufferedWrites = this._bufferedWrites; | ||
if (bufferedWrites.length === 0) { | ||
request.end(); | ||
// Otherwise, write the request entity and end afterwards. | ||
} else { | ||
var i = 0; | ||
(function writeNext() { | ||
if (i < bufferedWrites.length) { | ||
var bufferedWrite = bufferedWrites[i++]; | ||
request.write(bufferedWrite.data, bufferedWrite.encoding, writeNext); | ||
} else { | ||
request.end(); | ||
} | ||
})(); | ||
} | ||
} | ||
// Set up event handlers | ||
request._redirectable = this; | ||
for (var event in eventHandlers) { | ||
/* istanbul ignore else */ | ||
if (event) { | ||
request.on(event, eventHandlers[event]); | ||
} | ||
} | ||
// End a redirected request | ||
// (The first request must be ended explicitly with RedirectableRequest#end) | ||
if (this._isRedirect) { | ||
// Write the request entity and end. | ||
var requestBodyBuffers = this._requestBodyBuffers; | ||
(function writeNext() { | ||
if (requestBodyBuffers.length !== 0) { | ||
var buffer = requestBodyBuffers.pop(); | ||
request.write(buffer.data, buffer.encoding, writeNext); | ||
} | ||
else { | ||
request.end(); | ||
} | ||
}()); | ||
} | ||
}; | ||
@@ -111,61 +105,63 @@ | ||
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.')); | ||
} | ||
// 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) { | ||
this.emit("error", new Error("Max redirects exceeded.")); | ||
return; | ||
} | ||
// 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. | ||
// 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. | ||
var header; | ||
var headers = this._options.headers; | ||
if (response.statusCode !== 307 && !(this._options.method in safeMethods)) { | ||
this._options.method = 'GET'; | ||
// Drop a possible entity and headers related to it | ||
this._bufferedWrites = []; | ||
for (header in headers) { | ||
if (/^content-/i.test(header)) { | ||
delete headers[header]; | ||
} | ||
} | ||
} | ||
// 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. | ||
// 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. | ||
var header; | ||
var headers = this._options.headers; | ||
if (response.statusCode !== 307 && !(this._options.method in SAFE_METHODS)) { | ||
this._options.method = "GET"; | ||
// Drop a possible entity and headers related to it | ||
this._requestBodyBuffers = []; | ||
for (header in headers) { | ||
if (/^content-/i.test(header)) { | ||
delete headers[header]; | ||
} | ||
} | ||
} | ||
// Drop the Host header, as the redirect might lead to a different host | ||
if (!this._isRedirect) { | ||
for (header in headers) { | ||
if (/^host$/i.test(header)) { | ||
delete headers[header]; | ||
} | ||
} | ||
} | ||
// Drop the Host header, as the redirect might lead to a different host | ||
if (!this._isRedirect) { | ||
for (header in headers) { | ||
if (/^host$/i.test(header)) { | ||
delete headers[header]; | ||
} | ||
} | ||
} | ||
// Perform the redirected request | ||
var redirectUrl = url.resolve(this._currentUrl, location); | ||
debug('redirecting to', redirectUrl); | ||
Object.assign(this._options, url.parse(redirectUrl)); | ||
this._isRedirect = true; | ||
this._performRequest(); | ||
} else { | ||
// The response is not a redirect; return it as-is | ||
response.responseUrl = this._currentUrl; | ||
this.emit('response', response); | ||
// Perform the redirected request | ||
var redirectUrl = url.resolve(this._currentUrl, location); | ||
debug("redirecting to", redirectUrl); | ||
Object.assign(this._options, url.parse(redirectUrl)); | ||
this._isRedirect = true; | ||
this._performRequest(); | ||
} | ||
else { | ||
// The response is not a redirect; return it as-is | ||
response.responseUrl = this._currentUrl; | ||
this.emit("response", response); | ||
// Clean up | ||
delete this._options; | ||
delete this._bufferedWrites; | ||
} | ||
// Clean up | ||
delete this._options; | ||
delete this._requestBodyBuffers; | ||
} | ||
}; | ||
@@ -175,3 +171,3 @@ | ||
RedirectableRequest.prototype.abort = function () { | ||
this._currentRequest.abort(); | ||
this._currentRequest.abort(); | ||
}; | ||
@@ -181,3 +177,3 @@ | ||
RedirectableRequest.prototype.flushHeaders = function () { | ||
this._currentRequest.flushHeaders(); | ||
this._currentRequest.flushHeaders(); | ||
}; | ||
@@ -187,3 +183,3 @@ | ||
RedirectableRequest.prototype.setNoDelay = function (noDelay) { | ||
this._currentRequest.setNoDelay(noDelay); | ||
this._currentRequest.setNoDelay(noDelay); | ||
}; | ||
@@ -193,3 +189,3 @@ | ||
RedirectableRequest.prototype.setSocketKeepAlive = function (enable, initialDelay) { | ||
this._currentRequest.setSocketKeepAlive(enable, initialDelay); | ||
this._currentRequest.setSocketKeepAlive(enable, initialDelay); | ||
}; | ||
@@ -199,3 +195,3 @@ | ||
RedirectableRequest.prototype.setTimeout = function (timeout, callback) { | ||
this._currentRequest.setTimeout(timeout, callback); | ||
this._currentRequest.setTimeout(timeout, callback); | ||
}; | ||
@@ -205,4 +201,11 @@ | ||
RedirectableRequest.prototype.write = function (data, encoding, callback) { | ||
this._currentRequest.write(data, encoding, callback); | ||
this._bufferedWrites.push({data: data, encoding: encoding}); | ||
if (this._requestBodyLength + data.length <= this._options.maxBodyLength) { | ||
this._requestBodyLength += data.length; | ||
this._requestBodyBuffers.push({ data: data, encoding: encoding }); | ||
this._currentRequest.write(data, encoding, callback); | ||
} | ||
else { | ||
this.emit("error", new Error("Request body larger than maxBodyLength limit")); | ||
this.abort(); | ||
} | ||
}; | ||
@@ -212,37 +215,59 @@ | ||
RedirectableRequest.prototype.end = function (data, encoding, callback) { | ||
this._currentRequest.end(data, encoding, callback); | ||
if (data) { | ||
this._bufferedWrites.push({data: data, encoding: encoding}); | ||
} | ||
var currentRequest = this._currentRequest; | ||
if (!data) { | ||
currentRequest.end(null, null, callback); | ||
} | ||
else { | ||
this.write(data, encoding, function () { | ||
currentRequest.end(null, null, callback); | ||
}); | ||
} | ||
}; | ||
// 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); | ||
// Wraps the key/value object of protocols with redirect functionality | ||
function wrap(protocols) { | ||
// Default settings | ||
var exports = { | ||
maxRedirects: 21, | ||
maxBodyLength: 10 * 1024 * 1024, | ||
}; | ||
// 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); | ||
// Wrap each protocol | ||
var nativeProtocols = {}; | ||
Object.keys(protocols).forEach(function (scheme) { | ||
var protocol = scheme + ":"; | ||
var nativeProtocol = nativeProtocols[protocol] = protocols[scheme]; | ||
var wrappedProtocol = exports[scheme] = Object.create(nativeProtocol); | ||
return new RedirectableRequest(options, callback); | ||
}; | ||
// Executes a request, following redirects | ||
wrappedProtocol.request = function (options, callback) { | ||
if (typeof options === "string") { | ||
options = url.parse(options); | ||
options.maxRedirects = exports.maxRedirects; | ||
} | ||
else { | ||
options = Object.assign({ | ||
protocol: protocol, | ||
maxRedirects: exports.maxRedirects, | ||
maxBodyLength: exports.maxBodyLength, | ||
}, options); | ||
} | ||
options.nativeProtocols = nativeProtocols; | ||
assert.equal(options.protocol, protocol, "protocol mismatch"); | ||
debug("options", options); | ||
return new RedirectableRequest(options, callback); | ||
}; | ||
// Executes a GET request, following redirects | ||
wrappedProtocol.get = function (options, callback) { | ||
var request = wrappedProtocol.request(options, callback); | ||
request.end(); | ||
return request; | ||
}; | ||
}); | ||
// Executes a GET request, following redirects | ||
wrappedProtocol.get = function (options, callback) { | ||
var request = wrappedProtocol.request(options, callback); | ||
request.end(); | ||
return request; | ||
}; | ||
}); | ||
return exports; | ||
} | ||
// Exports | ||
module.exports = wrap({ http: http, https: https }); | ||
module.exports.wrap = wrap; |
{ | ||
"name": "follow-redirects", | ||
"version": "1.2.6", | ||
"version": "1.3.0", | ||
"description": "HTTP and HTTPS modules that follow redirects.", | ||
@@ -10,3 +10,5 @@ "main": "index.js", | ||
"scripts": { | ||
"test": "xo && BLUEBIRD_DEBUG=1 nyc mocha" | ||
"test": "npm run lint && npm run mocha", | ||
"lint": "eslint *.js test", | ||
"mocha": "nyc mocha" | ||
}, | ||
@@ -52,6 +54,6 @@ "repository": { | ||
"coveralls": "^3.0.0", | ||
"eslint": "^4.14.0", | ||
"express": "^4.13.0", | ||
"mocha": "^4.0.1", | ||
"nyc": "^11.3.0", | ||
"xo": "^0.17.1" | ||
"nyc": "^11.3.0" | ||
}, | ||
@@ -64,8 +66,3 @@ "license": "MIT", | ||
] | ||
}, | ||
"xo": { | ||
"envs": [ | ||
"mocha" | ||
] | ||
} | ||
} |
@@ -8,3 +8,2 @@ ## Follow Redirects | ||
[![Coverage Status](https://coveralls.io/repos/olalonde/follow-redirects/badge.svg?branch=master)](https://coveralls.io/r/olalonde/follow-redirects?branch=master) | ||
[![Code Climate](https://codeclimate.com/github/olalonde/follow-redirects/badges/gpa.svg)](https://codeclimate.com/github/olalonde/follow-redirects) | ||
[![Dependency Status](https://david-dm.org/olalonde/follow-redirects.svg)](https://david-dm.org/olalonde/follow-redirects) | ||
@@ -52,2 +51,3 @@ [![devDependency Status](https://david-dm.org/olalonde/follow-redirects/dev-status.svg)](https://david-dm.org/olalonde/follow-redirects#info=devDependencies) | ||
followRedirects.maxRedirects = 10; | ||
followRedirects.maxBodyLength = 20 * 1024 * 1024; // 20 MB | ||
``` | ||
@@ -59,3 +59,5 @@ | ||
- `maxBodyLength` (default: 10MB) – sets the maximum size of the request body; if exceeded, an error will be emitted. | ||
### Per-request options | ||
@@ -79,5 +81,23 @@ Per-request options are set by passing an `options` object: | ||
- `maxBodyLength` (default: 10MB) – sets the maximum size of the request body; 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() }` | ||
### Advanced usage | ||
By default, `follow-redirects` will use the Node.js default implementations | ||
of [`http`](https://nodejs.org/api/http.html) | ||
and [`https`](https://nodejs.org/api/https.html). | ||
To enable features such as caching and/or intermediate request tracking, | ||
you might instead want to wrap `follow-redirects` around custom protocol implementations: | ||
```javascript | ||
var followRedirects = require('follow-redirects').wrap({ | ||
http: require('your-custom-http'), | ||
https: require('your-custom-https'), | ||
}); | ||
``` | ||
Such custom protocols only need an implementation of the `request` method. | ||
## Browserify Usage | ||
@@ -84,0 +104,0 @@ |
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
Network access
Supply chain riskThis module accesses the network.
Found 2 instances 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
Network access
Supply chain riskThis module accesses the network.
Found 2 instances in 1 package
18031
236
156