Skip to content
This repository has been archived by the owner on Dec 16, 2024. It is now read-only.

Commit

Permalink
Support asynchronous followRedirect filter (callback or promise)
Browse files Browse the repository at this point in the history
Cherry-pick 47cdc67

Co-authored-by: Brian Reavis <[email protected]>
  • Loading branch information
2 people authored and kevinvanrijn committed Sep 24, 2024
1 parent e993e40 commit b996f35
Show file tree
Hide file tree
Showing 4 changed files with 360 additions and 235 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -932,7 +932,10 @@ The first argument can be either a `url` or an `options` object. The only requir

---

- `followRedirect` - follow HTTP 3xx responses as redirects (default: `true`). This property can also be implemented as function which gets `response` object as a single argument and should return `true` if redirects should continue or `false` otherwise.
- `followRedirect` - follow HTTP 3xx responses as redirects (default: `true`). This property can also be implemented as function which gets `response` object as the first argument.
- *(synchronous usage)* It should return `true` if redirects should continue or `false` otherwise. If it returns a url string, the destination of the redirect will be overridden.
- *(async callback usage)* If the function has two arguments, it will be treated as an asynchronous function and will be passed a callback as the second argument. Invoke the callback with an error (`null` if no error) and the boolean/url result as the second.
- *(async promise usage)* Return a promise that resolves to the boolean/url result.
- `followAllRedirects` - follow non-GET HTTP 3xx responses as redirects (default: `false`)
- `followOriginalHttpMethod` - by default we redirect to HTTP method GET. you can enable this property to redirect to the original HTTP method (default: `false`)
- `followAuthorizationHeader` - retain `authorization` header when a redirect happens to a different hostname (default: `false`)
Expand Down
217 changes: 119 additions & 98 deletions lib/redirect.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,136 +83,157 @@ Redirect.prototype.redirectTo = function (response) {
return redirectTo
}

