'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const undici = require('..')
const {
startRedirectingServer,
startRedirectingWithBodyServer,
startRedirectingChainServers,
startRedirectingWithoutLocationServer,
startRedirectingWithAuthorization,
startRedirectingWithCookie,
startRedirectingWithQueryParams
} = require('./utils/redirecting-servers')
const { createReadable, createReadableStream } = require('./utils/stream')
const { Headers: UndiciHeaders } = require('..')
const redirect = undici.interceptors.redirect
for (const factory of [
(server, opts) => new undici.Agent(opts).compose(redirect({ maxRedirections: opts?.maxRedirections })),
(server, opts) => new undici.Pool(`http://${server}`, opts).compose(redirect({ maxRedirections: opts?.maxRedirections })),
(server, opts) => new undici.Client(`http://${server}`, opts).compose(redirect({ maxRedirections: opts?.maxRedirections }))
]) {
const request = (t, server, opts, ...args) => {
const dispatcher = factory(server, opts)
after(() => dispatcher.close())
return undici.request(args[0], { ...args[1], dispatcher }, args[2])
}
test('should always have a history with the final URL even if no redirections were followed', async t => {
t = tspl(t, { plan: 4 })
const server = await startRedirectingServer()
const { statusCode, headers, body: bodyStream, context: { history } } = await request(t, server, undefined, `http://${server}/200?key=value`, {
maxRedirections: 10
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 200)
t.ok(!headers.location)
t.deepStrictEqual(history.map(x => x.toString()), [`http://${server}/200?key=value`])
t.strictEqual(body, `GET /5 key=value :: host@${server} connection@keep-alive`)
await t.completed
})
test('should not follow redirection by default if not using RedirectAgent', async t => {
t = tspl(t, { plan: 3 })
const server = await startRedirectingServer()
const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}`)
const body = await bodyStream.text()
t.strictEqual(statusCode, 302)
t.strictEqual(headers.location, `http://${server}/302/1`)
t.strictEqual(body.length, 0)
await t.completed
})
test('should follow redirection after a HTTP 300', async t => {
t = tspl(t, { plan: 4 })
const server = await startRedirectingServer()
const { statusCode, headers, body: bodyStream, context: { history } } = await request(t, server, undefined, `http://${server}/300?key=value`, {
maxRedirections: 10
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 200)
t.ok(!headers.location)
t.deepStrictEqual(history.map(x => x.toString()), [
`http://${server}/300?key=value`,
`http://${server}/300/1?key=value`,
`http://${server}/300/2?key=value`,
`http://${server}/300/3?key=value`,
`http://${server}/300/4?key=value`,
`http://${server}/300/5?key=value`
])
t.strictEqual(body, `GET /5 key=value :: host@${server} connection@keep-alive`)
await t.completed
})
test('should follow redirection after a HTTP 300 default', async t => {
t = tspl(t, { plan: 4 })
const server = await startRedirectingServer()
const { statusCode, headers, body: bodyStream, context: { history } } = await request(t, server, undefined, `http://${server}/300?key=value`, { maxRedirections: 10 })
const body = await bodyStream.text()
t.strictEqual(statusCode, 200)
t.ok(!headers.location)
t.deepStrictEqual(history.map(x => x.toString()), [
`http://${server}/300?key=value`,
`http://${server}/300/1?key=value`,
`http://${server}/300/2?key=value`,
`http://${server}/300/3?key=value`,
`http://${server}/300/4?key=value`,
`http://${server}/300/5?key=value`
])
t.strictEqual(body, `GET /5 key=value :: host@${server} connection@keep-alive`)
await t.completed
})
test('should follow redirection after a HTTP 301', async t => {
t = tspl(t, { plan: 3 })
const server = await startRedirectingServer()
const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/301`, {
method: 'POST',
body: 'REQUEST',
maxRedirections: 10
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 200)
t.ok(!headers.location)
t.strictEqual(body, `GET /5 :: host@${server} connection@keep-alive`)
})
test('should follow redirection after a HTTP 302', async t => {
t = tspl(t, { plan: 3 })
const server = await startRedirectingServer()
const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/302`, {
method: 'PUT',
body: Buffer.from('REQUEST'),
maxRedirections: 10
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 200)
t.ok(!headers.location)
t.strictEqual(body, `PUT /5 :: host@${server} connection@keep-alive content-length@7 :: REQUEST`)
})
test('should remove request body headers when HTTP 302 changes POST to GET', async t => {
t = tspl(t, { plan: 3 })
const server = await startRedirectingServer()
const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/302`, {
method: 'POST',
body: 'REQUEST',
headers: {
'Content-Type': 'application/json',
'Content-Language': 'en',
'X-Test': 'ok'
},
maxRedirections: 10
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 200)
t.ok(!headers.location)
t.strictEqual(body, `GET /5 :: host@${server} connection@keep-alive x-test@ok`)
})
test('should follow redirection after a HTTP 303 changing method to GET', async t => {
t = tspl(t, { plan: 3 })
const server = await startRedirectingServer()
const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/303`, {
method: 'PATCH',
body: 'REQUEST',
maxRedirections: 10
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 200)
t.ok(!headers.location)
t.strictEqual(body, `GET /5 :: host@${server} connection@keep-alive`)
await t.completed
})
test('should remove Host and request body related headers when following HTTP 303 (array)', async t => {
t = tspl(t, { plan: 3 })
const server = await startRedirectingServer()
const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/303`, {
method: 'PATCH',
headers: [
'Content-Encoding',
'gzip',
'X-Foo1',
'1',
'X-Foo2',
'2',
'Content-Type',
'application/json',
'X-Foo3',
'3',
'Host',
'localhost',
'X-Bar',
'4'
],
maxRedirections: 10
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 200)
t.ok(!headers.location)
t.strictEqual(body, `GET /5 :: host@${server} connection@keep-alive x-foo1@1 x-foo2@2 x-foo3@3 x-bar@4`)
await t.completed
})
test('should remove Host and request body related headers when following HTTP 303 (object)', async t => {
t = tspl(t, { plan: 3 })
const server = await startRedirectingServer()
const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/303`, {
method: 'PATCH',
headers: {
'Content-Encoding': 'gzip',
'X-Foo1': '1',
'X-Foo2': '2',
'Content-Type': 'application/json',
'X-Foo3': '3',
Host: 'localhost',
'X-Bar': '4'
},
maxRedirections: 10
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 200)
t.ok(!headers.location)
t.strictEqual(body, `GET /5 :: host@${server} connection@keep-alive x-foo1@1 x-foo2@2 x-foo3@3 x-bar@4`)
await t.completed
})
test('should remove Host and request body related headers when following HTTP 303 (Global Headers)', async t => {
t = tspl(t, { plan: 3 })
const server = await startRedirectingServer()
const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/303`, {
method: 'PATCH',
// eslint-disable-next-line no-restricted-globals
headers: new Headers({
'Content-Encoding': 'gzip',
'X-Foo1': '1',
'X-Foo2': '2',
'Content-Type': 'application/json',
'X-Foo3': '3',
Host: 'localhost',
'X-Bar': '4'
}),
maxRedirections: 10
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 200)
t.ok(!headers.location)
t.strictEqual(body, `GET /5 :: host@${server} connection@keep-alive x-bar@4 x-foo1@1 x-foo2@2 x-foo3@3`)
await t.completed
})
test('should remove Host and request body related headers when following HTTP 303 (Undici Headers)', async t => {
t = tspl(t, { plan: 3 })
const server = await startRedirectingServer()
const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/303`, {
method: 'PATCH',
headers: new UndiciHeaders({
'Content-Encoding': 'gzip',
'X-Foo1': '1',
'X-Foo2': '2',
'Content-Type': 'application/json',
'X-Foo3': '3',
Host: 'localhost',
'X-Bar': '4'
}),
maxRedirections: 10
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 200)
t.ok(!headers.location)
t.strictEqual(body, `GET /5 :: host@${server} connection@keep-alive x-bar@4 x-foo1@1 x-foo2@2 x-foo3@3`)
await t.completed
})
test('should remove Host and request body related headers when following HTTP 303 (Maps)', async t => {
t = tspl(t, { plan: 3 })
const server = await startRedirectingServer()
const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/303`, {
method: 'PATCH',
headers: new Map([
['Content-Encoding', 'gzip'],
['X-Foo1', '1'],
['X-Foo2', '2'],
['Content-Type', 'application/json'],
['X-Foo3', '3'],
['Host', 'localhost'],
['X-Bar', '4']
]),
maxRedirections: 10
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 200)
t.ok(!headers.location)
t.strictEqual(body, `GET /5 :: host@${server} connection@keep-alive x-foo1@1 x-foo2@2 x-foo3@3 x-bar@4`)
await t.completed
})
test('should follow redirection after a HTTP 307', async t => {
t = tspl(t, { plan: 3 })
const server = await startRedirectingServer()
const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/307`, {
method: 'DELETE',
maxRedirections: 10
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 200)
t.ok(!headers.location)
t.strictEqual(body, `DELETE /5 :: host@${server} connection@keep-alive`)
await t.completed
})
test('should follow redirection after a HTTP 308', async t => {
t = tspl(t, { plan: 3 })
const server = await startRedirectingServer()
const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/308`, {
method: 'OPTIONS',
maxRedirections: 10
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 200)
t.ok(!headers.location)
t.strictEqual(body, `OPTIONS /5 :: host@${server} connection@keep-alive`)
await t.completed
})
test('should ignore HTTP 3xx response bodies', async t => {
t = tspl(t, { plan: 4 })
const server = await startRedirectingWithBodyServer()
const { statusCode, headers, body: bodyStream, context: { history } } = await request(t, server, undefined, `http://${server}/`, {
maxRedirections: 10
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 200)
t.ok(!headers.location)
t.deepStrictEqual(history.map(x => x.toString()), [`http://${server}/`, `http://${server}/end`])
t.strictEqual(body, 'FINAL')
await t.completed
})
test('should ignore query after redirection', async t => {
t = tspl(t, { plan: 3 })
const server = await startRedirectingWithQueryParams()
const { statusCode, headers, context: { history } } = await request(t, server, undefined, `http://${server}/`, {
maxRedirections: 10,
query: { param1: 'first' }
})
t.strictEqual(statusCode, 200)
t.ok(!headers.location)
t.deepStrictEqual(history.map(x => x.toString()), [`http://${server}/`, `http://${server}/?param2=second`])
await t.completed
})
test('should follow a redirect chain up to the allowed number of times', async t => {
t = tspl(t, { plan: 4 })
const server = await startRedirectingServer()
const { statusCode, headers, body: bodyStream, context: { history } } = await request(t, server, undefined, `http://${server}/300`, {
maxRedirections: 2
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 300)
t.strictEqual(headers.location, `http://${server}/300/3`)
t.deepStrictEqual(history.map(x => x.toString()), [`http://${server}/300`, `http://${server}/300/1`, `http://${server}/300/2`])
t.strictEqual(body.length, 0)
await t.completed
})
test('should throw when max redirections is reached and throwOnMaxRedirect is enabled', async t => {
t = tspl(t, { plan: 2 })
const server = await startRedirectingServer()
try {
await request(t, server, undefined, `http://${server}/300`, {
maxRedirections: 2,
throwOnMaxRedirect: true
})
} catch (error) {
if (error.message.startsWith('max redirects')) {
t.ok(true, 'Max redirects handled correctly')
} else {
t.fail(`Unexpected error: ${error.message}`)
}
t.strictEqual(error.code, 'UND_ERR_REDIRECT_MAX')
}
await t.completed
})
test('when a Location response header is NOT present', async t => {
t = tspl(t, { plan: 6 * 3 })
const redirectCodes = [300, 301, 302, 303, 307, 308]
const server = await startRedirectingWithoutLocationServer()
for (const code of redirectCodes) {
const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/${code}`, {
maxRedirections: 10
})
const body = await bodyStream.text()
t.strictEqual(statusCode, code)
t.ok(!headers.location)
t.strictEqual(body.length, 0)
}
await t.completed
})
test('should not allow invalid maxRedirections arguments', async t => {
t = tspl(t, { plan: 1 })
try {
await request(t, 'localhost', undefined, 'http://localhost', {
method: 'GET',
maxRedirections: 'INVALID'
})
t.fail('Did not throw')
} catch (err) {
t.strictEqual(err.message, 'maxRedirections must be a positive number')
}
await t.completed
})
test('should not allow invalid maxRedirections arguments default', async t => {
t = tspl(t, { plan: 1 })
try {
await request(t, 'localhost', undefined, 'http://localhost', {
method: 'GET',
maxRedirections: 'INVALID'
})
t.fail('Did not throw')
} catch (err) {
t.strictEqual(err.message, 'maxRedirections must be a positive number')
}
await t.completed
})
test('should not follow redirects when using ReadableStream request bodies', async t => {
t = tspl(t, { plan: 3 })
const server = await startRedirectingServer()
const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/301`, {
method: 'PUT',
body: createReadableStream('REQUEST'),
maxRedirections: 10
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 301)
t.strictEqual(headers.location, `http://${server}/301/2`)
t.strictEqual(body.length, 0)
await t.completed
})
test('should stop following redirects once async iterable request bodies are disturbed', async t => {
t = tspl(t, { plan: 3 })
const server = await startRedirectingServer()
const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/301`, {
method: 'PUT',
body: (async function * () {
yield 'REQUEST'
})(),
maxRedirections: 10
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 301)
t.strictEqual(headers.location, `http://${server}/301/2`)
t.strictEqual(body.length, 0)
await t.completed
})
test('should not follow redirects when using Readable request bodies', async t => {
t = tspl(t, { plan: 3 })
const server = await startRedirectingServer()
const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/301`, {
method: 'PUT',
body: createReadable('REQUEST'),
maxRedirections: 10
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 301)
t.strictEqual(headers.location, `http://${server}/301/1`)
t.strictEqual(body.length, 0)
await t.completed
})
test('should follow redirects when using Readable request bodies for POST 301', async t => {
t = tspl(t, { plan: 1 })
const server = await startRedirectingServer()
const { statusCode, body: bodyStream } = await request(t, server, undefined, `http://${server}/301`, {
method: 'POST',
body: createReadable('REQUEST'),
maxRedirections: 10
})
await bodyStream.text()
t.strictEqual(statusCode, 200)
await t.completed
})
}
test('should follow redirections when going cross origin', async t => {
t = tspl(t, { plan: 4 })
const [server1, server2, server3] = await startRedirectingChainServers()
const { statusCode, headers, body: bodyStream, context: { history } } = await undici.request(`http://${server1}`, {
method: 'POST',
dispatcher: new undici.Agent({}).compose(redirect({ maxRedirections: 10 }))
})
const body = await bodyStream.text()
t.strictEqual(statusCode, 200)
t.ok(!headers.location)
t.deepStrictEqual(history.map(x => x.toString()), [
`http://${server1}/`,
`http://${server2}/`,
`http://${server3}/`,
`http://${server2}/end`,
`http://${server3}/end`,
`http://${server1}/end`
])
t.strictEqual(body, 'GET')
await t.completed
})
test('should handle errors (callback)', async t => {
t = tspl(t, { plan: 1 })
undici.request(
'http://localhost:0',
{
dispatcher: new undici.Agent({}).compose(redirect({ maxRedirections: 10 }))
},
error => {
t.match(error.code, /EADDRNOTAVAIL|ECONNREFUSED/)
}
)
await t.completed
})
test('should handle errors (promise)', async t => {
t = tspl(t, { plan: 1 })
try {
await undici.request('http://localhost:0', { dispatcher: new undici.Agent({}).compose(redirect({ maxRedirections: 10 })) })
t.fail('Did not throw')
} catch (error) {
t.match(error.code, /EADDRNOTAVAIL|ECONNREFUSED/)
}
await t.completed
})
test('removes authorization header on third party origin', async t => {
t = tspl(t, { plan: 1 })
const [server1] = await startRedirectingWithAuthorization('secret')
const { body: bodyStream } = await undici.request(`http://${server1}`, {
dispatcher: new undici.Agent({}).compose(redirect({ maxRedirections: 10 })),
headers: {
authorization: 'secret'
}
})
const body = await bodyStream.text()
t.strictEqual(body, '')
await t.completed
})
test('removes cookie header on third party origin', async t => {
t = tspl(t, { plan: 1 })
const [server1] = await startRedirectingWithCookie('a=b')
const { body: bodyStream } = await undici.request(`http://${server1}`, {
dispatcher: new undici.Agent({}).compose(redirect({ maxRedirections: 10 })),
headers: {
cookie: 'a=b'
}
})
const body = await bodyStream.text()
t.strictEqual(body, '')
await t.completed
})