Skip to content

Commit 5a47b01

Browse files
authored
fix: we can redirect disturbed request body if it's not going to be used (#3873)
* fix: we can redirect disturbed request body if it's not going to be used * fixup * fixup * fixup * fixup
1 parent 79f9d2f commit 5a47b01

File tree

3 files changed

+69
-35
lines changed

3 files changed

+69
-35
lines changed

lib/handler/redirect-handler.js

Lines changed: 30 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ class RedirectHandler {
4949
this.maxRedirections = maxRedirections
5050
this.handler = handler
5151
this.history = []
52-
this.redirectionLimitReached = false
5352

5453
if (util.isStream(this.opts.body)) {
5554
// TODO (fix): Provide some way for the user to cache the file to e.g. /tmp
@@ -99,27 +98,42 @@ class RedirectHandler {
9998
this.handler.onError(error)
10099
}
101100

102-
onHeaders (statusCode, headers, resume, statusText) {
103-
this.location = this.history.length >= this.maxRedirections || util.isDisturbed(this.opts.body)
104-
? null
105-
: parseLocation(statusCode, headers)
106-
101+
onHeaders (statusCode, rawHeaders, resume, statusText) {
107102
if (this.opts.throwOnMaxRedirect && this.history.length >= this.maxRedirections) {
108-
if (this.request) {
109-
this.request.abort(new Error('max redirects'))
103+
throw new Error('max redirects')
104+
}
105+
106+
// https://tools.ietf.org/html/rfc7231#section-6.4.2
107+
// https://fetch.spec.whatwg.org/#http-redirect-fetch
108+
// In case of HTTP 301 or 302 with POST, change the method to GET
109+
if ((statusCode === 301 || statusCode === 302) && this.opts.method === 'POST') {
110+
this.opts.method = 'GET'
111+
if (util.isStream(this.opts.body)) {
112+
util.destroy(this.opts.body.on('error', noop))
110113
}
114+
this.opts.body = null
115+
}
111116

112-
this.redirectionLimitReached = true
113-
this.abort(new Error('max redirects'))
114-
return
117+
// https://tools.ietf.org/html/rfc7231#section-6.4.4
118+
// In case of HTTP 303, always replace method to be either HEAD or GET
119+
if (statusCode === 303 && this.opts.method !== 'HEAD') {
120+
this.opts.method = 'GET'
121+
if (util.isStream(this.opts.body)) {
122+
util.destroy(this.opts.body.on('error', noop))
123+
}
124+
this.opts.body = null
115125
}
116126

127+
this.location = this.history.length >= this.maxRedirections || util.isDisturbed(this.opts.body)
128+
? null
129+
: parseLocation(statusCode, rawHeaders)
130+
117131
if (this.opts.origin) {
118132
this.history.push(new URL(this.opts.path, this.opts.origin))
119133
}
120134

121135
if (!this.location) {
122-
return this.handler.onHeaders(statusCode, headers, resume, statusText)
136+
return this.handler.onHeaders(statusCode, rawHeaders, resume, statusText)
123137
}
124138

125139
const { origin, pathname, search } = util.parseURL(new URL(this.location, this.opts.origin && new URL(this.opts.path, this.opts.origin)))
@@ -133,23 +147,6 @@ class RedirectHandler {
133147
this.opts.origin = origin
134148
this.opts.maxRedirections = 0
135149
this.opts.query = null
136-
137-
// https://tools.ietf.org/html/rfc7231#section-6.4.2
138-
// https://fetch.spec.whatwg.org/#http-redirect-fetch
139-
// In case of HTTP 301 or 302 with POST, change the method to GET
140-
if ((statusCode === 301 || statusCode === 302) && this.opts.method === 'POST') {
141-
this.opts.method = 'GET'
142-
if (util.isStream(this.opts.body)) util.destroy(this.opts.body.on('error', noop))
143-
this.opts.body = null
144-
}
145-
146-
// https://tools.ietf.org/html/rfc7231#section-6.4.4
147-
// In case of HTTP 303, always replace method to be either HEAD or GET
148-
if (statusCode === 303 && this.opts.method !== 'HEAD') {
149-
this.opts.method = 'GET'
150-
if (util.isStream(this.opts.body)) util.destroy(this.opts.body.on('error', noop))
151-
this.opts.body = null
152-
}
153150
}
154151

155152
onData (chunk) {
@@ -203,14 +200,14 @@ class RedirectHandler {
203200
}
204201
}
205202

206-
function parseLocation (statusCode, headers) {
203+
function parseLocation (statusCode, rawHeaders) {
207204
if (redirectableStatusCodes.indexOf(statusCode) === -1) {
208205
return null
209206
}
210207

211-
for (let i = 0; i < headers.length; i += 2) {
212-
if (headers[i].length === 8 && util.headerNameToString(headers[i]) === 'location') {
213-
return headers[i + 1]
208+
for (let i = 0; i < rawHeaders.length; i += 2) {
209+
if (rawHeaders[i].length === 8 && util.headerNameToString(rawHeaders[i]) === 'location') {
210+
return rawHeaders[i + 1]
214211
}
215212
}
216213
}

test/interceptors/redirect.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -564,7 +564,7 @@ for (const factory of [
564564
headers,
565565
body: bodyStream
566566
} = await request(t, server, undefined, `http://${server}/301`, {
567-
method: 'POST',
567+
method: 'PUT',
568568
body: createReadable('REQUEST'),
569569
maxRedirections: 10
570570
})
@@ -576,6 +576,26 @@ for (const factory of [
576576
t.strictEqual(body.length, 0)
577577
await t.completed
578578
})
579+
580+
test('should follow redirects when using Readable request bodies w/ POST 101', async t => {
581+
t = tspl(t, { plan: 1 })
582+
583+
const server = await startRedirectingServer()
584+
585+
const {
586+
statusCode,
587+
body: bodyStream
588+
} = await request(t, server, undefined, `http://${server}/301`, {
589+
method: 'POST',
590+
body: createReadable('REQUEST'),
591+
maxRedirections: 10
592+
})
593+
594+
await bodyStream.text()
595+
596+
t.strictEqual(statusCode, 200)
597+
await t.completed
598+
})
579599
}
580600

581601
test('should follow redirections when going cross origin', async t => {

test/redirect-request.js

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -505,7 +505,7 @@ for (const factory of [
505505
const server = await startRedirectingServer()
506506

507507
const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/301`, {
508-
method: 'POST',
508+
method: 'PUT',
509509
body: createReadable('REQUEST'),
510510
maxRedirections: 10
511511
})
@@ -517,6 +517,23 @@ for (const factory of [
517517
t.strictEqual(body.length, 0)
518518
await t.completed
519519
})
520+
521+
test('should follow redirects when using Readable request bodies for POST 301', async t => {
522+
t = tspl(t, { plan: 1 })
523+
524+
const server = await startRedirectingServer()
525+
526+
const { statusCode, body: bodyStream } = await request(t, server, undefined, `http://${server}/301`, {
527+
method: 'POST',
528+
body: createReadable('REQUEST'),
529+
maxRedirections: 10
530+
})
531+
532+
await bodyStream.text()
533+
534+
t.strictEqual(statusCode, 200)
535+
await t.completed
536+
})
520537
}
521538

522539
test('should follow redirections when going cross origin', async t => {

0 commit comments

Comments
 (0)