Redirect.prototype.onResponse = function (response) {
Redirect.prototype.onResponse = function (response, callback) {
var self = this
var request = self.request
var urlParser = request.urlParser
var options = {}

var redirectTo = self.redirectTo(response)
if (!redirectTo || !self.allowRedirect.call(request, response)) {
return false
}
if (!redirectTo) return callback(null, false)

function processRedirect (shouldRedirect) {
if (!shouldRedirect) return callback(null, false)
if (typeof shouldRedirect === 'string') {
// overridden redirect url
request.debug('redirect overridden', redirectTo)
redirectTo = shouldRedirect
}

request.debug('redirect to', redirectTo)
request.debug('redirect to', redirectTo)

// ignore any potential response body. it cannot possibly be useful
// to us at this point.
// response.resume should be defined, but check anyway before calling. Workaround for browserify.
if (response.resume) {
response.resume()
}
// ignore any potential response body. it cannot possibly be useful
// to us at this point.
// response.resume should be defined, but check anyway before calling. Workaround for browserify.
if (response.resume) {
response.resume()
}

if (self.redirectsFollowed >= self.maxRedirects) {
request.emit('error', new Error('Exceeded maxRedirects. Probably stuck in a redirect loop ' + request.uri.href))
return false
}
self.redirectsFollowed += 1
if (self.redirectsFollowed >= self.maxRedirects) {
return callback(new Error('Exceeded maxRedirects. Probably stuck in a redirect loop ' + request.uri.href))
}
self.redirectsFollowed += 1

if (!isUrl.test(redirectTo)) {
redirectTo = urlParser.resolve(request.uri.href, redirectTo)
}
if (!isUrl.test(redirectTo)) {
redirectTo = urlParser.resolve(request.uri.href, redirectTo)
}

var uriPrev = request.uri
request.uri = urlParser.parse(redirectTo)
var uriPrev = request.uri
request.uri = urlParser.parse(redirectTo)

// handle the case where we change protocol from https to http or vice versa
if (request.uri.protocol !== uriPrev.protocol) {
delete request.agent
}
// handle the case where we change protocol from https to http or vice versa
if (request.uri.protocol !== uriPrev.protocol) {
delete request.agent
}

self.redirects.push({ statusCode: response.statusCode, redirectUri: redirectTo })
self.redirects.push({ statusCode: response.statusCode, redirectUri: redirectTo })

// if the redirect hostname (not just port or protocol) is changed:
// 1. remove host header, the new host will be populated on request.init
// 2. remove authorization header, avoid authentication leak
// @note: This is done because of security reasons, irrespective of the
// status code or request method used.
if (request.headers && uriPrev.hostname !== request.uri.hostname) {
request.removeHeader('host')
// if the redirect hostname (not just port or protocol) is changed:
// 1. remove host header, the new host will be populated on request.init
// 2. remove authorization header, avoid authentication leak
// @note: This is done because of security reasons, irrespective of the
// status code or request method used.
if (request.headers && uriPrev.hostname !== request.uri.hostname) {
request.removeHeader('host')

// use followAuthorizationHeader option to retain authorization header
if (!self.followAuthorizationHeader) {
request.removeHeader('authorization')
// use followAuthorizationHeader option to retain authorization header
if (!self.followAuthorizationHeader) {
request.removeHeader('authorization')
}
}
}

delete request.src
delete request.req
delete request._started

// Switch request method to GET
// - if followOriginalHttpMethod is not set [OVERRIDE]
// - or, statusCode code is not 401, 307 or 308 [STANDARD]
// - also, remove request body for the GET redirect [STANDARD]
// @note: when followOriginalHttpMethod is set,
// it will always retain the request body irrespective of the method (say GET) or status code (any 3XX).
if (!self.followOriginalHttpMethod &&
response.statusCode !== 401 && response.statusCode !== 307 && response.statusCode !== 308) {
// force all redirects to use GET (legacy reasons)
// but, HEAD is considered as a safe method so, the method is retained.
if (request.method !== 'HEAD') {
request.method = 'GET'
delete request.src
delete request.req
delete request._started

// Switch request method to GET
// - if followOriginalHttpMethod is not set [OVERRIDE]
// - or, statusCode code is not 401, 307 or 308 [STANDARD]
// - also, remove request body for the GET redirect [STANDARD]
// @note: when followOriginalHttpMethod is set,
// it will always retain the request body irrespective of the method (say GET) or status code (any 3XX).
if (!self.followOriginalHttpMethod &&
response.statusCode !== 401 && response.statusCode !== 307 && response.statusCode !== 308) {
// force all redirects to use GET (legacy reasons)
// but, HEAD is considered as a safe method so, the method is retained.
if (request.method !== 'HEAD') {
request.method = 'GET'
}

// Remove parameters from the previous response, unless this is the second request
// for a server that requires digest authentication.
delete request.body
delete request._form
delete request._multipart
if (request.headers) {
request.removeHeader('content-type')
request.removeHeader('content-length')
}
}

// Remove parameters from the previous response, unless this is the second request
// for a server that requires digest authentication.
delete request.body
delete request._form
delete request._multipart
if (request.headers) {
// Restore form-data stream if request body is retained
if (request.formData &&
// make sure _form is released and there's no pending _streams left
// which will be the case for 401 redirects. so, reuse _form on redirect
// @note: multiple form-param / file-streams may cause following issue:
// https://github.com/request/request/issues/887
// @todo: expose stream errors as events
request._form && request._form._released &&
request._form._streams && !request._form._streams.length) {
// reinitialize FormData stream for 307 or 308 redirects
delete request._form
// remove content-type header for new boundary
request.removeHeader('content-type')
// remove content-length header since FormValue may be dropped if its not a file stream
request.removeHeader('content-length')
}
}

// Restore form-data stream if request body is retained
if (request.formData &&
// make sure _form is released and there's no pending _streams left
// which will be the case for 401 redirects. so, reuse _form on redirect
// @note: multiple form-param / file-streams may cause following issue:
// https://github.com/request/request/issues/887
// @todo: expose stream errors as events
request._form && request._form._released &&
request._form._streams && !request._form._streams.length) {
// reinitialize FormData stream for 307 or 308 redirects
delete request._form
// remove content-type header for new boundary
request.removeHeader('content-type')
// remove content-length header since FormValue may be dropped if its not a file stream
request.removeHeader('content-length')

var formData = []
var resetFormData = function (key, value, paramOptions) {
// if `value` is of type stream
if (typeof (value && value.pipe) === 'function') {
// bail out if not a file stream
if (!(value.hasOwnProperty('fd') && value.path)) return
// create new file stream
value = fs.createReadStream(value.path)
var formData = []
var resetFormData = function (key, value, paramOptions) {
// if `value` is of type stream
if (typeof (value && value.pipe) === 'function') {
// bail out if not a file stream
if (!(value.hasOwnProperty('fd') && value.path)) return
// create new file stream
value = fs.createReadStream(value.path)
}

formData.push({key: key, value: value, options: paramOptions})
}
for (var i = 0, ii = request.formData.length; i < ii; i++) {
var formParam = request.formData[i]
if (!formParam) { continue }
resetFormData(formParam.key, formParam.value, formParam.options)
}

formData.push({key: key, value: value, options: paramOptions})
// setting `options.formData` will reinitialize FormData in `request.init`
options.formData = formData
}
for (var i = 0, ii = request.formData.length; i < ii; i++) {
var formParam = request.formData[i]
if (!formParam) { continue }
resetFormData(formParam.key, formParam.value, formParam.options)

if (!self.removeRefererHeader) {
request.setHeader('Referer', uriPrev.href)
}

// setting `options.formData` will reinitialize FormData in `request.init`
options.formData = formData
request.emit('redirect')
request.init(options)
callback(null, true)
}

if (!self.removeRefererHeader) {
request.setHeader('Referer', uriPrev.href)
// test allowRedirect arity; if has more than one argument,
// assume it's asynchronous via a callback
if (self.allowRedirect.length > 1) {
return self.allowRedirect.call(request, response, function (err, result) {
if (err) return callback(err)
processRedirect(result)
})
}

request.emit('redirect')

request.init(options)
var allowsRedirect = self.allowRedirect.call(request, response)
if (allowsRedirect && allowsRedirect.then) {
return allowsRedirect.then(processRedirect, callback)
}

return true
// treat as a regular boolean
processRedirect(allowsRedirect)
}

exports.Redirect = Redirect
Loading

0 comments on commit b996f35

Please sign in to comment.