'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { createServer } = require('node:http')
const { test, after, describe } = require('node:test')
const { request, errors } = require('..')
test('no-slash/one-slash pathname should be included in req.path', async (t) => {
t = tspl(t, { plan: 24 })
const pathServer = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.fail('it shouldn\'t be called')
res.statusCode = 200
res.end('hello')
})
const requestedServer = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual(`/localhost:${pathServer.address().port}`, req.url)
t.strictEqual('GET', req.method)
t.strictEqual(`localhost:${requestedServer.address().port}`, req.headers.host)
res.statusCode = 200
res.end('hello')
})
after(() => {
requestedServer.close()
pathServer.close()
})
await Promise.all([
requestedServer.listen(0),
pathServer.listen(0)
])
const noSlashPathname = await request({
method: 'GET',
origin: `http://localhost:${requestedServer.address().port}`,
pathname: `localhost:${pathServer.address().port}`
})
t.strictEqual(noSlashPathname.statusCode, 200)
await noSlashPathname.body.dump()
const noSlashPath = await request({
method: 'GET',
origin: `http://localhost:${requestedServer.address().port}`,
path: `localhost:${pathServer.address().port}`
})
t.strictEqual(noSlashPath.statusCode, 200)
await noSlashPath.body.dump()
const noSlashPath2Arg = await request(
`http://localhost:${requestedServer.address().port}`,
{ path: `localhost:${pathServer.address().port}` }
)
t.strictEqual(noSlashPath2Arg.statusCode, 200)
await noSlashPath2Arg.body.dump()
const oneSlashPathname = await request({
method: 'GET',
origin: `http://localhost:${requestedServer.address().port}`,
pathname: `/localhost:${pathServer.address().port}`
})
t.strictEqual(oneSlashPathname.statusCode, 200)
await oneSlashPathname.body.dump()
const oneSlashPath = await request({
method: 'GET',
origin: `http://localhost:${requestedServer.address().port}`,
path: `/localhost:${pathServer.address().port}`
})
t.strictEqual(oneSlashPath.statusCode, 200)
await oneSlashPath.body.dump()
const oneSlashPath2Arg = await request(
`http://localhost:${requestedServer.address().port}`,
{ path: `/localhost:${pathServer.address().port}` }
)
t.strictEqual(oneSlashPath2Arg.statusCode, 200)
await oneSlashPath2Arg.body.dump()
t.end()
})
test('protocol-relative URL as pathname should be included in req.path', async (t) => {
t = tspl(t, { plan: 12 })
const pathServer = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.fail('it shouldn\'t be called')
res.statusCode = 200
res.end('hello')
})
const requestedServer = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual(`//localhost:${pathServer.address().port}`, req.url)
t.strictEqual('GET', req.method)
t.strictEqual(`localhost:${requestedServer.address().port}`, req.headers.host)
res.statusCode = 200
res.end('hello')
})
after(() => {
requestedServer.close()
pathServer.close()
})
await Promise.all([
requestedServer.listen(0),
pathServer.listen(0)
])
const noSlashPathname = await request({
method: 'GET',
origin: `http://localhost:${requestedServer.address().port}`,
pathname: `//localhost:${pathServer.address().port}`
})
t.strictEqual(noSlashPathname.statusCode, 200)
await noSlashPathname.body.dump()
const noSlashPath = await request({
method: 'GET',
origin: `http://localhost:${requestedServer.address().port}`,
path: `//localhost:${pathServer.address().port}`
})
t.strictEqual(noSlashPath.statusCode, 200)
await noSlashPath.body.dump()
const noSlashPath2Arg = await request(
`http://localhost:${requestedServer.address().port}`,
{ path: `//localhost:${pathServer.address().port}` }
)
t.strictEqual(noSlashPath2Arg.statusCode, 200)
await noSlashPath2Arg.body.dump()
t.end()
})
test('Absolute URL as pathname should be included in req.path', async (t) => {
t = tspl(t, { plan: 12 })
const pathServer = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.fail('it shouldn\'t be called')
res.statusCode = 200
res.end('hello')
})
const requestedServer = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual(`/http://localhost:${pathServer.address().port}`, req.url)
t.strictEqual('GET', req.method)
t.strictEqual(`localhost:${requestedServer.address().port}`, req.headers.host)
res.statusCode = 200
res.end('hello')
})
after(() => {
requestedServer.close()
pathServer.close()
})
await Promise.all([
requestedServer.listen(0),
pathServer.listen(0)
])
const noSlashPathname = await request({
method: 'GET',
origin: `http://localhost:${requestedServer.address().port}`,
pathname: `http://localhost:${pathServer.address().port}`
})
t.strictEqual(noSlashPathname.statusCode, 200)
await noSlashPathname.body.dump()
const noSlashPath = await request({
method: 'GET',
origin: `http://localhost:${requestedServer.address().port}`,
path: `http://localhost:${pathServer.address().port}`
})
t.strictEqual(noSlashPath.statusCode, 200)
await noSlashPath.body.dump()
const noSlashPath2Arg = await request(
`http://localhost:${requestedServer.address().port}`,
{ path: `http://localhost:${pathServer.address().port}` }
)
t.strictEqual(noSlashPath2Arg.statusCode, 200)
await noSlashPath2Arg.body.dump()
t.end()
})
describe('DispatchOptions#expectContinue', () => {
test('Should throw if invalid expectContinue option', async t => {
t = tspl(t, { plan: 1 })
await t.rejects(request({
method: 'GET',
origin: 'http://somehost.xyz',
expectContinue: 0
}), /invalid expectContinue/)
await t.completed
})
})
describe('DispatchOptions#maxRedirections', () => {
test('Should throw if maxRedirections option is used', async t => {
t = tspl(t, { plan: 2 })
await t.rejects(request({
method: 'GET',
origin: 'http://somehost.xyz',
maxRedirections: 5
}), /maxRedirections is not supported, use the redirect interceptor/)
await t.rejects(request({
method: 'GET',
origin: 'http://somehost.xyz',
maxRedirections: 1
}), /maxRedirections is not supported, use the redirect interceptor/)
await t.completed
})
test('Should allow maxRedirections: 0 for internal use', async t => {
t = tspl(t, { plan: 2 })
const server = createServer((req, res) => {
t.ok('request received')
res.end('hello world')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await new Promise((resolve) => server.listen(0, resolve))
const res = await request({
method: 'GET',
origin: `http://localhost:${server.address().port}`,
maxRedirections: 0
})
const body = await res.body.text()
t.strictEqual(body, 'hello world')
})
})
describe('DispatchOptions#reset', () => {
test('Should throw if invalid reset option', async t => {
t = tspl(t, { plan: 1 })
await t.rejects(request({
method: 'GET',
origin: 'http://somehost.xyz',
reset: 0
}), /invalid reset/)
await t.completed
})
test('Should include "connection:close" if reset true', async t => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual('GET', req.method)
t.strictEqual(`localhost:${server.address().port}`, req.headers.host)
t.strictEqual(req.headers.connection, 'close')
res.statusCode = 200
res.end('hello')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await new Promise((resolve, reject) => {
server.listen(0, (err) => {
if (err != null) reject(err)
else resolve()
})
})
const { body } = await request({
method: 'GET',
origin: `http://localhost:${server.address().port}`,
reset: true
})
await body.dump()
})
test('Should include "connection:keep-alive" if reset false', async t => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual('GET', req.method)
t.strictEqual(`localhost:${server.address().port}`, req.headers.host)
t.strictEqual(req.headers.connection, 'keep-alive')
res.statusCode = 200
res.end('hello')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await new Promise((resolve, reject) => {
server.listen(0, (err) => {
if (err != null) reject(err)
else resolve()
})
})
const { body } = await request({
method: 'GET',
origin: `http://localhost:${server.address().port}`,
reset: false
})
await body.dump()
})
test('Should react to manual set of "connection:close" header', async t => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual('GET', req.method)
t.strictEqual(`localhost:${server.address().port}`, req.headers.host)
t.strictEqual(req.headers.connection, 'close')
res.statusCode = 200
res.end('hello')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await new Promise((resolve, reject) => {
server.listen(0, (err) => {
if (err != null) reject(err)
else resolve()
})
})
const { body } = await request({
method: 'GET',
origin: `http://localhost:${server.address().port}`,
headers: {
connection: 'close'
}
})
await body.dump()
})
})
describe('Should include headers from iterable objects', scope => {
test('Should include headers built with Headers global object', { skip: !globalThis.Headers }, async t => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual('GET', req.method)
t.strictEqual(`localhost:${server.address().port}`, req.headers.host)
t.strictEqual(req.headers.hello, 'world')
res.statusCode = 200
res.end('hello')
})
const headers = new globalThis.Headers()
headers.set('hello', 'world')
after(() => {
server.closeAllConnections?.()
server.close()
})
await new Promise((resolve, reject) => {
server.listen(0, (err) => {
if (err != null) reject(err)
else resolve()
})
})
const { body } = await request({
method: 'GET',
origin: `http://localhost:${server.address().port}`,
reset: true,
headers
})
await body.dump()
})
test('Should include headers built with Map', async t => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual('GET', req.method)
t.strictEqual(`localhost:${server.address().port}`, req.headers.host)
t.strictEqual(req.headers.hello, 'world')
res.statusCode = 200
res.end('hello')
})
const headers = new Map()
headers.set('hello', 'world')
after(() => {
server.closeAllConnections?.()
server.close()
})
await new Promise((resolve, reject) => {
server.listen(0, (err) => {
if (err != null) reject(err)
else resolve()
})
})
const { body } = await request({
method: 'GET',
origin: `http://localhost:${server.address().port}`,
reset: true,
headers
})
await body.dump()
})
test('Should include headers built with custom iterable object', async t => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual('GET', req.method)
t.strictEqual(`localhost:${server.address().port}`, req.headers.host)
t.strictEqual(req.headers.hello, 'world')
res.statusCode = 200
res.end('hello')
})
const headers = {
* [Symbol.iterator] () {
yield ['hello', 'world']
}
}
after(() => {
server.closeAllConnections?.()
server.close()
})
await new Promise((resolve, reject) => {
server.listen(0, (err) => {
if (err != null) reject(err)
else resolve()
})
})
const { body } = await request({
method: 'GET',
origin: `http://localhost:${server.address().port}`,
reset: true,
headers
})
await body.dump()
})
test('Should include headers from plain objects with polluted Object.prototype[Symbol.iterator]', async t => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual('GET', req.method)
t.strictEqual(`localhost:${server.address().port}`, req.headers.host)
t.strictEqual(req.headers.hello, 'world')
res.statusCode = 200
res.end('hello')
})
const headers = {
hello: 'world'
}
after(() => {
server.closeAllConnections?.()
server.close()
})
const originalIterator = Object.prototype[Symbol.iterator]
// eslint-disable-next-line no-extend-native
Object.prototype[Symbol.iterator] = function * () {}
try {
await new Promise((resolve, reject) => {
server.listen(0, (err) => {
if (err != null) reject(err)
else resolve()
})
})
const { body } = await request({
method: 'GET',
origin: `http://localhost:${server.address().port}`,
reset: true,
headers
})
await body.dump()
} finally {
if (originalIterator === undefined) {
delete Object.prototype[Symbol.iterator]
} else {
// eslint-disable-next-line no-extend-native
Object.prototype[Symbol.iterator] = originalIterator
}
}
})
test('Should throw error if headers iterable object does not yield key-value pairs', async t => {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('hello')
})
const headers = {
* [Symbol.iterator] () {
yield 'Bad formatted header'
}
}
after(() => {
server.closeAllConnections?.()
server.close()
})
await new Promise((resolve, reject) => {
server.listen(0, (err) => {
if (err != null) reject(err)
else resolve()
})
})
await request({
method: 'GET',
origin: `http://localhost:${server.address().port}`,
reset: true,
headers
}).catch((err) => {
t.ok(err instanceof errors.InvalidArgumentError)
t.strictEqual(err.message, 'headers must be in key-value pair format')
})
})
})
test('request should include statusText in response', async t => {
t = tspl(t, { plan: 2 })
const server = createServer((req, res) => {
res.writeHead(200, 'Custom Status Text', { 'content-type': 'text/plain' })
res.end('hello')
})
after(() => {
server.closeAllConnections?.()
server.close()
})
await new Promise((resolve) => server.listen(0, resolve))
const { statusText, body } = await request({
method: 'GET',
origin: `http://localhost:${server.address().port}`,
path: '/'
})
t.strictEqual(statusText, 'Custom Status Text')
await body.dump()
t.ok('request completed')
})
describe('connection header per RFC 7230', () => {
test('should allow close', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer((req, res) => {
res.end('ok')
})
after(() => server.close())
await new Promise((resolve) => server.listen(0, resolve))
const { statusCode, body } = await request({
method: 'GET',
origin: `http://localhost:${server.address().port}`,
headers: { connection: 'close' }
})
await body.dump()
t.strictEqual(statusCode, 200)
})
test('should allow keep-alive', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer((req, res) => {
res.end('ok')
})
after(() => server.close())
await new Promise((resolve) => server.listen(0, resolve))
const { statusCode, body } = await request({
method: 'GET',
origin: `http://localhost:${server.address().port}`,
headers: { connection: 'keep-alive' }
})
await body.dump()
t.strictEqual(statusCode, 200)
})
test('should allow custom header name as connection option', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer((req, res) => {
res.end('ok')
})
after(() => server.close())
await new Promise((resolve) => server.listen(0, resolve))
const { statusCode, body } = await request({
method: 'GET',
origin: `http://localhost:${server.address().port}`,
headers: {
'x-custom-header': 'value',
connection: 'x-custom-header'
}
})
await body.dump()
t.strictEqual(statusCode, 200)
})
test('should allow comma-separated list of connection options', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer((req, res) => {
res.end('ok')
})
after(() => server.close())
await new Promise((resolve) => server.listen(0, resolve))
const { statusCode, body } = await request({
method: 'GET',
origin: `http://localhost:${server.address().port}`,
headers: {
'x-custom-header': 'value',
connection: 'close, x-custom-header'
}
})
await body.dump()
t.strictEqual(statusCode, 200)
})
test('should reject invalid tokens in connection header', async (t) => {
t = tspl(t, { plan: 2 })
await request({
method: 'GET',
origin: 'http://localhost:1234',
headers: { connection: 'invalid header with spaces' }
}).catch((err) => {
t.ok(err instanceof errors.InvalidArgumentError)
t.strictEqual(err.message, 'invalid connection header')
})
})
})