summaryrefslogtreecommitdiffstats
path: root/test/client.js
diff options
context:
space:
mode:
Diffstat (limited to 'test/client.js')
-rw-r--r--test/client.js2096
1 files changed, 2096 insertions, 0 deletions
diff --git a/test/client.js b/test/client.js
new file mode 100644
index 0000000..92315d6
--- /dev/null
+++ b/test/client.js
@@ -0,0 +1,2096 @@
+'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')
+ })
+})