'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') }) })