'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { readFileSync, createReadStream } = require('node:fs')
const { createServer } = require('node:http')
const { createServer: createNetServer } = require('node:net')
const { Readable, PassThrough } = require('node:stream')
const { test, after } = require('node:test')
const { Client, errors } = require('..')
const { kSocket } = require('../lib/core/symbols')
const { wrapWithAsyncIterable } = require('./utils/async-iterators')
const EE = require('node:events')
const { kUrl, kSize, kConnect, kBusy, kConnected, kRunning } = require('../lib/core/symbols')
const hasIPv6 = (() => {
const iFaces = require('node:os').networkInterfaces()
const re = process.platform === 'win32' ? /Loopback Pseudo-Interface/ : /lo/
return Object.keys(iFaces).some(
(name) => re.test(name) && iFaces[name].some(({ family }) => family === 'IPv6')
)
})()
test('basic get', async (t) => {
t = tspl(t, { plan: 24 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual('/', req.url)
t.strictEqual('GET', req.method)
t.strictEqual(`localhost:${server.address().port}`, req.headers.host)
t.strictEqual(undefined, req.headers.foo)
t.strictEqual('bar', req.headers.bar)
t.strictEqual(undefined, req.headers['content-length'])
res.setHeader('Content-Type', 'text/plain')
res.end('hello')
})
after(() => server.close())
const reqHeaders = {
foo: undefined,
bar: 'bar'
}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
keepAliveTimeout: 300e3
})
after(() => client.close())
t.strictEqual(client[kUrl].origin, `http://localhost:${server.address().port}`)
const signal = new EE()
client.request({
signal,
path: '/',
method: 'GET',
headers: reqHeaders
}, (err, data) => {
t.ifError(err)
const { statusCode, headers, body } = data
t.strictEqual(statusCode, 200)
t.strictEqual(signal.listenerCount('abort'), 1)
t.strictEqual(headers['content-type'], 'text/plain')
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('close', () => {
t.strictEqual(signal.listenerCount('abort'), 0)
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
t.strictEqual(signal.listenerCount('abort'), 1)
client.request({
path: '/',
method: 'GET',
headers: reqHeaders
}, (err, { statusCode, headers, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
t.strictEqual(headers['content-type'], 'text/plain')
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
})
await t.completed
})
test('passes socketPath to custom connect function', async (t) => {
t = tspl(t, { plan: 2 })
const connectError = new Error('custom connect error')
const socketPath = '/var/run/test.sock'
const client = new Client('http://localhost', {
socketPath,
connect (opts, cb) {
t.strictEqual(opts.socketPath, socketPath)
cb(connectError, null)
}
})
after(() => client.close())
client.request({
path: '/',
method: 'GET'
}, (err) => {
t.strictEqual(err, connectError)
})
await t.completed
})
test('passes lookup from connect options to built connector', async (t) => {
t = tspl(t, { plan: 5 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual(req.headers.host, `undici.test:${server.address().port}`)
res.end('lookup ok')
})
after(() => server.close())
server.listen(0, '127.0.0.1', () => {
const client = new Client(`http://undici.test:${server.address().port}`, {
connect: {
lookup (hostname, options, callback) {
t.strictEqual(hostname, 'undici.test')
if (options.all === true) {
callback(null, [{ address: '127.0.0.1', family: 4 }])
} else {
callback(null, '127.0.0.1', 4)
}
}
}
})
after(() => client.close())
client.request({
path: '/',
method: 'GET'
}, (err, { statusCode, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
const chunks = []
body.on('data', (chunk) => {
chunks.push(chunk)
})
body.on('end', () => {
t.strictEqual(Buffer.concat(chunks).toString(), 'lookup ok')
})
})
})
await t.completed
})
test('basic get with custom request.reset=true', async (t) => {
t = tspl(t, { plan: 26 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual('/', req.url)
t.strictEqual('GET', req.method)
t.strictEqual(`localhost:${server.address().port}`, req.headers.host)
t.strictEqual(req.headers.connection, 'close')
t.strictEqual(undefined, req.headers.foo)
t.strictEqual('bar', req.headers.bar)
t.strictEqual(undefined, req.headers['content-length'])
res.setHeader('Content-Type', 'text/plain')
res.end('hello')
})
after(() => server.close())
const reqHeaders = {
foo: undefined,
bar: 'bar'
}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {})
after(() => client.close())
t.strictEqual(client[kUrl].origin, `http://localhost:${server.address().port}`)
const signal = new EE()
client.request({
signal,
path: '/',
method: 'GET',
reset: true,
headers: reqHeaders
}, (err, data) => {
t.ifError(err)
const { statusCode, headers, body } = data
t.strictEqual(statusCode, 200)
t.strictEqual(signal.listenerCount('abort'), 1)
t.strictEqual(headers['content-type'], 'text/plain')
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('close', () => {
t.strictEqual(signal.listenerCount('abort'), 0)
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
t.strictEqual(signal.listenerCount('abort'), 1)
client.request({
path: '/',
reset: true,
method: 'GET',
headers: reqHeaders
}, (err, { statusCode, headers, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
t.strictEqual(headers['content-type'], 'text/plain')
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
})
await t.completed
})
test('basic get with query params', async (t) => {
t = tspl(t, { plan: 4 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
const searchParamsObject = buildParams(req.url)
t.deepStrictEqual(searchParamsObject, {
bool: 'true',
foo: '1',
bar: 'bar',
'%60~%3A%24%2C%2B%5B%5D%40%5E*()-': '%60~%3A%24%2C%2B%5B%5D%40%5E*()-',
multi: ['1', '2'],
nullVal: '',
undefinedVal: ''
})
res.statusCode = 200
res.end('hello')
})
after(() => server.close())
const query = {
bool: true,
foo: 1,
bar: 'bar',
nullVal: null,
undefinedVal: undefined,
'`~:$,+[]@^*()-': '`~:$,+[]@^*()-',
multi: [1, 2]
}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
keepAliveTimeout: 300e3
})
after(() => client.close())
const signal = new EE()
client.request({
signal,
path: '/',
method: 'GET',
query
}, (err, data) => {
t.ifError(err)
const { statusCode } = data
t.strictEqual(statusCode, 200)
})
t.strictEqual(signal.listenerCount('abort'), 1)
})
await t.completed
})
test('basic get with query params fails if url includes hashmark', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.fail()
})
after(() => server.close())
const query = {
foo: 1,
bar: 'bar',
multi: [1, 2]
}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
keepAliveTimeout: 300e3
})
after(() => client.close())
const signal = new EE()
client.request({
signal,
path: '/#',
method: 'GET',
query
}, (err, data) => {
t.strictEqual(err.message, 'Query params cannot be passed when url already contains "?" or "#".')
})
})
await t.completed
})
test('basic get with empty query params', async (t) => {
t = tspl(t, { plan: 4 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
const searchParamsObject = buildParams(req.url)
t.deepStrictEqual(searchParamsObject, {})
res.statusCode = 200
res.end('hello')
})
after(() => server.close())
const query = {}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
keepAliveTimeout: 300e3
})
after(() => client.close())
const signal = new EE()
client.request({
signal,
path: '/',
method: 'GET',
query
}, (err, data) => {
t.ifError(err)
const { statusCode } = data
t.strictEqual(statusCode, 200)
})
t.strictEqual(signal.listenerCount('abort'), 1)
})
await t.completed
})
test('basic get with query params partially in path', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.fail()
})
after(() => server.close())
const query = {
foo: 1
}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
keepAliveTimeout: 300e3
})
after(() => client.close())
const signal = new EE()
client.request({
signal,
path: '/?bar=2',
method: 'GET',
query
}, (err, data) => {
t.strictEqual(err.message, 'Query params cannot be passed when url already contains "?" or "#".')
})
})
await t.completed
})
test('using throwOnError should throw (request)', async (t) => {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.statusCode = 400
res.end('hello')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
keepAliveTimeout: 300e3
})
after(() => client.close())
const signal = new EE()
client.request({
signal,
path: '/',
method: 'GET',
throwOnError: true
}, (err) => {
t.strictEqual(err.message, 'invalid throwOnError')
t.strictEqual(err.code, 'UND_ERR_INVALID_ARG')
})
})
await t.completed
})
test('using throwOnError should throw (stream)', async (t) => {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.statusCode = 400
res.end('hello')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
keepAliveTimeout: 300e3
})
after(() => client.close())
client.stream({
path: '/',
method: 'GET',
throwOnError: true,
opaque: new PassThrough()
}, ({ opaque: pt }) => {
pt.on('data', () => {
t.fail()
})
return pt
}, err => {
t.strictEqual(err.message, 'invalid throwOnError')
t.strictEqual(err.code, 'UND_ERR_INVALID_ARG')
})
})
await t.completed
})
test('basic head', async (t) => {
t = tspl(t, { plan: 14 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual('/123', req.url)
t.strictEqual('HEAD', req.method)
t.strictEqual(`localhost:${server.address().port}`, req.headers.host)
res.setHeader('content-type', 'text/plain')
res.end('hello')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.request({ path: '/123', method: 'HEAD' }, (err, { statusCode, headers, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
t.strictEqual(headers['content-type'], 'text/plain')
body
.resume()
.on('end', () => {
t.ok(true, 'pass')
})
})
client.request({ path: '/123', method: 'HEAD' }, (err, { statusCode, headers, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
t.strictEqual(headers['content-type'], 'text/plain')
body
.resume()
.on('end', () => {
t.ok(true, 'pass')
})
})
})
await t.completed
})
test('basic head (IPv6)', { skip: !hasIPv6 }, async (t) => {
t = tspl(t, { plan: 10 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual('/123', req.url)
t.strictEqual('HEAD', req.method)
t.strictEqual(`[::1]:${server.address().port}`, req.headers.host)
res.setHeader('content-type', 'text/plain')
res.end('hello')
})
after(() => server.close())
server.listen(0, '::', () => {
const client = new Client(`http://[::1]:${server.address().port}`)
after(() => client.close())
client.request({ path: '/123', method: 'HEAD' }, (err, { statusCode, headers, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
t.strictEqual(headers['content-type'], 'text/plain')
body
.resume()
.on('end', () => {
t.ok(true, 'pass')
})
})
client.request({ path: '/123', method: 'HEAD' }, (err, { statusCode, headers, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
t.strictEqual(headers['content-type'], 'text/plain')
body
.resume()
.on('end', () => {
t.ok(true, 'pass')
})
})
})
await t.completed
})
test('get with host header', async (t) => {
t = tspl(t, { plan: 7 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual('/', req.url)
t.strictEqual('GET', req.method)
t.strictEqual('example.com', req.headers.host)
res.setHeader('content-type', 'text/plain')
res.end('hello from ' + req.headers.host)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.request({ path: '/', method: 'GET', headers: { host: 'example.com' } }, (err, { statusCode, headers, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
t.strictEqual(headers['content-type'], 'text/plain')
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.strictEqual('hello from example.com', Buffer.concat(bufs).toString('utf8'))
})
})
})
await t.completed
})
test('get with host header (IPv6)', { skip: !hasIPv6 }, async (t) => {
t = tspl(t, { plan: 7 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual('/', req.url)
t.strictEqual('GET', req.method)
t.strictEqual('[::1]', req.headers.host)
res.setHeader('content-type', 'text/plain')
res.end('hello from ' + req.headers.host)
})
after(() => server.close())
server.listen(0, '::', () => {
const client = new Client(`http://[::1]:${server.address().port}`)
after(() => client.close())
client.request({ path: '/', method: 'GET', headers: { host: '[::1]' } }, (err, { statusCode, headers, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
t.strictEqual(headers['content-type'], 'text/plain')
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.strictEqual('hello from [::1]', Buffer.concat(bufs).toString('utf8'))
})
})
})
await t.completed
})
test('head with host header', async (t) => {
t = tspl(t, { plan: 7 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual('/', req.url)
t.strictEqual('HEAD', req.method)
t.strictEqual('example.com', req.headers.host)
res.setHeader('content-type', 'text/plain')
res.end('hello from ' + req.headers.host)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.request({ path: '/', method: 'HEAD', headers: { host: 'example.com' } }, (err, { statusCode, headers, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
t.strictEqual(headers['content-type'], 'text/plain')
body
.resume()
.on('end', () => {
t.ok(true, 'pass')
})
})
})
await t.completed
})
function postServer (t, expected) {
return function (req, res) {
t.strictEqual(req.url, '/')
t.strictEqual(req.method, 'POST')
t.notStrictEqual(req.headers['content-length'], null)
req.setEncoding('utf8')
let data = ''
req.on('data', function (d) { data += d })
req.on('end', () => {
t.strictEqual(data, expected)
res.end('hello')
})
}
}
test('basic POST with string', async (t) => {
t = tspl(t, { plan: 7 })
const expected = readFileSync(__filename, 'utf8')
const server = createServer({ joinDuplicateHeaders: true }, postServer(t, expected))
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.request({ path: '/', method: 'POST', body: expected }, (err, data) => {
t.ifError(err)
t.strictEqual(data.statusCode, 200)
const bufs = []
data.body
.on('data', (buf) => {
bufs.push(buf)
})
.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
})
await t.completed
})
test('basic POST with empty string', async (t) => {
t = tspl(t, { plan: 7 })
const server = createServer({ joinDuplicateHeaders: true }, postServer(t, ''))
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.request({ path: '/', method: 'POST', body: '' }, (err, { statusCode, headers, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
})
await t.completed
})
test('basic POST with string and content-length', async (t) => {
t = tspl(t, { plan: 7 })
const expected = readFileSync(__filename, 'utf8')
const server = createServer({ joinDuplicateHeaders: true }, postServer(t, expected))
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.request({
path: '/',
method: 'POST',
headers: {
'content-length': Buffer.byteLength(expected)
},
body: expected
}, (err, { statusCode, headers, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
})
await t.completed
})
test('basic POST with Buffer', async (t) => {
t = tspl(t, { plan: 7 })
const expected = readFileSync(__filename)
const server = createServer({ joinDuplicateHeaders: true }, postServer(t, expected.toString()))
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.request({ path: '/', method: 'POST', body: expected }, (err, { statusCode, headers, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
})
await t.completed
})
test('basic POST with stream', async (t) => {
t = tspl(t, { plan: 7 })
const expected = readFileSync(__filename, 'utf8')
const server = createServer({ joinDuplicateHeaders: true }, postServer(t, expected))
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.request({
path: '/',
method: 'POST',
headers: {
'content-length': Buffer.byteLength(expected)
},
headersTimeout: 0,
body: createReadStream(__filename)
}, (err, { statusCode, headers, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
})
await t.completed
})
test('basic POST with paused stream', async (t) => {
t = tspl(t, { plan: 7 })
const expected = readFileSync(__filename, 'utf8')
const server = createServer({ joinDuplicateHeaders: true }, postServer(t, expected))
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
const stream = createReadStream(__filename)
stream.pause()
client.request({
path: '/',
method: 'POST',
headers: {
'content-length': Buffer.byteLength(expected)
},
headersTimeout: 0,
body: stream
}, (err, { statusCode, headers, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
})
await t.completed
})
test('basic POST with custom stream', async (t) => {
t = tspl(t, { plan: 4 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
req.resume().on('end', () => {
res.end('hello')
})
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
const body = new EE()
body.pipe = () => {}
client.request({
path: '/',
method: 'POST',
headersTimeout: 0,
body
}, (err, data) => {
t.ifError(err)
t.strictEqual(data.statusCode, 200)
const bufs = []
data.body.on('data', (buf) => {
bufs.push(buf)
})
data.body.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
t.deepStrictEqual(client[kBusy], true)
body.on('close', () => {
body.emit('end')
})
client.on('connect', () => {
setImmediate(() => {
body.emit('data', '')
while (!client[kSocket]._writableState.needDrain) {
body.emit('data', Buffer.alloc(4096))
}
client[kSocket].on('drain', () => {
body.emit('data', Buffer.alloc(4096))
body.emit('close')
})
})
})
})
await t.completed
})
test('basic POST with iterator', async (t) => {
t = tspl(t, { plan: 3 })
const expected = 'hello'
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
req.resume().on('end', () => {
res.end(expected)
})
})
after(() => server.close())
const iterable = {
[Symbol.iterator]: function * () {
for (let i = 0; i < expected.length - 1; i++) {
yield expected[i]
}
return expected[expected.length - 1]
}
}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.request({
path: '/',
method: 'POST',
requestTimeout: 0,
body: iterable
}, (err, { statusCode, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
})
await t.completed
})
test('basic POST with iterator with invalid data', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, () => {})
after(() => server.close())
const iterable = {
[Symbol.iterator]: function * () {
yield 0
}
}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.request({
path: '/',
method: 'POST',
requestTimeout: 0,
body: iterable
}, err => {
t.ok(err instanceof TypeError)
})
})
await t.completed
})
test('basic POST with async iterator', async (t) => {
t = tspl(t, { plan: 7 })
const expected = readFileSync(__filename, 'utf8')
const server = createServer({ joinDuplicateHeaders: true }, postServer(t, expected))
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.request({
path: '/',
method: 'POST',
headers: {
'content-length': Buffer.byteLength(expected)
},
headersTimeout: 0,
body: wrapWithAsyncIterable(createReadStream(__filename))
}, (err, { statusCode, headers, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
})
await t.completed
})
test('basic POST with transfer encoding: chunked', async (t) => {
t = tspl(t, { plan: 8 })
let body
const server = createServer({ joinDuplicateHeaders: true }, function (req, res) {
t.strictEqual(req.url, '/')
t.strictEqual(req.method, 'POST')
t.strictEqual(req.headers['content-length'], undefined)
t.strictEqual(req.headers['transfer-encoding'], 'chunked')
body.push(null)
req.setEncoding('utf8')
let data = ''
req.on('data', function (d) { data += d })
req.on('end', () => {
t.strictEqual(data, 'asd')
res.end('hello')
})
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
body = new Readable({
read () { }
})
body.push('asd')
client.request({
path: '/',
method: 'POST',
// no content-length header
body
}, (err, { statusCode, headers, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
})
await t.completed
})
test('basic POST with empty stream', async (t) => {
t = tspl(t, { plan: 4 })
const server = createServer({ joinDuplicateHeaders: true }, function (req, res) {
t.deepStrictEqual(req.headers['content-length'], '0')
req.pipe(res)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
const body = new Readable({
autoDestroy: false,
read () {
},
destroy (err, callback) {
callback(!this._readableState.endEmitted ? new Error('asd') : err)
}
}).on('end', () => {
process.nextTick(() => {
t.strictEqual(body.destroyed, true)
})
})
body.push(null)
client.request({
path: '/',
method: 'POST',
body
}, (err, { statusCode, headers, body }) => {
t.ifError(err)
body
.on('data', () => {
t.fail()
})
.on('end', () => {
t.ok(true, 'pass')
})
})
})
await t.completed
})
test('10 times GET', async (t) => {
const num = 10
t = tspl(t, { plan: 3 * num })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(req.url)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
for (let i = 0; i < num; i++) {
makeRequest(i)
}
function makeRequest (i) {
client.request({ path: '/' + i, method: 'GET' }, (err, { statusCode, headers, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.strictEqual('/' + i, Buffer.concat(bufs).toString('utf8'))
})
})
}
})
await t.completed
})
test('10 times HEAD', async (t) => {
const num = 10
t = tspl(t, { plan: num * 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end(req.url)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
for (let i = 0; i < num; i++) {
makeRequest(i)
}
function makeRequest (i) {
client.request({ path: '/' + i, method: 'HEAD' }, (err, { statusCode, headers, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
body
.resume()
.on('end', () => {
t.ok(true, 'pass')
})
})
}
})
await t.completed
})
test('Set-Cookie', async (t) => {
t = tspl(t, { plan: 4 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.setHeader('content-type', 'text/plain')
res.setHeader('Set-Cookie', ['a cookie', 'another cookie', 'more cookies'])
res.end('hello')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
t.deepStrictEqual(headers['set-cookie'], ['a cookie', 'another cookie', 'more cookies'])
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.strictEqual('hello', Buffer.concat(bufs).toString('utf8'))
})
})
})
await t.completed
})
test('ignore request header mutations', async (t) => {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual(req.headers.test, 'test')
res.end()
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
const headers = { test: 'test' }
client.request({
path: '/',
method: 'GET',
headers
}, (err, { body }) => {
t.ifError(err)
body.resume()
})
headers.test = 'asd'
})
await t.completed
})
test('url-like url', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end()
})
after(() => server.close())
server.listen(0, () => {
const client = new Client({
hostname: 'localhost',
port: server.address().port,
protocol: 'http:'
})
after(() => client.close())
client.request({ path: '/', method: 'GET' }, (err, data) => {
t.ifError(err)
data.body.resume()
})
})
await t.completed
})
test('an absolute url as path', async (t) => {
t = tspl(t, { plan: 2 })
const path = 'http://example.com'
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual(req.url, path)
res.end()
})
after(() => server.close())
server.listen(0, () => {
const client = new Client({
hostname: 'localhost',
port: server.address().port,
protocol: 'http:'
})
after(() => client.close())
client.request({ path, method: 'GET' }, (err, data) => {
t.ifError(err)
data.body.resume()
})
})
await t.completed
})
test('multiple destroy callback', async (t) => {
t = tspl(t, { plan: 4 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end()
})
after(() => server.close())
server.listen(0, () => {
const client = new Client({
hostname: 'localhost',
port: server.address().port,
protocol: 'http:'
})
after(() => client.destroy())
client.request({ path: '/', method: 'GET' }, (err, data) => {
t.ifError(err)
data.body
.resume()
.on('error', (err) => {
t.ok(err instanceof Error)
})
client.destroy(new Error(), (err) => {
t.ifError(err)
})
client.destroy(new Error(), (err) => {
t.ifError(err)
})
})
})
await t.completed
})
// Regression tests for https://github.com/nodejs/undici/issues/5360: when a
// response body applies backpressure the llhttp parser is left paused; if the
// peer then FINs, onHttpSocketEnd -> parser.finish() used to assert !paused and
// crash the process. finish() must instead complete the paused parser. The body
// is only consumed *after* the FIN (attaching just 'end'/'error' does not switch
// the stream to flowing mode), so the parser is genuinely paused at FIN. The
// 64 KiB payload matches the default highWaterMark: large enough to pause, small
// enough to arrive in a single socket read.
function pausedAtFin (t, writeResponse, onBody) {
const payload = Buffer.alloc(64 * 1024, 0x61)
const server = createNetServer((socket) => {
socket.once('data', () => {
writeResponse(socket, payload)
socket.end() // FIN while the body is still unconsumed -> parser paused.
})
})
after(() => server.close())
server.listen(0, '127.0.0.1', () => {
const client = new Client(`http://127.0.0.1:${server.address().port}`)
after(() => client.close())
client.request({ path: '/', method: 'GET' }, (err, data) => {
t.ifError(err)
const chunks = []
data.body.on('end', () => onBody(t, payload, Buffer.concat(chunks), null))
data.body.on('error', (err) => onBody(t, payload, Buffer.concat(chunks), err))
// Defer consuming until after FIN has been handled while paused.
setTimeout(() => data.body.on('data', (chunk) => chunks.push(chunk)), 100)
})
})
}
const expectComplete = (t, payload, body, err) => {
t.ifError(err)
t.strictEqual(body.length, payload.length)
}
const expectError = (t, payload, body, err) => {
// A truncated body must surface as an error, never a clean completion.
t.ok(err instanceof Error, `expected an error, got ${err}`)
}
test('socket end completes Content-Length response paused by backpressure', async (t) => {
t = tspl(t, { plan: 3 })
pausedAtFin(t, (socket, payload) => {
socket.write(`HTTP/1.1 200 OK\r\nContent-Length: ${payload.length}\r\nConnection: close\r\n\r\n`)
socket.write(payload)
}, expectComplete)
await t.completed
})
test('socket end completes EOF-delimited response paused by backpressure', async (t) => {
t = tspl(t, { plan: 3 })
pausedAtFin(t, (socket, payload) => {
socket.write('HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n')
socket.write(payload)
}, expectComplete)
await t.completed
})
test('socket end errors on truncated Content-Length response paused by backpressure', async (t) => {
t = tspl(t, { plan: 2 })
pausedAtFin(t, (socket, payload) => {
// Declare twice the body we actually send, then FIN -> truncated.
socket.write(`HTTP/1.1 200 OK\r\nContent-Length: ${payload.length * 2}\r\nConnection: close\r\n\r\n`)
socket.write(payload)
}, expectError)
await t.completed
})
test('socket end errors on truncated chunked response paused by backpressure', async (t) => {
t = tspl(t, { plan: 2 })
pausedAtFin(t, (socket, payload) => {
socket.write('HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\nConnection: close\r\n\r\n')
socket.write(`${payload.length.toString(16)}\r\n`)
socket.write(payload) // no trailing CRLF / terminating 0-chunk -> truncated
}, expectError)
await t.completed
})
test('only one streaming req at a time', async (t) => {
t = tspl(t, { plan: 7 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
req.pipe(res)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 4
})
after(() => client.destroy())
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
t.ifError(err)
data.body.resume()
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
t.ifError(err)
data.body.resume()
})
client.request({
path: '/',
method: 'PUT',
idempotent: true,
body: new Readable({
read () {
setImmediate(() => {
t.strictEqual(client[kBusy], true)
this.push(null)
})
}
}).on('resume', () => {
t.strictEqual(client[kSize], 1)
})
}, (err, data) => {
t.ifError(err)
data.body
.resume()
.on('end', () => {
t.ok(true, 'pass')
})
})
t.strictEqual(client[kBusy], true)
})
})
await t.completed
})
test('only one async iterating req at a time', async (t) => {
t = tspl(t, { plan: 6 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
req.pipe(res)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 4
})
after(() => client.destroy())
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
t.ifError(err)
data.body.resume()
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
t.ifError(err)
data.body.resume()
})
const body = wrapWithAsyncIterable(new Readable({
read () {
setImmediate(() => {
t.strictEqual(client[kBusy], true)
this.push(null)
})
}
}))
client.request({
path: '/',
method: 'PUT',
idempotent: true,
body
}, (err, data) => {
t.ifError(err)
data.body
.resume()
.on('end', () => {
t.ok(true, 'pass')
})
})
t.strictEqual(client[kBusy], true)
})
})
await t.completed
})
test('300 requests succeed', async (t) => {
t = tspl(t, { plan: 300 * 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end('asd')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
for (let n = 0; n < 300; ++n) {
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
t.ifError(err)
data.body.on('data', (chunk) => {
t.strictEqual(chunk.toString(), 'asd')
}).on('end', () => {
t.ok(true, 'pass')
})
})
}
})
await t.completed
})
test('request args validation', async (t) => {
t = tspl(t, { plan: 2 })
const client = new Client('http://localhost:5000')
client.request(null, (err) => {
t.ok(err instanceof errors.InvalidArgumentError)
})
try {
client.request(null, 'asd')
} catch (err) {
t.ok(err instanceof errors.InvalidArgumentError)
}
await t.completed
})
test('request args validation promise', async (t) => {
t = tspl(t, { plan: 1 })
const client = new Client('http://localhost:5000')
client.request(null).catch((err) => {
t.ok(err instanceof errors.InvalidArgumentError)
})
await t.completed
})
test('increase pipelining', async (t) => {
t = tspl(t, { plan: 4 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
req.resume()
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.destroy())
client.request({
path: '/',
method: 'GET',
blocking: false
}, () => {
if (!client.destroyed) {
t.fail()
}
})
client.request({
path: '/',
method: 'GET',
blocking: false
}, () => {
if (!client.destroyed) {
t.fail()
}
})
t.strictEqual(client[kRunning], 0)
client.on('connect', () => {
t.strictEqual(client[kRunning], 0)
process.nextTick(() => {
t.strictEqual(client[kRunning], 1)
client.pipelining = 3
t.strictEqual(client[kRunning], 2)
})
})
})
await t.completed
})
test('destroy in push', async (t) => {
t = tspl(t, { plan: 4 })
let _res
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.write('asd')
_res = res
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.request({ path: '/', method: 'GET' }, (err, { body }) => {
t.ifError(err)
body.once('data', () => {
_res.write('asd')
body.on('data', (buf) => {
body.destroy()
_res.end()
}).on('error', (err) => {
t.ok(err)
})
})
})
client.request({ path: '/', method: 'GET' }, (err, { body }) => {
t.ifError(err)
let buf = ''
body.on('data', (chunk) => {
buf = chunk.toString()
_res.end()
}).on('end', () => {
t.strictEqual('asd', buf)
})
})
})
await t.completed
})
test('non recoverable socket error fails pending request', async (t) => {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end()
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.request({ path: '/', method: 'GET' }, (err, data) => {
t.strictEqual(err.message, 'kaboom')
})
client.request({ path: '/', method: 'GET' }, (err, data) => {
t.strictEqual(err.message, 'kaboom')
})
client.on('connect', () => {
client[kSocket].destroy(new Error('kaboom'))
})
})
await t.completed
})
test('POST empty with error', async (t) => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
req.pipe(res)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
const body = new Readable({
read () {
}
})
body.push(null)
client.on('connect', () => {
process.nextTick(() => {
body.emit('error', new Error('asd'))
})
})
client.request({ path: '/', method: 'POST', body }, (err, data) => {
t.strictEqual(err.message, 'asd')
})
})
await t.completed
})
test('busy', async (t) => {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
req.pipe(res)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 1
})
after(() => client.close())
client[kConnect](() => {
client.request({
path: '/',
method: 'GET'
}, (err) => {
t.ifError(err)
})
t.strictEqual(client[kBusy], true)
})
})
await t.completed
})
test('connected', async (t) => {
t = tspl(t, { plan: 7 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
// needed so that disconnect is emitted
res.setHeader('connection', 'close')
req.pipe(res)
})
after(() => server.close())
server.listen(0, () => {
const url = new URL(`http://localhost:${server.address().port}`)
const client = new Client(url, {
pipelining: 1
})
after(() => client.close())
client.on('connect', (origin, [self]) => {
t.strictEqual(origin, url)
t.strictEqual(client, self)
})
client.on('disconnect', (origin, [self]) => {
t.strictEqual(origin, url)
t.strictEqual(client, self)
})
t.strictEqual(client[kConnected], false)
client[kConnect](() => {
client.request({
path: '/',
method: 'GET'
}, (err) => {
t.ifError(err)
})
t.strictEqual(client[kConnected], true)
})
})
await t.completed
})
test('emit disconnect after destroy', async t => {
t = tspl(t, { plan: 4 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
req.pipe(res)
})
after(() => server.close())
server.listen(0, () => {
const url = new URL(`http://localhost:${server.address().port}`)
const client = new Client(url)
t.strictEqual(client[kConnected], false)
client[kConnect](() => {
t.strictEqual(client[kConnected], true)
let disconnected = false
client.on('disconnect', () => {
disconnected = true
t.ok(true, 'pass')
})
client.destroy(() => {
t.strictEqual(disconnected, true)
})
})
})
await t.completed
})
test('end response before request', async t => {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end()
})
after(() => server.close())
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`)
const readable = new Readable({
read () {
this.push('asd')
}
})
const { body } = await client.request({
method: 'GET',
path: '/',
body: readable
})
body
.on('error', () => {
t.fail()
})
.on('end', () => {
t.ok(true, 'pass')
})
.resume()
client.on('disconnect', (url, targets, err) => {
t.strictEqual(err.code, 'UND_ERR_INFO')
})
})
await t.completed
})
test('parser pause with no body timeout', async (t) => {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
let counter = 0
const t = setInterval(() => {
counter++
const payload = Buffer.alloc(counter * 4096).fill(0)
if (counter === 3) {
clearInterval(t)
res.end(payload)
} else {
res.write(payload)
}
}, 20)
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
bodyTimeout: 0
})
after(() => client.close())
client.request({ path: '/', method: 'GET' }, (err, { statusCode, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
body.resume()
})
})
await t.completed
})
test('TypedArray and DataView body', async (t) => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual(req.headers['content-length'], '8')
res.end()
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
bodyTimeout: 0
})
after(() => client.close())
const body = Uint8Array.from(Buffer.alloc(8))
client.request({ path: '/', method: 'POST', body }, (err, { statusCode, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
body.resume()
})
})
await t.completed
})
test('async iterator empty chunk continues', async (t) => {
t = tspl(t, { plan: 5 })
const serverChunks = ['hello', 'world']
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
let str = ''
let i = 0
req.on('data', (chunk) => {
const content = chunk.toString()
t.strictEqual(serverChunks[i++], content)
str += content
}).on('end', () => {
t.strictEqual(str, serverChunks.join(''))
res.end()
})
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
bodyTimeout: 0
})
after(() => client.close())
const body = (async function * () {
yield serverChunks[0]
yield ''
yield serverChunks[1]
})()
client.request({ path: '/', method: 'POST', body }, (err, { statusCode, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
body.resume()
})
})
await t.completed
})
test('async iterator error from server destroys early', async (t) => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
req.on('data', (chunk) => {
res.destroy()
})
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
bodyTimeout: 0
})
after(() => client.close())
let gotDestroyed
const body = (async function * () {
try {
const promise = new Promise(resolve => {
gotDestroyed = resolve
})
yield 'hello'
await promise
yield 'inner-value'
t.fail('should not get here, iterator should be destroyed')
} finally {
t.ok(true, 'pass')
}
})()
client.request({ path: '/', method: 'POST', body }, (err, { statusCode, body }) => {
t.ok(err)
t.strictEqual(statusCode, undefined)
gotDestroyed()
})
})
await t.completed
})
test('regular iterator error from server closes early', async (t) => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
req.on('data', () => {
process.nextTick(() => {
res.destroy()
})
})
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
bodyTimeout: 0
})
after(() => client.close())
let gotDestroyed = false
const body = (function * () {
try {
yield 'start'
while (!gotDestroyed) {
yield 'zzz'
// for eslint
gotDestroyed = gotDestroyed || false
}
yield 'zzz'
t.fail('should not get here, iterator should be destroyed')
yield 'zzz'
} finally {
t.ok(true, 'pass')
}
})()
client.request({ path: '/', method: 'POST', body }, (err, { statusCode, body }) => {
t.ok(err)
t.strictEqual(statusCode, undefined)
gotDestroyed = true
})
})
await t.completed
})
test('async iterator early return closes early', async (t) => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
req.on('data', () => {
res.writeHead(200)
res.end()
})
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
bodyTimeout: 0
})
after(() => client.close())
let gotDestroyed
const body = (async function * () {
try {
const promise = new Promise(resolve => {
gotDestroyed = resolve
})
yield 'hello'
await promise
yield 'inner-value'
t.fail('should not get here, iterator should be destroyed')
} finally {
t.ok(true, 'pass')
}
})()
client.request({ path: '/', method: 'POST', body }, (err, { statusCode, body }) => {
t.ifError(err)
t.strictEqual(statusCode, 200)
gotDestroyed()
})
})
await t.completed
})
test('async iterator yield unsupported TypedArray', {
skip: !!require('node:stream')._isArrayBufferView
}, async (t) => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
req.on('end', () => {
res.writeHead(200)
res.end()
})
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
bodyTimeout: 0
})
after(() => client.close())
const body = (async function * () {
try {
yield new Int32Array([1])
t.fail('should not get here, iterator should be destroyed')
} finally {
t.ok(true, 'pass')
}
})()
client.request({ path: '/', method: 'POST', body }, (err) => {
t.ok(err)
t.strictEqual(err.code, 'ERR_INVALID_ARG_TYPE')
})
})
await t.completed
})
test('async iterator yield object error', async (t) => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
req.on('end', () => {
res.writeHead(200)
res.end()
})
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
bodyTimeout: 0
})
after(() => client.close())
const body = (async function * () {
try {
yield {}
t.fail('should not get here, iterator should be destroyed')
} finally {
t.ok(true, 'pass')
}
})()
client.request({ path: '/', method: 'POST', body }, (err) => {
t.ok(err)
t.strictEqual(err.code, 'ERR_INVALID_ARG_TYPE')
})
})
await t.completed
})
test('Successfully get a Response when neither a Transfer-Encoding or Content-Length header is present', async (t) => {
t = tspl(t, { plan: 4 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
req.on('data', (data) => {
})
req.on('end', () => {
res.removeHeader('transfer-encoding')
res.writeHead(200, {
// Header isn't actually necessary, but tells node to close after response
connection: 'close',
foo: 'bar'
})
res.flushHeaders()
res.end('a response body')
})
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.request({ path: '/', method: 'GET' }, (err, { body, headers }) => {
t.ifError(err)
t.equal(headers['content-length'], undefined)
t.equal(headers['transfer-encoding'], undefined)
const bufs = []
body.on('error', () => {
t.fail('Closing the connection is valid')
})
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.equal('a response body', Buffer.concat(bufs).toString('utf8'))
})
})
})
await t.completed
})
function buildParams (path) {
const cleanPath = path.replace('/?', '').replace('/', '').split('&')
const builtParams = cleanPath.reduce((acc, entry) => {
const [key, value] = entry.split('=')
if (key.length === 0) {
return acc
}
if (acc[key]) {
if (Array.isArray(acc[key])) {
acc[key].push(value)
} else {
acc[key] = [acc[key], value]
}
} else {
acc[key] = value
}
return acc
}, {})
return builtParams
}
test('\\r\\n in Headers', async (t) => {
t = tspl(t, { plan: 1 })
const reqHeaders = {
bar: '\r\nbar'
}
const client = new Client('http://localhost:4242', {
keepAliveTimeout: 300e3
})
after(() => client.close())
client.request({
path: '/',
method: 'GET',
headers: reqHeaders
}, (err) => {
t.strictEqual(err.message, 'invalid bar header')
})
})
test('\\r in Headers', async (t) => {
t = tspl(t, { plan: 1 })
const reqHeaders = {
bar: '\rbar'
}
const client = new Client('http://localhost:4242', {
keepAliveTimeout: 300e3
})
after(() => client.close())
client.request({
path: '/',
method: 'GET',
headers: reqHeaders
}, (err) => {
t.strictEqual(err.message, 'invalid bar header')
})
})
test('\\n in Headers', async (t) => {
t = tspl(t, { plan: 1 })
const reqHeaders = {
bar: '\nbar'
}
const client = new Client('http://localhost:4242', {
keepAliveTimeout: 300e3
})
after(() => client.close())
client.request({
path: '/',
method: 'GET',
headers: reqHeaders
}, (err) => {
t.strictEqual(err.message, 'invalid bar header')
})
})
test('\\n in Headers', async (t) => {
t = tspl(t, { plan: 1 })
const reqHeaders = {
'\nbar': 'foo'
}
const client = new Client('http://localhost:4242', {
keepAliveTimeout: 300e3
})
after(() => client.close())
client.request({
path: '/',
method: 'GET',
headers: reqHeaders
}, (err) => {
t.strictEqual(err.message, 'invalid header key')
})
})
test('\\n in Path', async (t) => {
t = tspl(t, { plan: 1 })
const client = new Client('http://localhost:4242', {
keepAliveTimeout: 300e3
})
after(() => client.close())
client.request({
path: '/\n',
method: 'GET'
}, (err) => {
t.strictEqual(err.message, 'invalid request path')
})
})
test('\\n in Method', async (t) => {
t = tspl(t, { plan: 1 })
const client = new Client('http://localhost:4242', {
keepAliveTimeout: 300e3
})
after(() => client.close())
client.request({
path: '/',
method: 'GET\n'
}, (err) => {
t.strictEqual(err.message, 'invalid request method')
})
})
test('stats', async (t) => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
t.strictEqual('/', req.url)
t.strictEqual('GET', req.method)
t.strictEqual(`localhost:${server.address().port}`, req.headers.host)
res.setHeader('Content-Type', 'text/plain')
res.end('hello')
})
after(() => server.close())
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
after(() => client.close())
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
t.ifError(err)
t.strictEqual(client.stats.connected, true)
t.strictEqual(client.stats.pending, 0)
t.strictEqual(client.stats.running, 1)
})
})
await t.completed
})