package-rebuilds/pkgs/node-undici/node-undici-5.28.4+dfsg1+~cs23.12.11/test/client.js
Fabian Grünbichler afee63929c add node-undici for libgit2 1.8.1
Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
2024-09-10 11:23:24 +02:00

2097 lines
50 KiB
JavaScript

'use strict'
const { readFileSync, createReadStream } = require('fs')
const { createServer } = require('http')
const { Readable } = require('stream')
const { test } = require('tap')
const { Client, errors } = require('..')
const { kSocket } = require('../lib/core/symbols')
const { wrapWithAsyncIterable } = require('./utils/async-iterators')
const EE = require('events')
const { kUrl, kSize, kConnect, kBusy, kConnected, kRunning } = require('../lib/core/symbols')
const hasIPv6 = (() => {
const iFaces = require('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 === 6)
)
})()
test('basic get', (t) => {
t.plan(24)
const server = createServer((req, res) => {
t.equal('/', req.url)
t.equal('GET', req.method)
t.equal(`localhost:${server.address().port}`, req.headers.host)
t.equal(undefined, req.headers.foo)
t.equal('bar', req.headers.bar)
t.equal(undefined, req.headers['content-length'])
res.setHeader('Content-Type', 'text/plain')
res.end('hello')
})
t.teardown(server.close.bind(server))
const reqHeaders = {
foo: undefined,
bar: 'bar'
}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
keepAliveTimeout: 300e3
})
t.teardown(client.close.bind(client))
t.equal(client[kUrl].origin, `http://localhost:${server.address().port}`)
const signal = new EE()
client.request({
signal,
path: '/',
method: 'GET',
headers: reqHeaders
}, (err, data) => {
t.error(err)
const { statusCode, headers, body } = data
t.equal(statusCode, 200)
t.equal(signal.listenerCount('abort'), 1)
t.equal(headers['content-type'], 'text/plain')
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.equal(signal.listenerCount('abort'), 0)
t.equal('hello', Buffer.concat(bufs).toString('utf8'))
})
})
t.equal(signal.listenerCount('abort'), 1)
client.request({
path: '/',
method: 'GET',
headers: reqHeaders
}, (err, { statusCode, headers, body }) => {
t.error(err)
t.equal(statusCode, 200)
t.equal(headers['content-type'], 'text/plain')
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.equal('hello', Buffer.concat(bufs).toString('utf8'))
})
})
})
})
test('basic get with custom request.reset=true', (t) => {
t.plan(26)
const server = createServer((req, res) => {
t.equal('/', req.url)
t.equal('GET', req.method)
t.equal(`localhost:${server.address().port}`, req.headers.host)
t.equal(req.headers.connection, 'close')
t.equal(undefined, req.headers.foo)
t.equal('bar', req.headers.bar)
t.equal(undefined, req.headers['content-length'])
res.setHeader('Content-Type', 'text/plain')
res.end('hello')
})
t.teardown(server.close.bind(server))
const reqHeaders = {
foo: undefined,
bar: 'bar'
}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {})
t.teardown(client.close.bind(client))
t.equal(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.error(err)
const { statusCode, headers, body } = data
t.equal(statusCode, 200)
t.equal(signal.listenerCount('abort'), 1)
t.equal(headers['content-type'], 'text/plain')
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.equal(signal.listenerCount('abort'), 0)
t.equal('hello', Buffer.concat(bufs).toString('utf8'))
})
})
t.equal(signal.listenerCount('abort'), 1)
client.request({
path: '/',
reset: true,
method: 'GET',
headers: reqHeaders
}, (err, { statusCode, headers, body }) => {
t.error(err)
t.equal(statusCode, 200)
t.equal(headers['content-type'], 'text/plain')
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.equal('hello', Buffer.concat(bufs).toString('utf8'))
})
})
})
})
test('basic get with query params', (t) => {
t.plan(4)
const server = createServer((req, res) => {
const searchParamsObject = buildParams(req.url)
t.strictSame(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')
})
t.teardown(server.close.bind(server))
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
})
t.teardown(client.close.bind(client))
const signal = new EE()
client.request({
signal,
path: '/',
method: 'GET',
query
}, (err, data) => {
t.error(err)
const { statusCode } = data
t.equal(statusCode, 200)
})
t.equal(signal.listenerCount('abort'), 1)
})
})
test('basic get with query params fails if url includes hashmark', (t) => {
t.plan(1)
const server = createServer((req, res) => {
t.fail()
})
t.teardown(server.close.bind(server))
const query = {
foo: 1,
bar: 'bar',
multi: [1, 2]
}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
keepAliveTimeout: 300e3
})
t.teardown(client.close.bind(client))
const signal = new EE()
client.request({
signal,
path: '/#',
method: 'GET',
query
}, (err, data) => {
t.equal(err.message, 'Query params cannot be passed when url already contains "?" or "#".')
})
})
})
test('basic get with empty query params', (t) => {
t.plan(4)
const server = createServer((req, res) => {
const searchParamsObject = buildParams(req.url)
t.strictSame(searchParamsObject, {})
res.statusCode = 200
res.end('hello')
})
t.teardown(server.close.bind(server))
const query = {}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
keepAliveTimeout: 300e3
})
t.teardown(client.close.bind(client))
const signal = new EE()
client.request({
signal,
path: '/',
method: 'GET',
query
}, (err, data) => {
t.error(err)
const { statusCode } = data
t.equal(statusCode, 200)
})
t.equal(signal.listenerCount('abort'), 1)
})
})
test('basic get with query params partially in path', (t) => {
t.plan(1)
const server = createServer((req, res) => {
t.fail()
})
t.teardown(server.close.bind(server))
const query = {
foo: 1
}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
keepAliveTimeout: 300e3
})
t.teardown(client.close.bind(client))
const signal = new EE()
client.request({
signal,
path: '/?bar=2',
method: 'GET',
query
}, (err, data) => {
t.equal(err.message, 'Query params cannot be passed when url already contains "?" or "#".')
})
})
})
test('basic get returns 400 when configured to throw on errors (callback)', (t) => {
t.plan(7)
const server = createServer((req, res) => {
res.statusCode = 400
res.end('hello')
})
t.teardown(server.close.bind(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
keepAliveTimeout: 300e3
})
t.teardown(client.close.bind(client))
const signal = new EE()
client.request({
signal,
path: '/',
method: 'GET',
throwOnError: true
}, (err) => {
t.equal(err.message, 'Response status code 400: Bad Request')
t.equal(err.status, 400)
t.equal(err.statusCode, 400)
t.equal(err.headers.connection, 'keep-alive')
t.equal(err.headers['content-length'], '5')
t.same(err.body, null)
})
t.equal(signal.listenerCount('abort'), 1)
})
})
test('basic get returns 400 when configured to throw on errors and correctly handles malformed json (callback)', (t) => {
t.plan(6)
const server = createServer((req, res) => {
res.writeHead(400, 'Invalid params', { 'content-type': 'application/json' })
res.end('Invalid params')
})
t.teardown(server.close.bind(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
keepAliveTimeout: 300e3
})
t.teardown(client.close.bind(client))
const signal = new EE()
client.request({
signal,
path: '/',
method: 'GET',
throwOnError: true
}, (err) => {
t.equal(err.message, 'Response status code 400: Invalid params')
t.equal(err.status, 400)
t.equal(err.statusCode, 400)
t.equal(err.headers.connection, 'keep-alive')
t.same(err.body, null)
})
t.equal(signal.listenerCount('abort'), 1)
})
})
test('basic get returns 400 when configured to throw on errors (promise)', (t) => {
t.plan(6)
const server = createServer((req, res) => {
res.writeHead(400, 'Invalid params', { 'content-type': 'text/plain' })
res.end('Invalid params')
})
t.teardown(server.close.bind(server))
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`, {
keepAliveTimeout: 300e3
})
t.teardown(client.close.bind(client))
const signal = new EE()
try {
await client.request({
signal,
path: '/',
method: 'GET',
throwOnError: true
})
t.fail('Should throw an error')
} catch (err) {
t.equal(err.message, 'Response status code 400: Invalid params')
t.equal(err.status, 400)
t.equal(err.statusCode, 400)
t.equal(err.body, 'Invalid params')
t.equal(err.headers.connection, 'keep-alive')
t.equal(err.headers['content-type'], 'text/plain')
}
})
})
test('basic get returns error body when configured to throw on errors', (t) => {
t.plan(6)
const server = createServer((req, res) => {
const body = { msg: 'Error', details: { code: 94 } }
const bodyAsString = JSON.stringify(body)
res.writeHead(400, 'Invalid params', {
'Content-Type': 'application/json'
})
res.end(bodyAsString)
})
t.teardown(server.close.bind(server))
server.listen(0, async () => {
const client = new Client(`http://localhost:${server.address().port}`, {
keepAliveTimeout: 300e3
})
t.teardown(client.close.bind(client))
const signal = new EE()
try {
await client.request({
signal,
path: '/',
method: 'GET',
throwOnError: true
})
t.fail('Should throw an error')
} catch (err) {
t.equal(err.message, 'Response status code 400: Invalid params')
t.equal(err.status, 400)
t.equal(err.statusCode, 400)
t.equal(err.headers.connection, 'keep-alive')
t.equal(err.headers['content-type'], 'application/json')
t.same(err.body, { msg: 'Error', details: { code: 94 } })
}
})
})
test('basic head', (t) => {
t.plan(14)
const server = createServer((req, res) => {
t.equal('/123', req.url)
t.equal('HEAD', req.method)
t.equal(`localhost:${server.address().port}`, req.headers.host)
res.setHeader('content-type', 'text/plain')
res.end('hello')
})
t.teardown(server.close.bind(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.teardown(client.close.bind(client))
client.request({ path: '/123', method: 'HEAD' }, (err, { statusCode, headers, body }) => {
t.error(err)
t.equal(statusCode, 200)
t.equal(headers['content-type'], 'text/plain')
body
.resume()
.on('end', () => {
t.pass()
})
})
client.request({ path: '/123', method: 'HEAD' }, (err, { statusCode, headers, body }) => {
t.error(err)
t.equal(statusCode, 200)
t.equal(headers['content-type'], 'text/plain')
body
.resume()
.on('end', () => {
t.pass()
})
})
})
})
test('basic head (IPv6)', { skip: !hasIPv6 }, (t) => {
t.plan(14)
const server = createServer((req, res) => {
t.equal('/123', req.url)
t.equal('HEAD', req.method)
t.equal(`[::1]:${server.address().port}`, req.headers.host)
res.setHeader('content-type', 'text/plain')
res.end('hello')
})
t.teardown(server.close.bind(server))
server.listen(0, '::', () => {
const client = new Client(`http://[::1]:${server.address().port}`)
t.teardown(client.close.bind(client))
client.request({ path: '/123', method: 'HEAD' }, (err, { statusCode, headers, body }) => {
t.error(err)
t.equal(statusCode, 200)
t.equal(headers['content-type'], 'text/plain')
body
.resume()
.on('end', () => {
t.pass()
})
})
client.request({ path: '/123', method: 'HEAD' }, (err, { statusCode, headers, body }) => {
t.error(err)
t.equal(statusCode, 200)
t.equal(headers['content-type'], 'text/plain')
body
.resume()
.on('end', () => {
t.pass()
})
})
})
})
test('get with host header', (t) => {
t.plan(7)
const server = createServer((req, res) => {
t.equal('/', req.url)
t.equal('GET', req.method)
t.equal('example.com', req.headers.host)
res.setHeader('content-type', 'text/plain')
res.end('hello from ' + req.headers.host)
})
t.teardown(server.close.bind(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.teardown(client.close.bind(client))
client.request({ path: '/', method: 'GET', headers: { host: 'example.com' } }, (err, { statusCode, headers, body }) => {
t.error(err)
t.equal(statusCode, 200)
t.equal(headers['content-type'], 'text/plain')
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.equal('hello from example.com', Buffer.concat(bufs).toString('utf8'))
})
})
})
})
test('get with host header (IPv6)', { skip: !hasIPv6 }, (t) => {
t.plan(7)
const server = createServer((req, res) => {
t.equal('/', req.url)
t.equal('GET', req.method)
t.equal('[::1]', req.headers.host)
res.setHeader('content-type', 'text/plain')
res.end('hello from ' + req.headers.host)
})
t.teardown(server.close.bind(server))
server.listen(0, '::', () => {
const client = new Client(`http://[::1]:${server.address().port}`)
t.teardown(client.close.bind(client))
client.request({ path: '/', method: 'GET', headers: { host: '[::1]' } }, (err, { statusCode, headers, body }) => {
t.error(err)
t.equal(statusCode, 200)
t.equal(headers['content-type'], 'text/plain')
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.equal('hello from [::1]', Buffer.concat(bufs).toString('utf8'))
})
})
})
})
test('head with host header', (t) => {
t.plan(7)
const server = createServer((req, res) => {
t.equal('/', req.url)
t.equal('HEAD', req.method)
t.equal('example.com', req.headers.host)
res.setHeader('content-type', 'text/plain')
res.end('hello from ' + req.headers.host)
})
t.teardown(server.close.bind(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.teardown(client.close.bind(client))
client.request({ path: '/', method: 'HEAD', headers: { host: 'example.com' } }, (err, { statusCode, headers, body }) => {
t.error(err)
t.equal(statusCode, 200)
t.equal(headers['content-type'], 'text/plain')
body
.resume()
.on('end', () => {
t.pass()
})
})
})
})
function postServer (t, expected) {
return function (req, res) {
t.equal(req.url, '/')
t.equal(req.method, 'POST')
t.notSame(req.headers['content-length'], null)
req.setEncoding('utf8')
let data = ''
req.on('data', function (d) { data += d })
req.on('end', () => {
t.equal(data, expected)
res.end('hello')
})
}
}
test('basic POST with string', (t) => {
t.plan(7)
const expected = readFileSync(__filename, 'utf8')
const server = createServer(postServer(t, expected))
t.teardown(server.close.bind(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.teardown(client.close.bind(client))
client.request({ path: '/', method: 'POST', body: expected }, (err, data) => {
t.error(err)
t.equal(data.statusCode, 200)
const bufs = []
data.body
.on('data', (buf) => {
bufs.push(buf)
})
.on('end', () => {
t.equal('hello', Buffer.concat(bufs).toString('utf8'))
})
})
})
})
test('basic POST with empty string', (t) => {
t.plan(7)
const server = createServer(postServer(t, ''))
t.teardown(server.close.bind(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.teardown(client.close.bind(client))
client.request({ path: '/', method: 'POST', body: '' }, (err, { statusCode, headers, body }) => {
t.error(err)
t.equal(statusCode, 200)
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.equal('hello', Buffer.concat(bufs).toString('utf8'))
})
})
})
})
test('basic POST with string and content-length', (t) => {
t.plan(7)
const expected = readFileSync(__filename, 'utf8')
const server = createServer(postServer(t, expected))
t.teardown(server.close.bind(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.teardown(client.close.bind(client))
client.request({
path: '/',
method: 'POST',
headers: {
'content-length': Buffer.byteLength(expected)
},
body: expected
}, (err, { statusCode, headers, body }) => {
t.error(err)
t.equal(statusCode, 200)
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.equal('hello', Buffer.concat(bufs).toString('utf8'))
})
})
})
})
test('basic POST with Buffer', (t) => {
t.plan(7)
const expected = readFileSync(__filename)
const server = createServer(postServer(t, expected.toString()))
t.teardown(server.close.bind(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.teardown(client.close.bind(client))
client.request({ path: '/', method: 'POST', body: expected }, (err, { statusCode, headers, body }) => {
t.error(err)
t.equal(statusCode, 200)
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.equal('hello', Buffer.concat(bufs).toString('utf8'))
})
})
})
})
test('basic POST with stream', (t) => {
t.plan(7)
const expected = readFileSync(__filename, 'utf8')
const server = createServer(postServer(t, expected))
t.teardown(server.close.bind(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.teardown(client.close.bind(client))
client.request({
path: '/',
method: 'POST',
headers: {
'content-length': Buffer.byteLength(expected)
},
headersTimeout: 0,
body: createReadStream(__filename)
}, (err, { statusCode, headers, body }) => {
t.error(err)
t.equal(statusCode, 200)
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.equal('hello', Buffer.concat(bufs).toString('utf8'))
})
})
})
})
test('basic POST with paused stream', (t) => {
t.plan(7)
const expected = readFileSync(__filename, 'utf8')
const server = createServer(postServer(t, expected))
t.teardown(server.close.bind(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.teardown(client.close.bind(client))
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.error(err)
t.equal(statusCode, 200)
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.equal('hello', Buffer.concat(bufs).toString('utf8'))
})
})
})
})
test('basic POST with custom stream', (t) => {
t.plan(4)
const server = createServer((req, res) => {
req.resume().on('end', () => {
res.end('hello')
})
})
t.teardown(server.close.bind(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.teardown(client.close.bind(client))
const body = new EE()
body.pipe = () => {}
client.request({
path: '/',
method: 'POST',
headersTimeout: 0,
body
}, (err, data) => {
t.error(err)
t.equal(data.statusCode, 200)
const bufs = []
data.body.on('data', (buf) => {
bufs.push(buf)
})
data.body.on('end', () => {
t.equal('hello', Buffer.concat(bufs).toString('utf8'))
})
})
t.strictSame(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')
})
})
})
})
})
test('basic POST with iterator', (t) => {
t.plan(3)
const expected = 'hello'
const server = createServer((req, res) => {
req.resume().on('end', () => {
res.end(expected)
})
})
t.teardown(server.close.bind(server))
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}`)
t.teardown(client.close.bind(client))
client.request({
path: '/',
method: 'POST',
requestTimeout: 0,
body: iterable
}, (err, { statusCode, body }) => {
t.error(err)
t.equal(statusCode, 200)
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.equal('hello', Buffer.concat(bufs).toString('utf8'))
})
})
})
})
test('basic POST with iterator with invalid data', (t) => {
t.plan(1)
const server = createServer(() => {})
t.teardown(server.close.bind(server))
const iterable = {
[Symbol.iterator]: function * () {
yield 0
}
}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.teardown(client.close.bind(client))
client.request({
path: '/',
method: 'POST',
requestTimeout: 0,
body: iterable
}, err => {
t.ok(err instanceof TypeError)
})
})
})
test('basic POST with async iterator', (t) => {
t.plan(7)
const expected = readFileSync(__filename, 'utf8')
const server = createServer(postServer(t, expected))
t.teardown(server.close.bind(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.teardown(client.close.bind(client))
client.request({
path: '/',
method: 'POST',
headers: {
'content-length': Buffer.byteLength(expected)
},
headersTimeout: 0,
body: wrapWithAsyncIterable(createReadStream(__filename))
}, (err, { statusCode, headers, body }) => {
t.error(err)
t.equal(statusCode, 200)
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.equal('hello', Buffer.concat(bufs).toString('utf8'))
})
})
})
})
test('basic POST with transfer encoding: chunked', (t) => {
t.plan(8)
let body
const server = createServer(function (req, res) {
t.equal(req.url, '/')
t.equal(req.method, 'POST')
t.same(req.headers['content-length'], null)
t.equal(req.headers['transfer-encoding'], 'chunked')
body.push(null)
req.setEncoding('utf8')
let data = ''
req.on('data', function (d) { data += d })
req.on('end', () => {
t.equal(data, 'asd')
res.end('hello')
})
})
t.teardown(server.close.bind(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.teardown(client.close.bind(client))
body = new Readable({
read () { }
})
body.push('asd')
client.request({
path: '/',
method: 'POST',
// no content-length header
body
}, (err, { statusCode, headers, body }) => {
t.error(err)
t.equal(statusCode, 200)
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.equal('hello', Buffer.concat(bufs).toString('utf8'))
})
})
})
})
test('basic POST with empty stream', (t) => {
t.plan(4)
const server = createServer(function (req, res) {
t.same(req.headers['content-length'], 0)
req.pipe(res)
})
t.teardown(server.close.bind(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.teardown(client.close.bind(client))
const body = new Readable({
autoDestroy: false,
read () {
},
destroy (err, callback) {
callback(!this._readableState.endEmitted ? new Error('asd') : err)
}
}).on('end', () => {
process.nextTick(() => {
t.equal(body.destroyed, true)
})
})
body.push(null)
client.request({
path: '/',
method: 'POST',
body
}, (err, { statusCode, headers, body }) => {
t.error(err)
body
.on('data', () => {
t.fail()
})
.on('end', () => {
t.pass()
})
})
})
})
test('10 times GET', (t) => {
const num = 10
t.plan(3 * 10)
const server = createServer((req, res) => {
res.end(req.url)
})
t.teardown(server.close.bind(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.teardown(client.close.bind(client))
for (let i = 0; i < num; i++) {
makeRequest(i)
}
function makeRequest (i) {
client.request({ path: '/' + i, method: 'GET' }, (err, { statusCode, headers, body }) => {
t.error(err)
t.equal(statusCode, 200)
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.equal('/' + i, Buffer.concat(bufs).toString('utf8'))
})
})
}
})
})
test('10 times HEAD', (t) => {
const num = 10
t.plan(3 * 10)
const server = createServer((req, res) => {
res.end(req.url)
})
t.teardown(server.close.bind(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.teardown(client.close.bind(client))
for (let i = 0; i < num; i++) {
makeRequest(i)
}
function makeRequest (i) {
client.request({ path: '/' + i, method: 'HEAD' }, (err, { statusCode, headers, body }) => {
t.error(err)
t.equal(statusCode, 200)
body
.resume()
.on('end', () => {
t.pass()
})
})
}
})
})
test('Set-Cookie', (t) => {
t.plan(4)
const server = createServer((req, res) => {
res.setHeader('content-type', 'text/plain')
res.setHeader('Set-Cookie', ['a cookie', 'another cookie', 'more cookies'])
res.end('hello')
})
t.teardown(server.close.bind(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.teardown(client.close.bind(client))
client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => {
t.error(err)
t.equal(statusCode, 200)
t.strictSame(headers['set-cookie'], ['a cookie', 'another cookie', 'more cookies'])
const bufs = []
body.on('data', (buf) => {
bufs.push(buf)
})
body.on('end', () => {
t.equal('hello', Buffer.concat(bufs).toString('utf8'))
})
})
})
})
test('ignore request header mutations', (t) => {
t.plan(2)
const server = createServer((req, res) => {
t.equal(req.headers.test, 'test')
res.end()
})
t.teardown(server.close.bind(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.teardown(client.close.bind(client))
const headers = { test: 'test' }
client.request({
path: '/',
method: 'GET',
headers
}, (err, { body }) => {
t.error(err)
body.resume()
})
headers.test = 'asd'
})
})
test('url-like url', (t) => {
t.plan(1)
const server = createServer((req, res) => {
res.end()
})
t.teardown(server.close.bind(server))
server.listen(0, () => {
const client = new Client({
hostname: 'localhost',
port: server.address().port,
protocol: 'http:'
})
t.teardown(client.close.bind(client))
client.request({ path: '/', method: 'GET' }, (err, data) => {
t.error(err)
data.body.resume()
})
})
})
test('an absolute url as path', (t) => {
t.plan(2)
const path = 'http://example.com'
const server = createServer((req, res) => {
t.equal(req.url, path)
res.end()
})
t.teardown(server.close.bind(server))
server.listen(0, () => {
const client = new Client({
hostname: 'localhost',
port: server.address().port,
protocol: 'http:'
})
t.teardown(client.close.bind(client))
client.request({ path, method: 'GET' }, (err, data) => {
t.error(err)
data.body.resume()
})
})
})
test('multiple destroy callback', (t) => {
t.plan(4)
const server = createServer((req, res) => {
res.end()
})
t.teardown(server.close.bind(server))
server.listen(0, () => {
const client = new Client({
hostname: 'localhost',
port: server.address().port,
protocol: 'http:'
})
t.teardown(client.destroy.bind(client))
client.request({ path: '/', method: 'GET' }, (err, data) => {
t.error(err)
data.body
.resume()
.on('error', () => {
t.pass()
})
client.destroy(new Error(), (err) => {
t.error(err)
})
client.destroy(new Error(), (err) => {
t.error(err)
})
})
})
})
test('only one streaming req at a time', (t) => {
t.plan(7)
const server = createServer((req, res) => {
req.pipe(res)
})
t.teardown(server.close.bind(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 4
})
t.teardown(client.destroy.bind(client))
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
t.error(err)
data.body.resume()
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
t.error(err)
data.body.resume()
})
client.request({
path: '/',
method: 'PUT',
idempotent: true,
body: new Readable({
read () {
setImmediate(() => {
t.equal(client[kBusy], true)
this.push(null)
})
}
}).on('resume', () => {
t.equal(client[kSize], 1)
})
}, (err, data) => {
t.error(err)
data.body
.resume()
.on('end', () => {
t.pass()
})
})
t.equal(client[kBusy], true)
})
})
})
test('only one async iterating req at a time', (t) => {
t.plan(6)
const server = createServer((req, res) => {
req.pipe(res)
})
t.teardown(server.close.bind(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 4
})
t.teardown(client.destroy.bind(client))
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
t.error(err)
data.body.resume()
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
t.error(err)
data.body.resume()
})
const body = wrapWithAsyncIterable(new Readable({
read () {
setImmediate(() => {
t.equal(client[kBusy], true)
this.push(null)
})
}
}))
client.request({
path: '/',
method: 'PUT',
idempotent: true,
body
}, (err, data) => {
t.error(err)
data.body
.resume()
.on('end', () => {
t.pass()
})
})
t.equal(client[kBusy], true)
})
})
})
test('300 requests succeed', (t) => {
t.plan(300 * 3)
const server = createServer((req, res) => {
res.end('asd')
})
t.teardown(server.close.bind(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.teardown(client.destroy.bind(client))
for (let n = 0; n < 300; ++n) {
client.request({
path: '/',
method: 'GET'
}, (err, data) => {
t.error(err)
data.body.on('data', (chunk) => {
t.equal(chunk.toString(), 'asd')
}).on('end', () => {
t.pass()
})
})
}
})
})
test('request args validation', (t) => {
t.plan(2)
const client = new Client('http://localhost:5000')
client.request(null, (err) => {
t.type(err, errors.InvalidArgumentError)
})
try {
client.request(null, 'asd')
} catch (err) {
t.type(err, errors.InvalidArgumentError)
}
})
test('request args validation promise', (t) => {
t.plan(1)
const client = new Client('http://localhost:5000')
client.request(null).catch((err) => {
t.type(err, errors.InvalidArgumentError)
})
})
test('increase pipelining', (t) => {
t.plan(4)
const server = createServer((req, res) => {
req.resume()
})
t.teardown(server.close.bind(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.teardown(client.destroy.bind(client))
client.request({
path: '/',
method: 'GET'
}, () => {
if (!client.destroyed) {
t.fail()
}
})
client.request({
path: '/',
method: 'GET'
}, () => {
if (!client.destroyed) {
t.fail()
}
})
t.equal(client[kRunning], 0)
client.on('connect', () => {
t.equal(client[kRunning], 0)
process.nextTick(() => {
t.equal(client[kRunning], 1)
client.pipelining = 3
t.equal(client[kRunning], 2)
})
})
})
})
test('destroy in push', (t) => {
t.plan(4)
let _res
const server = createServer((req, res) => {
res.write('asd')
_res = res
})
t.teardown(server.close.bind(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.teardown(client.close.bind(client))
client.request({ path: '/', method: 'GET' }, (err, { body }) => {
t.error(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.error(err)
let buf = ''
body.on('data', (chunk) => {
buf = chunk.toString()
_res.end()
}).on('end', () => {
t.equal('asd', buf)
})
})
})
})
test('non recoverable socket error fails pending request', (t) => {
t.plan(2)
const server = createServer((req, res) => {
res.end()
})
t.teardown(server.close.bind(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.teardown(client.close.bind(client))
client.request({ path: '/', method: 'GET' }, (err, data) => {
t.equal(err.message, 'kaboom')
})
client.request({ path: '/', method: 'GET' }, (err, data) => {
t.equal(err.message, 'kaboom')
})
client.on('connect', () => {
client[kSocket].destroy(new Error('kaboom'))
})
})
})
test('POST empty with error', (t) => {
t.plan(1)
const server = createServer((req, res) => {
req.pipe(res)
})
t.teardown(server.close.bind(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
t.teardown(client.close.bind(client))
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.equal(err.message, 'asd')
})
})
})
test('busy', (t) => {
t.plan(2)
const server = createServer((req, res) => {
req.pipe(res)
})
t.teardown(server.close.bind(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
pipelining: 1
})
t.teardown(client.close.bind(client))
client[kConnect](() => {
client.request({
path: '/',
method: 'GET'
}, (err) => {
t.error(err)
})
t.equal(client[kBusy], true)
})
})
})
test('connected', (t) => {
t.plan(7)
const server = createServer((req, res) => {
// needed so that disconnect is emitted
res.setHeader('connection', 'close')
req.pipe(res)
})
t.teardown(server.close.bind(server))
server.listen(0, () => {
const url = new URL(`http://localhost:${server.address().port}`)
const client = new Client(url, {
pipelining: 1
})
t.teardown(client.close.bind(client))
client.on('connect', (origin, [self]) => {
t.equal(origin, url)
t.equal(client, self)
})
client.on('disconnect', (origin, [self]) => {
t.equal(origin, url)
t.equal(client, self)
})
t.equal(client[kConnected], false)
client[kConnect](() => {
client.request({
path: '/',
method: 'GET'
}, (err) => {
t.error(err)
})
t.equal(client[kConnected], true)
})
})
})
test('emit disconnect after destroy', t => {
t.plan(4)
const server = createServer((req, res) => {
req.pipe(res)
})
t.teardown(server.close.bind(server))
server.listen(0, () => {
const url = new URL(`http://localhost:${server.address().port}`)
const client = new Client(url)
t.equal(client[kConnected], false)
client[kConnect](() => {
t.equal(client[kConnected], true)
let disconnected = false
client.on('disconnect', () => {
disconnected = true
t.pass()
})
client.destroy(() => {
t.equal(disconnected, true)
})
})
})
})
test('end response before request', t => {
t.plan(2)
const server = createServer((req, res) => {
res.end()
})
t.teardown(server.close.bind(server))
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.pass()
})
.resume()
client.on('disconnect', (url, targets, err) => {
t.equal(err.code, 'UND_ERR_INFO')
})
})
})
test('parser pause with no body timeout', (t) => {
t.plan(2)
const server = createServer((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)
})
t.teardown(server.close.bind(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
bodyTimeout: 0
})
t.teardown(client.close.bind(client))
client.request({ path: '/', method: 'GET' }, (err, { statusCode, body }) => {
t.error(err)
t.equal(statusCode, 200)
body.resume()
})
})
})
test('TypedArray and DataView body', (t) => {
t.plan(3)
const server = createServer((req, res) => {
t.equal(req.headers['content-length'], '8')
res.end()
})
t.teardown(server.close.bind(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
bodyTimeout: 0
})
t.teardown(client.close.bind(client))
const body = Uint8Array.from(Buffer.alloc(8))
client.request({ path: '/', method: 'POST', body }, (err, { statusCode, body }) => {
t.error(err)
t.equal(statusCode, 200)
body.resume()
})
})
})
test('async iterator empty chunk continues', (t) => {
t.plan(5)
const serverChunks = ['hello', 'world']
const server = createServer((req, res) => {
let str = ''
let i = 0
req.on('data', (chunk) => {
const content = chunk.toString()
t.equal(serverChunks[i++], content)
str += content
}).on('end', () => {
t.equal(str, serverChunks.join(''))
res.end()
})
})
t.teardown(server.close.bind(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
bodyTimeout: 0
})
t.teardown(client.close.bind(client))
const body = (async function * () {
yield serverChunks[0]
yield ''
yield serverChunks[1]
})()
client.request({ path: '/', method: 'POST', body }, (err, { statusCode, body }) => {
t.error(err)
t.equal(statusCode, 200)
body.resume()
})
})
})
test('async iterator error from server destroys early', (t) => {
t.plan(3)
const server = createServer((req, res) => {
req.on('data', (chunk) => {
res.destroy()
})
})
t.teardown(server.close.bind(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
bodyTimeout: 0
})
t.teardown(client.close.bind(client))
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)
}
})()
client.request({ path: '/', method: 'POST', body }, (err, { statusCode, body }) => {
t.ok(err)
t.equal(statusCode, undefined)
gotDestroyed()
})
})
})
test('regular iterator error from server closes early', (t) => {
t.plan(3)
const server = createServer((req, res) => {
req.on('data', () => {
process.nextTick(() => {
res.destroy()
})
})
})
t.teardown(server.close.bind(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
bodyTimeout: 0
})
t.teardown(client.close.bind(client))
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)
}
})()
client.request({ path: '/', method: 'POST', body }, (err, { statusCode, body }) => {
t.ok(err)
t.equal(statusCode, undefined)
gotDestroyed = true
})
})
})
test('async iterator early return closes early', (t) => {
t.plan(3)
const server = createServer((req, res) => {
req.on('data', () => {
res.writeHead(200)
res.end()
})
})
t.teardown(server.close.bind(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
bodyTimeout: 0
})
t.teardown(client.close.bind(client))
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)
}
})()
client.request({ path: '/', method: 'POST', body }, (err, { statusCode, body }) => {
t.error(err)
t.equal(statusCode, 200)
gotDestroyed()
})
})
})
test('async iterator yield unsupported TypedArray', (t) => {
t.plan(3)
const server = createServer((req, res) => {
req.on('end', () => {
res.writeHead(200)
res.end()
})
})
t.teardown(server.close.bind(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
bodyTimeout: 0
})
t.teardown(client.close.bind(client))
const body = (async function * () {
try {
yield new Int32Array([1])
t.fail('should not get here, iterator should be destroyed')
} finally {
t.ok(true)
}
})()
client.request({ path: '/', method: 'POST', body }, (err) => {
t.ok(err)
t.equal(err.code, 'ERR_INVALID_ARG_TYPE')
})
})
})
test('async iterator yield object error', (t) => {
t.plan(3)
const server = createServer((req, res) => {
req.on('end', () => {
res.writeHead(200)
res.end()
})
})
t.teardown(server.close.bind(server))
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`, {
bodyTimeout: 0
})
t.teardown(client.close.bind(client))
const body = (async function * () {
try {
yield {}
t.fail('should not get here, iterator should be destroyed')
} finally {
t.ok(true)
}
})()
client.request({ path: '/', method: 'POST', body }, (err) => {
t.ok(err)
t.equal(err.code, 'ERR_INVALID_ARG_TYPE')
})
})
})
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', (t) => {
t.plan(1)
const reqHeaders = {
bar: '\r\nbar'
}
const client = new Client('http://localhost:4242', {
keepAliveTimeout: 300e3
})
t.teardown(client.close.bind(client))
client.request({
path: '/',
method: 'GET',
headers: reqHeaders
}, (err) => {
t.equal(err.message, 'invalid bar header')
})
})
test('\\r in Headers', (t) => {
t.plan(1)
const reqHeaders = {
bar: '\rbar'
}
const client = new Client('http://localhost:4242', {
keepAliveTimeout: 300e3
})
t.teardown(client.close.bind(client))
client.request({
path: '/',
method: 'GET',
headers: reqHeaders
}, (err) => {
t.equal(err.message, 'invalid bar header')
})
})
test('\\n in Headers', (t) => {
t.plan(1)
const reqHeaders = {
bar: '\nbar'
}
const client = new Client('http://localhost:4242', {
keepAliveTimeout: 300e3
})
t.teardown(client.close.bind(client))
client.request({
path: '/',
method: 'GET',
headers: reqHeaders
}, (err) => {
t.equal(err.message, 'invalid bar header')
})
})
test('\\n in Headers', (t) => {
t.plan(1)
const reqHeaders = {
'\nbar': 'foo'
}
const client = new Client('http://localhost:4242', {
keepAliveTimeout: 300e3
})
t.teardown(client.close.bind(client))
client.request({
path: '/',
method: 'GET',
headers: reqHeaders
}, (err) => {
t.equal(err.message, 'invalid header key')
})
})
test('\\n in Path', (t) => {
t.plan(1)
const client = new Client('http://localhost:4242', {
keepAliveTimeout: 300e3
})
t.teardown(client.close.bind(client))
client.request({
path: '/\n',
method: 'GET'
}, (err) => {
t.equal(err.message, 'invalid request path')
})
})
test('\\n in Method', (t) => {
t.plan(1)
const client = new Client('http://localhost:4242', {
keepAliveTimeout: 300e3
})
t.teardown(client.close.bind(client))
client.request({
path: '/',
method: 'GET\n'
}, (err) => {
t.equal(err.message, 'invalid request method')
})
})