diff options
Diffstat (limited to '')
-rw-r--r-- | test/client.js | 2096 |
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') + }) +}) |