diff --git a/test/client-connect.js b/test/client-connect.js index 7c8ca5ee6e1..4131c6e0a13 100644 --- a/test/client-connect.js +++ b/test/client-connect.js @@ -6,277 +6,7 @@ const http = require('http') const EE = require('events') const { kBusy } = require('../lib/core/symbols') -test('basic connect', (t) => { - t.plan(3) - - const server = http.createServer((c) => { - t.fail() - }) - server.on('connect', (req, socket, firstBodyChunk) => { - socket.write('HTTP/1.1 200 Connection established\r\n\r\n') - - let data = firstBodyChunk.toString() - socket.on('data', (buf) => { - data += buf.toString() - }) - - socket.on('end', () => { - socket.end(data) - }) - }) - t.teardown(server.close.bind(server)) - - server.listen(0, async () => { - const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) - - const signal = new EE() - const promise = client.connect({ - signal, - path: '/' - }) - t.equal(signal.listenerCount('abort'), 1) - const { socket } = await promise - t.equal(signal.listenerCount('abort'), 0) - - let recvData = '' - socket.on('data', (d) => { - recvData += d - }) - - socket.on('end', () => { - t.equal(recvData.toString(), 'Body') - }) - - socket.write('Body') - socket.end() - }) -}) - -test('connect error', (t) => { - t.plan(1) - - const server = http.createServer((c) => { - t.fail() - }) - server.on('connect', (req, socket, firstBodyChunk) => { - socket.destroy() - }) - t.teardown(server.close.bind(server)) - - server.listen(0, async () => { - const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) - - try { - await client.connect({ - path: '/' - }) - } catch (err) { - t.ok(err) - } - }) -}) - -test('connect invalid opts', (t) => { - t.plan(6) - - const client = new Client('http://localhost:5432') - - client.connect(null, err => { - t.type(err, errors.InvalidArgumentError) - t.equal(err.message, 'invalid opts') - }) - - try { - client.connect(null, null) - t.fail() - } catch (err) { - t.type(err, errors.InvalidArgumentError) - t.equal(err.message, 'invalid opts') - } - - try { - client.connect({ path: '/' }, null) - t.fail() - } catch (err) { - t.type(err, errors.InvalidArgumentError) - t.equal(err.message, 'invalid callback') - } -}) - -test('connect wait for empty pipeline', (t) => { - t.plan(7) - - let canConnect = false - const server = http.createServer((req, res) => { - res.end() - canConnect = true - }) - server.on('connect', (req, socket, firstBodyChunk) => { - t.equal(canConnect, true) - socket.write('HTTP/1.1 200 Connection established\r\n\r\n') - - let data = firstBodyChunk.toString() - socket.on('data', (buf) => { - data += buf.toString() - }) - - socket.on('end', () => { - socket.end(data) - }) - }) - t.teardown(server.close.bind(server)) - - server.listen(0, async () => { - const client = new Client(`http://localhost:${server.address().port}`, { - pipelining: 3 - }) - t.teardown(client.close.bind(client)) - - client.request({ - path: '/', - method: 'GET' - }, (err) => { - t.error(err) - }) - client.once('connect', () => { - process.nextTick(() => { - t.equal(client[kBusy], false) - - client.connect({ - path: '/' - }, (err, { socket }) => { - t.error(err) - let recvData = '' - socket.on('data', (d) => { - recvData += d - }) - - socket.on('end', () => { - t.equal(recvData.toString(), 'Body') - }) - - socket.write('Body') - socket.end() - }) - t.equal(client[kBusy], true) - - client.request({ - path: '/', - method: 'GET' - }, (err) => { - t.error(err) - }) - }) - }) - }) -}) - -test('connect aborted', (t) => { - t.plan(6) - - const server = http.createServer((req, res) => { - t.fail() - }) - server.on('connect', (req, c, firstBodyChunk) => { - t.fail() - }) - t.teardown(server.close.bind(server)) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`, { - pipelining: 3 - }) - t.teardown(client.destroy.bind(client)) - - const signal = new EE() - client.connect({ - path: '/', - signal, - opaque: 'asd' - }, (err, { opaque }) => { - t.equal(opaque, 'asd') - t.equal(signal.listenerCount('abort'), 0) - t.type(err, errors.RequestAbortedError) - }) - t.equal(client[kBusy], true) - t.equal(signal.listenerCount('abort'), 1) - signal.emit('abort') - - client.close(() => { - t.pass() - }) - }) -}) - -test('basic connect error', (t) => { - t.plan(2) - - const server = http.createServer((c) => { - t.fail() - }) - server.on('connect', (req, socket, firstBodyChunk) => { - socket.write('HTTP/1.1 200 Connection established\r\n\r\n') - - let data = firstBodyChunk.toString() - socket.on('data', (buf) => { - data += buf.toString() - }) - - socket.on('end', () => { - socket.end(data) - }) - }) - t.teardown(server.close.bind(server)) - - server.listen(0, async () => { - const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) - - const _err = new Error() - client.connect({ - path: '/' - }, (err, { socket }) => { - t.error(err) - socket.on('error', (err) => { - t.equal(err, _err) - }) - throw _err - }) - }) -}) - -test('connect invalid signal', (t) => { - t.plan(2) - - const server = http.createServer((req, res) => { - t.fail() - }) - server.on('connect', (req, c, firstBodyChunk) => { - t.fail() - }) - 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.on('disconnect', () => { - t.fail() - }) - - client.connect({ - path: '/', - signal: 'error', - opaque: 'asd' - }, (err, { opaque }) => { - t.equal(opaque, 'asd') - t.type(err, errors.InvalidArgumentError) - }) - }) -}) - +// TODO: move to test/node-test/client-connect.js test('connect aborted after connect', (t) => { t.plan(3) diff --git a/test/client-errors.js b/test/client-errors.js index cec7f37d62a..27cc52ba562 100644 --- a/test/client-errors.js +++ b/test/client-errors.js @@ -1,820 +1,10 @@ 'use strict' const { test } = require('tap') -const { Client, Pool, errors } = require('..') -const { createServer } = require('http') -const https = require('https') -const pem = require('https-pem') +const { Client } = require('..') const net = require('net') -const { Readable } = require('stream') - -const { kSocket } = require('../lib/core/symbols') -const { wrapWithAsyncIterable, maybeWrapStream, consts } = require('./utils/async-iterators') - -class IteratorError extends Error {} - -test('GET errors and reconnect with pipelining 1', (t) => { - t.plan(9) - - const server = createServer() - - server.once('request', (req, res) => { - t.pass('first request received, destroying') - res.socket.destroy() - - server.once('request', (req, res) => { - t.equal('/', req.url) - t.equal('GET', req.method) - 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}`, { - pipelining: 1 - }) - t.teardown(client.destroy.bind(client)) - - client.request({ path: '/', method: 'GET', idempotent: false, opaque: 'asd' }, (err, data) => { - t.type(err, Error) // we are expecting an error - t.equal(data.opaque, 'asd') - }) - - client.request({ path: '/', method: 'GET' }, (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('GET errors and reconnect with pipelining 3', (t) => { - const server = createServer() - const requestsThatWillError = 3 - let requests = 0 - - t.plan(6 + requestsThatWillError * 3) - - server.on('request', (req, res) => { - if (requests++ < requestsThatWillError) { - t.pass('request received, destroying') - - // socket might not be there if it was destroyed by another - // pipelined request - if (res.socket) { - res.socket.destroy() - } - } else { - t.equal('/', req.url) - t.equal('GET', req.method) - 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}`, { - pipelining: 3 - }) - t.teardown(client.destroy.bind(client)) - - // all of these will error - for (let i = 0; i < 3; i++) { - client.request({ path: '/', method: 'GET', idempotent: false, opaque: 'asd' }, (err, data) => { - t.type(err, Error) // we are expecting an error - t.equal(data.opaque, 'asd') - }) - } - - // this will be queued up - client.request({ path: '/', method: 'GET', idempotent: false }, (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')) - }) - }) - }) -}) - -function errorAndPipelining (type) { - test(`POST with a ${type} that errors and pipelining 1 should reconnect`, (t) => { - t.plan(12) - - const server = createServer() - server.once('request', (req, res) => { - t.equal('/', req.url) - t.equal('POST', req.method) - t.equal('42', req.headers['content-length']) - - const bufs = [] - req.on('data', (buf) => { - bufs.push(buf) - }) - - req.on('aborted', () => { - // we will abruptly close the connection here - // but this will still end - t.equal('a string', Buffer.concat(bufs).toString('utf8')) - }) - - server.once('request', (req, res) => { - t.equal('/', req.url) - t.equal('GET', req.method) - 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.destroy.bind(client)) - - client.request({ - path: '/', - method: 'POST', - headers: { - // higher than the length of the string - 'content-length': 42 - }, - opaque: 'asd', - body: maybeWrapStream(new Readable({ - read () { - this.push('a string') - this.destroy(new Error('kaboom')) - } - }), type) - }, (err, data) => { - t.equal(err.message, 'kaboom') - t.equal(data.opaque, 'asd') - }) - - // this will be queued up - client.request({ path: '/', method: 'GET', idempotent: false }, (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')) - }) - }) - }) - }) -} - -errorAndPipelining(consts.STREAM) -errorAndPipelining(consts.ASYNC_ITERATOR) - -function errorAndChunkedEncodingPipelining (type) { - test(`POST with chunked encoding, ${type} body that errors and pipelining 1 should reconnect`, (t) => { - t.plan(12) - - const server = createServer() - server.once('request', (req, res) => { - t.equal('/', req.url) - t.equal('POST', req.method) - t.equal(req.headers['content-length'], undefined) - - const bufs = [] - req.on('data', (buf) => { - bufs.push(buf) - }) - - req.on('aborted', () => { - // we will abruptly close the connection here - // but this will still end - t.equal('a string', Buffer.concat(bufs).toString('utf8')) - }) - - server.once('request', (req, res) => { - t.equal('/', req.url) - t.equal('GET', req.method) - 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.destroy.bind(client)) - - client.request({ - path: '/', - method: 'POST', - opaque: 'asd', - body: maybeWrapStream(new Readable({ - read () { - this.push('a string') - this.destroy(new Error('kaboom')) - } - }), type) - }, (err, data) => { - t.equal(err.message, 'kaboom') - t.equal(data.opaque, 'asd') - }) - - // this will be queued up - client.request({ path: '/', method: 'GET' }, (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')) - }) - }) - }) - }) -} - -errorAndChunkedEncodingPipelining(consts.STREAM) -errorAndChunkedEncodingPipelining(consts.ASYNC_ITERATOR) - -test('invalid options throws', (t) => { - try { - new Client({ port: 'foobar', protocol: 'https:' }) // eslint-disable-line - t.fail() - } catch (err) { - t.type(err, errors.InvalidArgumentError) - t.equal(err.message, 'Invalid URL: port must be a valid integer or a string representation of an integer.') - } - - try { - new Client(new URL('http://asd:200/somepath')) // eslint-disable-line - t.fail() - } catch (err) { - t.type(err, errors.InvalidArgumentError) - t.equal(err.message, 'invalid url') - } - - try { - new Client(new URL('http://asd:200?q=asd')) // eslint-disable-line - t.fail() - } catch (err) { - t.type(err, errors.InvalidArgumentError) - t.equal(err.message, 'invalid url') - } - - try { - new Client(new URL('http://asd:200#asd')) // eslint-disable-line - t.fail() - } catch (err) { - t.type(err, errors.InvalidArgumentError) - t.equal(err.message, 'invalid url') - } - - try { - new Client(new URL('http://localhost:200'), { // eslint-disable-line - socketPath: 1 - }) - t.fail() - } catch (err) { - t.type(err, errors.InvalidArgumentError) - t.equal(err.message, 'invalid socketPath') - } - - try { - new Client(new URL('http://localhost:200'), { // eslint-disable-line - keepAliveTimeout: 'asd' - }) // eslint-disable-line - t.fail() - } catch (err) { - t.type(err, errors.InvalidArgumentError) - t.equal(err.message, 'invalid keepAliveTimeout') - } - - try { - new Client(new URL('http://localhost:200'), { // eslint-disable-line - localAddress: 123 - }) // eslint-disable-line - t.fail() - } catch (err) { - t.type(err, errors.InvalidArgumentError) - t.equal(err.message, 'localAddress must be valid string IP address') - } - - try { - new Client(new URL('http://localhost:200'), { // eslint-disable-line - localAddress: 'abcd123' - }) // eslint-disable-line - t.fail() - } catch (err) { - t.type(err, errors.InvalidArgumentError) - t.equal(err.message, 'localAddress must be valid string IP address') - } - - try { - new Client(new URL('http://localhost:200'), { // eslint-disable-line - keepAliveMaxTimeout: 'asd' - }) // eslint-disable-line - t.fail() - } catch (err) { - t.type(err, errors.InvalidArgumentError) - t.equal(err.message, 'invalid keepAliveMaxTimeout') - } - - try { - new Client(new URL('http://localhost:200'), { // eslint-disable-line - keepAliveMaxTimeout: 0 - }) // eslint-disable-line - t.fail() - } catch (err) { - t.type(err, errors.InvalidArgumentError) - t.equal(err.message, 'invalid keepAliveMaxTimeout') - } - - try { - new Client(new URL('http://localhost:200'), { // eslint-disable-line - keepAliveTimeoutThreshold: 'asd' - }) // eslint-disable-line - t.fail() - } catch (err) { - t.type(err, errors.InvalidArgumentError) - t.equal(err.message, 'invalid keepAliveTimeoutThreshold') - } - - try { - new Client({ // eslint-disable-line - protocol: 'asd' - }) - t.fail() - } catch (err) { - t.type(err, errors.InvalidArgumentError) - t.equal(err.message, 'Invalid URL protocol: the URL must start with `http:` or `https:`.') - } - - try { - new Client({ // eslint-disable-line - hostname: 1 - }) - t.fail() - } catch (err) { - t.type(err, errors.InvalidArgumentError) - t.equal(err.message, 'Invalid URL protocol: the URL must start with `http:` or `https:`.') - } - - try { - new Client(new URL('http://localhost:200'), { // eslint-disable-line - maxHeaderSize: 'asd' - }) - t.fail() - } catch (err) { - t.type(err, errors.InvalidArgumentError) - t.equal(err.message, 'invalid maxHeaderSize') - } - - try { - new Client(1) // eslint-disable-line - t.fail() - } catch (err) { - t.type(err, errors.InvalidArgumentError) - t.equal(err.message, 'Invalid URL: The URL argument must be a non-null object.') - } - - try { - const client = new Client(new URL('http://localhost:200')) // eslint-disable-line - client.destroy(null, null) - t.fail() - } catch (err) { - t.type(err, errors.InvalidArgumentError) - t.equal(err.message, 'invalid callback') - } - - try { - const client = new Client(new URL('http://localhost:200')) // eslint-disable-line - client.close(null, null) - t.fail() - } catch (err) { - t.type(err, errors.InvalidArgumentError) - t.equal(err.message, 'invalid callback') - } - - try { - new Client(new URL('http://localhost:200'), { maxKeepAliveTimeout: 1e3 }) // eslint-disable-line - t.fail() - } catch (err) { - t.type(err, errors.InvalidArgumentError) - t.equal(err.message, 'unsupported maxKeepAliveTimeout, use keepAliveMaxTimeout instead') - } - - try { - new Client(new URL('http://localhost:200'), { keepAlive: false }) // eslint-disable-line - t.fail() - } catch (err) { - t.type(err, errors.InvalidArgumentError) - t.equal(err.message, 'unsupported keepAlive, use pipelining=0 instead') - } - - try { - new Client(new URL('http://localhost:200'), { idleTimeout: 30e3 }) // eslint-disable-line - t.fail() - } catch (err) { - t.type(err, errors.InvalidArgumentError) - t.equal(err.message, 'unsupported idleTimeout, use keepAliveTimeout instead') - } - - try { - new Client(new URL('http://localhost:200'), { socketTimeout: 30e3 }) // eslint-disable-line - t.fail() - } catch (err) { - t.type(err, errors.InvalidArgumentError) - t.equal(err.message, 'unsupported socketTimeout, use headersTimeout & bodyTimeout instead') - } - - try { - new Client(new URL('http://localhost:200'), { requestTimeout: 30e3 }) // eslint-disable-line - t.fail() - } catch (err) { - t.type(err, errors.InvalidArgumentError) - t.equal(err.message, 'unsupported requestTimeout, use headersTimeout & bodyTimeout instead') - } - - try { - new Client(new URL('http://localhost:200'), { connectTimeout: -1 }) // eslint-disable-line - t.fail() - } catch (err) { - t.type(err, errors.InvalidArgumentError) - t.equal(err.message, 'invalid connectTimeout') - } - - try { - new Client(new URL('http://localhost:200'), { connectTimeout: Infinity }) // eslint-disable-line - t.fail() - } catch (err) { - t.type(err, errors.InvalidArgumentError) - t.equal(err.message, 'invalid connectTimeout') - } - - try { - new Client(new URL('http://localhost:200'), { connectTimeout: 'asd' }) // eslint-disable-line - t.fail() - } catch (err) { - t.type(err, errors.InvalidArgumentError) - t.equal(err.message, 'invalid connectTimeout') - } - - try { - new Client(new URL('http://localhost:200'), { connect: 'asd' }) // eslint-disable-line - t.fail() - } catch (err) { - t.type(err, errors.InvalidArgumentError) - t.equal(err.message, 'connect must be a function or an object') - } - - try { - new Client(new URL('http://localhost:200'), { connect: -1 }) // eslint-disable-line - t.fail() - } catch (err) { - t.type(err, errors.InvalidArgumentError) - t.equal(err.message, 'connect must be a function or an object') - } - - try { - new Pool(new URL('http://localhost:200'), { connect: 'asd' }) // eslint-disable-line - t.fail() - } catch (err) { - t.type(err, errors.InvalidArgumentError) - t.equal(err.message, 'connect must be a function or an object') - } - - try { - new Pool(new URL('http://localhost:200'), { connect: -1 }) // eslint-disable-line - t.fail() - } catch (err) { - t.type(err, errors.InvalidArgumentError) - t.equal(err.message, 'connect must be a function or an object') - } - - try { - new Client(new URL('http://localhost:200'), { maxCachedSessions: -10 }) // eslint-disable-line - t.fail() - } catch (err) { - t.type(err, errors.InvalidArgumentError) - t.equal(err.message, 'maxCachedSessions must be a positive integer or zero') - } - - try { - new Client(new URL('http://localhost:200'), { maxCachedSessions: 'foo' }) // eslint-disable-line - t.fail() - } catch (err) { - t.type(err, errors.InvalidArgumentError) - t.equal(err.message, 'maxCachedSessions must be a positive integer or zero') - } - - try { - new Client(new URL('http://localhost:200'), { maxRequestsPerClient: 'foo' }) // eslint-disable-line - t.fail() - } catch (err) { - t.type(err, errors.InvalidArgumentError) - t.equal(err.message, 'maxRequestsPerClient must be a positive number') - } - - try { - new Client(new URL('http://localhost:200'), { autoSelectFamilyAttemptTimeout: 'foo' }) // eslint-disable-line - t.fail() - } catch (err) { - t.type(err, errors.InvalidArgumentError) - t.equal(err.message, 'autoSelectFamilyAttemptTimeout must be a positive number') - } - - t.end() -}) - -test('POST which fails should error response', (t) => { - t.plan(6) - - const server = createServer() - server.on('request', (req, res) => { - req.once('data', () => { - res.destroy() - }) - }) - t.teardown(server.close.bind(server)) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) - - function checkError (err) { - // Different platforms error with different codes... - t.ok( - err.code === 'EPIPE' || - err.code === 'ECONNRESET' || - err.code === 'UND_ERR_SOCKET' || - err.message === 'other side closed' - ) - } - - { - const body = new Readable({ read () {} }) - body.push('asd') - body.on('error', (err) => { - checkError(err) - }) - - client.request({ - path: '/', - method: 'POST', - body - }, (err) => { - checkError(err) - }) - } - - { - const body = new Readable({ read () {} }) - body.push('asd') - body.on('error', (err) => { - checkError(err) - }) - - client.request({ - path: '/', - method: 'POST', - headers: { - 'content-length': 100 - }, - body - }, (err) => { - checkError(err) - }) - } - - { - const body = wrapWithAsyncIterable(['asd'], true) - - client.request({ - path: '/', - method: 'POST', - body - }, (err) => { - checkError(err) - }) - } - - { - const body = wrapWithAsyncIterable(['asd'], true) - - client.request({ - path: '/', - method: 'POST', - headers: { - 'content-length': 100 - }, - body - }, (err) => { - checkError(err) - }) - } - }) -}) - -test('client destroy cleanup', (t) => { - t.plan(3) - - const _err = new Error('kaboom') - let client - const server = createServer() - server.once('request', (req, res) => { - req.once('data', () => { - client.destroy(_err, (err) => { - t.error(err) - }) - }) - }) - t.teardown(server.close.bind(server)) - - server.listen(0, () => { - client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) - - const body = new Readable({ read () {} }) - body.push('asd') - body.on('error', (err) => { - t.equal(err, _err) - }) - - client.request({ - path: '/', - method: 'POST', - body - }, (err, data) => { - t.equal(err, _err) - }) - }) -}) - -test('throwing async-iterator causes error', (t) => { - t.plan(1) - - const server = createServer((req, res) => { - res.end(Buffer.alloc(4 + 1, 'a')) - }) - 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({ - method: 'POST', - path: '/', - body: (async function * () { - yield 'hello' - throw new IteratorError('bad iterator') - })() - }, (err) => { - t.type(err, IteratorError) - }) - }) -}) - -test('client async-iterator destroy cleanup', (t) => { - t.plan(2) - - const _err = new Error('kaboom') - let client - const server = createServer() - server.once('request', (req, res) => { - req.once('data', () => { - client.destroy(_err, (err) => { - t.error(err) - }) - }) - }) - t.teardown(server.close.bind(server)) - - server.listen(0, () => { - client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) - - const body = wrapWithAsyncIterable(['asd'], true) - - client.request({ - path: '/', - method: 'POST', - body - }, (err, data) => { - t.equal(err, _err) - }) - }) -}) - -test('GET errors body', (t) => { - t.plan(2) - - const server = createServer() - server.once('request', (req, res) => { - res.write('asd') - setTimeout(() => { - res.destroy() - }, 19) - }) - 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' }, (err, { statusCode, headers, body }) => { - t.error(err) - body.resume() - body.on('error', err => ( - t.ok(err) - )) - }) - }) -}) - -test('validate request body', (t) => { - t.plan(6) - - 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.close.bind(client)) - - client.request({ - path: '/', - method: 'POST', - body: /asdasd/ - }, (err, data) => { - t.type(err, errors.InvalidArgumentError) - }) - - client.request({ - path: '/', - method: 'POST', - body: 0 - }, (err, data) => { - t.type(err, errors.InvalidArgumentError) - }) - - client.request({ - path: '/', - method: 'POST', - body: false - }, (err, data) => { - t.type(err, errors.InvalidArgumentError) - }) - - client.request({ - path: '/', - method: 'POST', - body: '' - }, (err, data) => { - t.error(err) - data.body.resume() - }) - - client.request({ - path: '/', - method: 'POST', - body: new Uint8Array() - }, (err, data) => { - t.error(err) - data.body.resume() - }) - - client.request({ - path: '/', - method: 'POST', - body: Buffer.alloc(10) - }, (err, data) => { - t.error(err) - data.body.resume() - }) - }) -}) +// TODO: move to test/node-test/client-connect.js test('parser error', (t) => { t.plan(2) @@ -836,450 +26,3 @@ test('parser error', (t) => { }) }) }) - -function socketFailWrite (type) { - test(`socket fail while writing ${type} request body`, (t) => { - t.plan(2) - - const server = createServer() - server.once('request', (req, res) => { - }) - t.teardown(server.close.bind(server)) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) - - const preBody = new Readable({ read () {} }) - preBody.push('asd') - const body = maybeWrapStream(preBody, type) - client.on('connect', () => { - process.nextTick(() => { - client[kSocket].destroy('kaboom') - }) - }) - - client.request({ - path: '/', - method: 'POST', - body - }, (err) => { - t.ok(err) - }) - client.close((err) => { - t.error(err) - }) - }) - }) -} -socketFailWrite(consts.STREAM) -socketFailWrite(consts.ASYNC_ITERATOR) - -function socketFailEndWrite (type) { - test(`socket fail while ending ${type} request body`, (t) => { - t.plan(3) - - const server = createServer() - server.once('request', (req, res) => { - res.end() - }) - t.teardown(server.close.bind(server)) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`, { - pipelining: 2 - }) - t.teardown(client.destroy.bind(client)) - - const _err = new Error('kaboom') - client.on('connect', () => { - process.nextTick(() => { - client[kSocket].destroy(_err) - }) - }) - const preBody = new Readable({ read () {} }) - preBody.push(null) - const body = maybeWrapStream(preBody, type) - - client.request({ - path: '/', - method: 'POST', - body - }, (err) => { - t.equal(err, _err) - }) - client.close((err) => { - t.error(err) - client.close((err) => { - t.type(err, errors.ClientDestroyedError) - }) - }) - }) - }) -} - -socketFailEndWrite(consts.STREAM) -socketFailEndWrite(consts.ASYNC_ITERATOR) - -test('queued request should not fail on socket destroy', (t) => { - t.plan(4) - - const server = createServer() - server.on('request', (req, res) => { - res.end() - }) - t.teardown(server.close.bind(server)) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`, { - pipelining: 1 - }) - t.teardown(client.destroy.bind(client)) - - client.request({ - path: '/', - method: 'GET' - }, (err, data) => { - t.error(err) - data.body.resume().on('error', () => { - t.pass() - }) - client[kSocket].destroy() - client.request({ - path: '/', - method: 'GET' - }, (err, data) => { - t.error(err) - data.body.resume().on('end', () => { - t.pass() - }) - }) - }) - }) -}) - -test('queued request should fail on client destroy', (t) => { - t.plan(6) - - const server = createServer() - server.on('request', (req, res) => { - res.end() - }) - t.teardown(server.close.bind(server)) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`, { - pipelining: 1 - }) - t.teardown(client.destroy.bind(client)) - - let requestErrored = false - client.request({ - path: '/', - method: 'GET' - }, (err, data) => { - t.error(err) - data.body.resume() - .on('error', () => { - t.pass() - }) - client.destroy((err) => { - t.error(err) - t.equal(requestErrored, true) - }) - }) - client.request({ - path: '/', - method: 'GET', - opaque: 'asd' - }, (err, data) => { - requestErrored = true - t.ok(err) - t.equal(data.opaque, 'asd') - }) - }) -}) - -test('retry idempotent inflight', (t) => { - t.plan(3) - - const server = createServer() - server.on('request', (req, res) => { - res.end() - }) - t.teardown(server.close.bind(server)) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`, { - pipelining: 3 - }) - t.teardown(client.close.bind(client)) - - client.request({ - path: '/', - method: 'POST', - body: new Readable({ - read () { - this.destroy(new Error('kaboom')) - } - }) - }, (err) => { - t.ok(err) - }) - 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() - }) - }) -}) - -test('invalid opts', (t) => { - t.plan(2) - - const client = new Client('http://localhost:5000') - client.request(null, (err) => { - t.type(err, errors.InvalidArgumentError) - }) - client.pipeline(null).on('error', (err) => { - t.type(err, errors.InvalidArgumentError) - }) -}) - -test('default port for http and https', (t) => { - t.plan(4) - - try { - new Client(new URL('http://localhost:80')) // eslint-disable-line - t.pass('Should not throw') - } catch (err) { - t.fail(err) - } - - try { - new Client(new URL('http://localhost')) // eslint-disable-line - t.pass('Should not throw') - } catch (err) { - t.fail(err) - } - - try { - new Client(new URL('https://localhost:443')) // eslint-disable-line - t.pass('Should not throw') - } catch (err) { - t.fail(err) - } - - try { - new Client(new URL('https://localhost')) // eslint-disable-line - t.pass('Should not throw') - } catch (err) { - t.fail(err) - } -}) - -test('CONNECT throws in next tick', (t) => { - t.plan(3) - - const server = createServer() - server.on('request', (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.destroy.bind(client)) - - client.request({ - path: '/', - method: 'GET' - }, (err, data) => { - t.error(err) - data.body - .on('end', () => { - let ticked = false - client.request({ - path: '/', - method: 'CONNECT' - }, (err) => { - t.ok(err) - t.strictSame(ticked, true) - }) - ticked = true - }) - .resume() - }) - }) -}) - -test('invalid signal', (t) => { - t.plan(8) - - const client = new Client('http://localhost:3333') - t.teardown(client.destroy.bind(client)) - - let ticked = false - client.request({ path: '/', method: 'GET', signal: {}, opaque: 'asd' }, (err, { opaque }) => { - t.equal(ticked, true) - t.equal(opaque, 'asd') - t.type(err, errors.InvalidArgumentError) - }) - client.pipeline({ path: '/', method: 'GET', signal: {} }, () => {}) - .on('error', (err) => { - t.equal(ticked, true) - t.type(err, errors.InvalidArgumentError) - }) - client.stream({ path: '/', method: 'GET', signal: {}, opaque: 'asd' }, () => {}, (err, { opaque }) => { - t.equal(ticked, true) - t.equal(opaque, 'asd') - t.type(err, errors.InvalidArgumentError) - }) - ticked = true -}) - -test('invalid body chunk does not crash', (t) => { - t.plan(1) - - const server = createServer() - server.on('request', (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.destroy.bind(client)) - - client.request({ - path: '/', - body: new Readable({ - objectMode: true, - read () { - this.push({}) - } - }), - method: 'GET' - }, (err) => { - t.equal(err.code, 'ERR_INVALID_ARG_TYPE') - }) - }) -}) - -test('socket errors', t => { - t.plan(2) - const client = new Client('http://localhost:5554') - t.teardown(client.destroy.bind(client)) - - client.request({ path: '/', method: 'GET' }, (err, data) => { - t.ok(err) - // TODO: Why UND_ERR_SOCKET? - t.ok(err.code === 'ECONNREFUSED' || err.code === 'UND_ERR_SOCKET', err.code) - t.end() - }) -}) - -test('headers overflow', t => { - t.plan(2) - const server = createServer() - server.on('request', (req, res) => { - res.writeHead(200, { - 'x-test-1': '1', - 'x-test-2': '2' - }) - res.end() - }) - t.teardown(server.close.bind(server)) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`, { - maxHeaderSize: 10 - }) - t.teardown(client.destroy.bind(client)) - - client.request({ path: '/', method: 'GET' }, (err, data) => { - t.ok(err) - t.equal(err.code, 'UND_ERR_HEADERS_OVERFLOW') - t.end() - }) - }) -}) - -test('SocketError should expose socket details (net)', (t) => { - t.plan(8) - - const server = createServer() - - server.once('request', (req, res) => { - res.destroy() - }) - 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' }, (err, data) => { - t.ok(err instanceof errors.SocketError) - if (err.socket.remoteFamily === 'IPv4') { - t.equal(err.socket.remoteFamily, 'IPv4') - t.equal(err.socket.localAddress, '127.0.0.1') - t.equal(err.socket.remoteAddress, '127.0.0.1') - } else { - t.equal(err.socket.remoteFamily, 'IPv6') - t.equal(err.socket.localAddress, '::1') - t.equal(err.socket.remoteAddress, '::1') - } - t.type(err.socket.localPort, 'number') - t.type(err.socket.remotePort, 'number') - t.type(err.socket.bytesWritten, 'number') - t.type(err.socket.bytesRead, 'number') - }) - }) -}) - -test('SocketError should expose socket details (tls)', (t) => { - t.plan(8) - - const server = https.createServer(pem) - - server.once('request', (req, res) => { - res.destroy() - }) - t.teardown(server.close.bind(server)) - - server.listen(0, () => { - const client = new Client(`https://localhost:${server.address().port}`, { - tls: { - rejectUnauthorized: false - } - }) - t.teardown(client.destroy.bind(client)) - - client.request({ path: '/', method: 'GET' }, (err, data) => { - t.ok(err instanceof errors.SocketError) - if (err.socket.remoteFamily === 'IPv4') { - t.equal(err.socket.remoteFamily, 'IPv4') - t.equal(err.socket.localAddress, '127.0.0.1') - t.equal(err.socket.remoteAddress, '127.0.0.1') - } else { - t.equal(err.socket.remoteFamily, 'IPv6') - t.equal(err.socket.localAddress, '::1') - t.equal(err.socket.remoteAddress, '::1') - } - t.type(err.socket.localPort, 'number') - t.type(err.socket.remotePort, 'number') - t.type(err.socket.bytesWritten, 'number') - t.type(err.socket.bytesRead, 'number') - }) - }) -}) diff --git a/test/node-test/async_hooks.js b/test/node-test/async_hooks.js index 85970e1e1fe..e670179fec6 100644 --- a/test/node-test/async_hooks.js +++ b/test/node-test/async_hooks.js @@ -194,7 +194,7 @@ test('async hooks pipeline handler', async (t) => { server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.after(() => client.close.bind(client)()) + t.after(() => { return client.close() }) setCurrentTransaction({ hello: 'world2' }) diff --git a/test/node-test/client-connect.js b/test/node-test/client-connect.js new file mode 100644 index 00000000000..7de9d2e39e3 --- /dev/null +++ b/test/node-test/client-connect.js @@ -0,0 +1,293 @@ +'use strict' + +const { test } = require('node:test') +const { Client, errors } = require('../..') +const http = require('http') +const EE = require('events') +const { kBusy } = require('../../lib/core/symbols') +const { tspl } = require('@matteo.collina/tspl') +const { closeServerAsPromise } = require('../utils/node-http') + +test('basic connect', async (t) => { + const p = tspl(t, { plan: 3 }) + + const server = http.createServer((c) => { + p.ok(0) + }) + server.on('connect', (req, socket, firstBodyChunk) => { + socket.write('HTTP/1.1 200 Connection established\r\n\r\n') + + let data = firstBodyChunk.toString() + socket.on('data', (buf) => { + data += buf.toString() + }) + + socket.on('end', () => { + socket.end(data) + }) + }) + t.after(closeServerAsPromise(server)) + + server.listen(0, async () => { + const client = new Client(`http://localhost:${server.address().port}`) + t.after(() => { return client.close() }) + + const signal = new EE() + const promise = client.connect({ + signal, + path: '/' + }) + p.strictEqual(signal.listenerCount('abort'), 1) + const { socket } = await promise + p.strictEqual(signal.listenerCount('abort'), 0) + + let recvData = '' + socket.on('data', (d) => { + recvData += d + }) + + socket.on('end', () => { + p.strictEqual(recvData.toString(), 'Body') + }) + + socket.write('Body') + socket.end() + }) + + await p.completed +}) + +test('connect error', async (t) => { + const p = tspl(t, { plan: 1 }) + + const server = http.createServer((c) => { + p.ok(0) + }) + server.on('connect', (req, socket, firstBodyChunk) => { + socket.destroy() + }) + t.after(closeServerAsPromise(server)) + + server.listen(0, async () => { + const client = new Client(`http://localhost:${server.address().port}`) + t.after(() => { return client.close() }) + + try { + await client.connect({ + path: '/' + }) + } catch (err) { + p.ok(err) + } + }) + + await p.completed +}) + +test('connect invalid opts', (t) => { + const p = tspl(t, { plan: 6 }) + + const client = new Client('http://localhost:5432') + + client.connect(null, err => { + p.ok(err instanceof errors.InvalidArgumentError) + p.strictEqual(err.message, 'invalid opts') + }) + + try { + client.connect(null, null) + p.ok(0) + } catch (err) { + p.ok(err instanceof errors.InvalidArgumentError) + p.strictEqual(err.message, 'invalid opts') + } + + try { + client.connect({ path: '/' }, null) + p.ok(0) + } catch (err) { + p.ok(err instanceof errors.InvalidArgumentError) + p.strictEqual(err.message, 'invalid callback') + } +}) + +test('connect wait for empty pipeline', async (t) => { + const p = tspl(t, { plan: 7 }) + + let canConnect = false + const server = http.createServer((req, res) => { + res.end() + canConnect = true + }) + server.on('connect', (req, socket, firstBodyChunk) => { + p.strictEqual(canConnect, true) + socket.write('HTTP/1.1 200 Connection established\r\n\r\n') + + let data = firstBodyChunk.toString() + socket.on('data', (buf) => { + data += buf.toString() + }) + + socket.on('end', () => { + socket.end(data) + }) + }) + t.after(closeServerAsPromise(server)) + + server.listen(0, async () => { + const client = new Client(`http://localhost:${server.address().port}`, { + pipelining: 3 + }) + t.after(() => { return client.close() }) + + client.request({ + path: '/', + method: 'GET' + }, (err) => { + p.ifError(err) + }) + client.once('connect', () => { + process.nextTick(() => { + p.strictEqual(client[kBusy], false) + + client.connect({ + path: '/' + }, (err, { socket }) => { + p.ifError(err) + let recvData = '' + socket.on('data', (d) => { + recvData += d + }) + + socket.on('end', () => { + p.strictEqual(recvData.toString(), 'Body') + }) + + socket.write('Body') + socket.end() + }) + p.strictEqual(client[kBusy], true) + + client.request({ + path: '/', + method: 'GET' + }, (err) => { + p.ifError(err) + }) + }) + }) + }) + await p.completed +}) + +test('connect aborted', async (t) => { + const p = tspl(t, { plan: 6 }) + + const server = http.createServer((req, res) => { + p.ok(0) + }) + server.on('connect', (req, c, firstBodyChunk) => { + p.ok(0) + }) + t.after(closeServerAsPromise(server)) + + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`, { + pipelining: 3 + }) + t.after(() => { + client.destroy() + }) + + const signal = new EE() + client.connect({ + path: '/', + signal, + opaque: 'asd' + }, (err, { opaque }) => { + p.strictEqual(opaque, 'asd') + p.strictEqual(signal.listenerCount('abort'), 0) + p.ok(err instanceof errors.RequestAbortedError) + }) + p.strictEqual(client[kBusy], true) + p.strictEqual(signal.listenerCount('abort'), 1) + signal.emit('abort') + + client.close(() => { + p.ok(1) + }) + }) + + await p.completed +}) + +test('basic connect error', async (t) => { + const p = tspl(t, { plan: 2 }) + + const server = http.createServer((c) => { + p.ok(0) + }) + server.on('connect', (req, socket, firstBodyChunk) => { + socket.write('HTTP/1.1 200 Connection established\r\n\r\n') + + let data = firstBodyChunk.toString() + socket.on('data', (buf) => { + data += buf.toString() + }) + + socket.on('end', () => { + socket.end(data) + }) + }) + t.after(closeServerAsPromise(server)) + + server.listen(0, async () => { + const client = new Client(`http://localhost:${server.address().port}`) + t.after(() => { return client.close() }) + + const _err = new Error() + client.connect({ + path: '/' + }, (err, { socket }) => { + p.ifError(err) + socket.on('error', (err) => { + p.strictEqual(err, _err) + }) + throw _err + }) + }) + + await p.completed +}) + +test('connect invalid signal', async (t) => { + const p = tspl(t, { plan: 2 }) + + const server = http.createServer((req, res) => { + p.ok(0) + }) + server.on('connect', (req, c, firstBodyChunk) => { + p.ok(0) + }) + t.after(closeServerAsPromise(server)) + + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`) + t.after(client.destroy.bind(client)) + + client.on('disconnect', () => { + p.ok(0) + }) + + client.connect({ + path: '/', + signal: 'error', + opaque: 'asd' + }, (err, { opaque }) => { + p.strictEqual(opaque, 'asd') + p.ok(err instanceof errors.InvalidArgumentError) + }) + }) + + await p.completed +}) diff --git a/test/client-dispatch.js b/test/node-test/client-dispatch.js similarity index 60% rename from test/client-dispatch.js rename to test/node-test/client-dispatch.js index 781118cc058..42fbf91b412 100644 --- a/test/client-dispatch.js +++ b/test/node-test/client-dispatch.js @@ -1,14 +1,17 @@ 'use strict' -const { test } = require('tap') +const { test } = require('node:test') +const assert = require('node:assert/strict') const http = require('http') -const { Client, Pool, errors } = require('..') +const { Client, Pool, errors } = require('../..') const stream = require('stream') const { createSecureServer } = require('node:http2') const pem = require('https-pem') +const { tspl } = require('@matteo.collina/tspl') +const { closeServerAsPromise, closeClientAndServerAsPromise } = require('../utils/node-http') test('dispatch invalid opts', (t) => { - t.plan(14) + const p = tspl(t, { plan: 14 }) const client = new Client('http://localhost:5000') @@ -19,8 +22,8 @@ test('dispatch invalid opts', (t) => { upgrade: 1 }, null) } catch (err) { - t.type(err, errors.InvalidArgumentError) - t.equal(err.message, 'handler must be an object') + p.ok(err instanceof errors.InvalidArgumentError) + p.strictEqual(err.message, 'handler must be an object') } try { @@ -30,8 +33,8 @@ test('dispatch invalid opts', (t) => { upgrade: 1 }, 'asd') } catch (err) { - t.type(err, errors.InvalidArgumentError) - t.equal(err.message, 'handler must be an object') + p.ok(err instanceof errors.InvalidArgumentError) + p.strictEqual(err.message, 'handler must be an object') } client.dispatch({ @@ -40,8 +43,8 @@ test('dispatch invalid opts', (t) => { upgrade: 1 }, { onError (err) { - t.type(err, errors.InvalidArgumentError) - t.equal(err.message, 'upgrade must be a string') + p.ok(err instanceof errors.InvalidArgumentError) + p.strictEqual(err.message, 'upgrade must be a string') } }) @@ -51,8 +54,8 @@ test('dispatch invalid opts', (t) => { headersTimeout: 'asd' }, { onError (err) { - t.type(err, errors.InvalidArgumentError) - t.equal(err.message, 'invalid headersTimeout') + p.ok(err instanceof errors.InvalidArgumentError) + p.strictEqual(err.message, 'invalid headersTimeout') } }) @@ -62,8 +65,8 @@ test('dispatch invalid opts', (t) => { bodyTimeout: 'asd' }, { onError (err) { - t.type(err, errors.InvalidArgumentError) - t.equal(err.message, 'invalid bodyTimeout') + p.ok(err instanceof errors.InvalidArgumentError) + p.strictEqual(err.message, 'invalid bodyTimeout') } }) @@ -74,33 +77,33 @@ test('dispatch invalid opts', (t) => { bodyTimeout: 'asd' }, { onError (err) { - t.type(err, errors.InvalidArgumentError) - t.equal(err.message, 'invalid bodyTimeout') + p.ok(err instanceof errors.InvalidArgumentError) + p.strictEqual(err.message, 'invalid bodyTimeout') } }) client.dispatch(null, { onError (err) { - t.type(err, errors.InvalidArgumentError) - t.equal(err.message, 'opts must be an object.') + p.ok(err instanceof errors.InvalidArgumentError) + p.strictEqual(err.message, 'opts must be an object.') } }) }) -test('basic dispatch get', (t) => { - t.plan(11) +test('basic dispatch get', async (t) => { + const p = tspl(t, { plan: 11 }) const server = http.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('', req.headers.baz) - t.equal(undefined, req.headers['content-length']) + p.strictEqual('/', req.url) + p.strictEqual('GET', req.method) + p.strictEqual(`localhost:${server.address().port}`, req.headers.host) + p.strictEqual(undefined, req.headers.foo) + p.strictEqual('bar', req.headers.bar) + p.strictEqual('', req.headers.baz) + p.strictEqual(undefined, req.headers['content-length']) res.end('hello') }) - t.teardown(server.close.bind(server)) + t.after(closeServerAsPromise(server)) const reqHeaders = { foo: undefined, @@ -110,7 +113,7 @@ test('basic dispatch get', (t) => { server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + t.after(() => { return client.close() }) const bufs = [] client.dispatch({ @@ -121,39 +124,41 @@ test('basic dispatch get', (t) => { onConnect () { }, onHeaders (statusCode, headers) { - t.equal(statusCode, 200) - t.equal(Array.isArray(headers), true) + p.strictEqual(statusCode, 200) + p.strictEqual(Array.isArray(headers), true) }, onData (buf) { bufs.push(buf) }, onComplete (trailers) { - t.same(trailers, []) - t.equal('hello', Buffer.concat(bufs).toString('utf8')) + p.deepStrictEqual(trailers, []) + p.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) }, onError () { - t.fail() + p.ok(0) } }) }) + + await p.completed }) -test('trailers dispatch get', (t) => { - t.plan(12) +test('trailers dispatch get', async (t) => { + const p = tspl(t, { plan: 12 }) const server = http.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']) + p.strictEqual('/', req.url) + p.strictEqual('GET', req.method) + p.strictEqual(`localhost:${server.address().port}`, req.headers.host) + p.strictEqual(undefined, req.headers.foo) + p.strictEqual('bar', req.headers.bar) + p.strictEqual(undefined, req.headers['content-length']) res.addTrailers({ 'Content-MD5': 'test' }) res.setHeader('Content-Type', 'text/plain') res.setHeader('Trailer', 'Content-MD5') res.end('hello') }) - t.teardown(server.close.bind(server)) + t.after(closeServerAsPromise(server)) const reqHeaders = { foo: undefined, @@ -162,7 +167,7 @@ test('trailers dispatch get', (t) => { server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + t.after(() => { return client.close() }) const bufs = [] client.dispatch({ @@ -173,42 +178,44 @@ test('trailers dispatch get', (t) => { onConnect () { }, onHeaders (statusCode, headers) { - t.equal(statusCode, 200) - t.equal(Array.isArray(headers), true) + p.strictEqual(statusCode, 200) + p.strictEqual(Array.isArray(headers), true) { const contentTypeIdx = headers.findIndex(x => x.toString() === 'Content-Type') - t.equal(headers[contentTypeIdx + 1].toString(), 'text/plain') + p.strictEqual(headers[contentTypeIdx + 1].toString(), 'text/plain') } }, onData (buf) { bufs.push(buf) }, onComplete (trailers) { - t.equal(Array.isArray(trailers), true) + p.strictEqual(Array.isArray(trailers), true) { const contentMD5Idx = trailers.findIndex(x => x.toString() === 'Content-MD5') - t.equal(trailers[contentMD5Idx + 1].toString(), 'test') + p.strictEqual(trailers[contentMD5Idx + 1].toString(), 'test') } - t.equal('hello', Buffer.concat(bufs).toString('utf8')) + p.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) }, onError () { - t.fail() + p.ok(0) } }) }) + + await p.completed }) -test('dispatch onHeaders error', (t) => { - t.plan(1) +test('dispatch onHeaders error', async (t) => { + const p = tspl(t, { plan: 1 }) const server = http.createServer((req, res) => { res.end() }) - t.teardown(server.close.bind(server)) + t.after(closeServerAsPromise(server)) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + t.after(() => { return client.close() }) const _err = new Error() client.dispatch({ @@ -221,29 +228,31 @@ test('dispatch onHeaders error', (t) => { throw _err }, onData (buf) { - t.fail() + p.ok(0) }, onComplete (trailers) { - t.fail() + p.ok(0) }, onError (err) { - t.equal(err, _err) + p.strictEqual(err, _err) } }) }) + + await p.completed }) -test('dispatch onComplete error', (t) => { - t.plan(2) +test('dispatch onComplete error', async (t) => { + const p = tspl(t, { plan: 2 }) const server = http.createServer((req, res) => { res.end() }) - t.teardown(server.close.bind(server)) + t.after(closeServerAsPromise(server)) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + t.after(() => { return client.close() }) const _err = new Error() client.dispatch({ @@ -253,32 +262,34 @@ test('dispatch onComplete error', (t) => { onConnect () { }, onHeaders (statusCode, headers) { - t.pass() + p.ok(1) }, onData (buf) { - t.fail() + p.ok(0) }, onComplete (trailers) { throw _err }, onError (err) { - t.equal(err, _err) + p.strictEqual(err, _err) } }) }) + + await p.completed }) -test('dispatch onData error', (t) => { - t.plan(2) +test('dispatch onData error', async (t) => { + const p = tspl(t, { plan: 2 }) const server = http.createServer((req, res) => { res.end('ad') }) - t.teardown(server.close.bind(server)) + t.after(closeServerAsPromise(server)) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + t.after(() => { return client.close() }) const _err = new Error() client.dispatch({ @@ -288,32 +299,34 @@ test('dispatch onData error', (t) => { onConnect () { }, onHeaders (statusCode, headers) { - t.pass() + p.ok(1) }, onData (buf) { throw _err }, onComplete (trailers) { - t.fail() + p.ok(0) }, onError (err) { - t.equal(err, _err) + p.strictEqual(err, _err) } }) }) + + await p.completed }) -test('dispatch onConnect error', (t) => { - t.plan(1) +test('dispatch onConnect error', async (t) => { + const p = tspl(t, { plan: 1 }) const server = http.createServer((req, res) => { res.end('ad') }) - t.teardown(server.close.bind(server)) + t.after(closeServerAsPromise(server)) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + t.after(() => { return client.close() }) const _err = new Error() client.dispatch({ @@ -324,26 +337,28 @@ test('dispatch onConnect error', (t) => { throw _err }, onHeaders (statusCode, headers) { - t.fail() + p.ok(0) }, onData (buf) { - t.fail() + p.ok(0) }, onComplete (trailers) { - t.fail() + p.ok(0) }, onError (err) { - t.equal(err, _err) + p.strictEqual(err, _err) } }) }) + + await p.completed }) -test('connect call onUpgrade once', (t) => { - t.plan(2) +test('connect call onUpgrade once', async (t) => { + const p = tspl(t, { plan: 2 }) const server = http.createServer((c) => { - t.fail() + p.ok(0) }) server.on('connect', (req, socket, firstBodyChunk) => { socket.write('HTTP/1.1 200 Connection established\r\n\r\n') @@ -357,11 +372,11 @@ test('connect call onUpgrade once', (t) => { socket.end(data) }) }) - t.teardown(server.close.bind(server)) + t.after(closeServerAsPromise(server)) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + t.after(() => { return client.close() }) let recvData = '' let count = 0 @@ -375,43 +390,45 @@ test('connect call onUpgrade once', (t) => { t.pass('should not throw') }, onUpgrade (statusCode, headers, socket) { - t.equal(count++, 0) + p.strictEqual(count++, 0) socket.on('data', (d) => { recvData += d }) socket.on('end', () => { - t.equal(recvData.toString(), 'Body') + p.strictEqual(recvData.toString(), 'Body') }) socket.write('Body') socket.end() }, onData (buf) { - t.fail() + p.ok(0) }, onComplete (trailers) { - t.fail() + p.ok(0) }, onError () { - t.fail() + p.ok(0) } }) }) + + await p.completed }) -test('dispatch onConnect missing', (t) => { - t.plan(1) +test('dispatch onConnect missing', async (t) => { + const p = tspl(t, { plan: 1 }) const server = http.createServer((req, res) => { res.end('ad') }) - t.teardown(server.close.bind(server)) + t.after(closeServerAsPromise(server)) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + t.after(() => { return client.close() }) client.dispatch({ path: '/', @@ -427,23 +444,25 @@ test('dispatch onConnect missing', (t) => { t.pass('should not throw') }, onError (err) { - t.equal(err.code, 'UND_ERR_INVALID_ARG') + p.strictEqual(err.code, 'UND_ERR_INVALID_ARG') } }) }) + + await p.completed }) -test('dispatch onHeaders missing', (t) => { - t.plan(1) +test('dispatch onHeaders missing', async (t) => { + const p = tspl(t, { plan: 1 }) const server = http.createServer((req, res) => { res.end('ad') }) - t.teardown(server.close.bind(server)) + t.after(closeServerAsPromise(server)) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + t.after(() => { return client.close() }) client.dispatch({ path: '/', @@ -452,29 +471,31 @@ test('dispatch onHeaders missing', (t) => { onConnect () { }, onData (buf) { - t.fail('should not throw') + p.ok(0, 'should not throw') }, onComplete (trailers) { - t.fail('should not throw') + p.ok(0, 'should not throw') }, onError (err) { - t.equal(err.code, 'UND_ERR_INVALID_ARG') + p.strictEqual(err.code, 'UND_ERR_INVALID_ARG') } }) }) + + await p.completed }) -test('dispatch onData missing', (t) => { - t.plan(1) +test('dispatch onData missing', async (t) => { + const p = tspl(t, { plan: 1 }) const server = http.createServer((req, res) => { res.end('ad') }) - t.teardown(server.close.bind(server)) + t.after(closeServerAsPromise(server)) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + t.after(() => { return client.close() }) client.dispatch({ path: '/', @@ -483,29 +504,31 @@ test('dispatch onData missing', (t) => { onConnect () { }, onHeaders (statusCode, headers) { - t.fail('should not throw') + p.ok(0, 'should not throw') }, onComplete (trailers) { - t.fail('should not throw') + p.ok(0, 'should not throw') }, onError (err) { - t.equal(err.code, 'UND_ERR_INVALID_ARG') + p.strictEqual(err.code, 'UND_ERR_INVALID_ARG') } }) }) + + await p.completed }) -test('dispatch onComplete missing', (t) => { - t.plan(1) +test('dispatch onComplete missing', async (t) => { + const p = tspl(t, { plan: 1 }) const server = http.createServer((req, res) => { res.end('ad') }) - t.teardown(server.close.bind(server)) + t.after(closeServerAsPromise(server)) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + t.after(() => { return client.close() }) client.dispatch({ path: '/', @@ -514,29 +537,31 @@ test('dispatch onComplete missing', (t) => { onConnect () { }, onHeaders (statusCode, headers) { - t.fail() + p.ok(0) }, onData (buf) { - t.fail() + p.ok(0) }, onError (err) { - t.equal(err.code, 'UND_ERR_INVALID_ARG') + p.strictEqual(err.code, 'UND_ERR_INVALID_ARG') } }) }) + + await p.completed }) -test('dispatch onError missing', (t) => { - t.plan(1) +test('dispatch onError missing', async (t) => { + const p = tspl(t, { plan: 1 }) const server = http.createServer((req, res) => { res.end('ad') }) - t.teardown(server.close.bind(server)) + t.after(closeServerAsPromise(server)) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + t.after(() => { return client.close() }) try { client.dispatch({ @@ -546,32 +571,34 @@ test('dispatch onError missing', (t) => { onConnect () { }, onHeaders (statusCode, headers) { - t.fail() + p.ok(0) }, onData (buf) { - t.fail() + p.ok(0) }, onComplete (trailers) { - t.fail() + p.ok(0) } }) } catch (err) { - t.equal(err.code, 'UND_ERR_INVALID_ARG') + p.strictEqual(err.code, 'UND_ERR_INVALID_ARG') } }) + + await p.completed }) -test('dispatch CONNECT onUpgrade missing', (t) => { - t.plan(2) +test('dispatch CONNECT onUpgrade missing', async (t) => { + const p = tspl(t, { plan: 2 }) const server = http.createServer((req, res) => { res.end('ad') }) - t.teardown(server.close.bind(server)) + t.after(closeServerAsPromise(server)) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + t.after(() => client.destroy.bind(client)()) client.dispatch({ path: '/', @@ -583,24 +610,26 @@ test('dispatch CONNECT onUpgrade missing', (t) => { onHeaders (statusCode, headers) { }, onError (err) { - t.equal(err.code, 'UND_ERR_INVALID_ARG') - t.equal(err.message, 'invalid onUpgrade method') + p.strictEqual(err.code, 'UND_ERR_INVALID_ARG') + p.strictEqual(err.message, 'invalid onUpgrade method') } }) }) + + await p.completed }) -test('dispatch upgrade onUpgrade missing', (t) => { - t.plan(2) +test('dispatch upgrade onUpgrade missing', async (t) => { + const p = tspl(t, { plan: 2 }) const server = http.createServer((req, res) => { res.end('ad') }) - t.teardown(server.close.bind(server)) + t.after(closeServerAsPromise(server)) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + t.after(() => { return client.close() }) client.dispatch({ path: '/', @@ -612,24 +641,26 @@ test('dispatch upgrade onUpgrade missing', (t) => { onHeaders (statusCode, headers) { }, onError (err) { - t.equal(err.code, 'UND_ERR_INVALID_ARG') - t.equal(err.message, 'invalid onUpgrade method') + p.strictEqual(err.code, 'UND_ERR_INVALID_ARG') + p.strictEqual(err.message, 'invalid onUpgrade method') } }) }) + + await p.completed }) -test('dispatch pool onError missing', (t) => { - t.plan(2) +test('dispatch pool onError missing', async (t) => { + const p = tspl(t, { plan: 2 }) const server = http.createServer((req, res) => { res.end('ad') }) - t.teardown(server.close.bind(server)) + t.after(closeServerAsPromise(server)) server.listen(0, () => { const client = new Pool(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + t.after(() => { return client.close() }) try { client.dispatch({ @@ -639,22 +670,24 @@ test('dispatch pool onError missing', (t) => { }, { }) } catch (err) { - t.equal(err.code, 'UND_ERR_INVALID_ARG') - t.equal(err.message, 'invalid onError method') + p.strictEqual(err.code, 'UND_ERR_INVALID_ARG') + p.strictEqual(err.message, 'invalid onError method') } }) + + await p.completed }) -test('dispatch onBodySent not a function', (t) => { - t.plan(2) +test('dispatch onBodySent not a function', async (t) => { + const p = tspl(t, { plan: 2 }) const server = http.createServer((req, res) => { res.end('ad') }) - t.teardown(server.close.bind(server)) + t.after(closeServerAsPromise(server)) server.listen(0, () => { const client = new Pool(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + t.after(() => { return client.close() }) client.dispatch({ path: '/', @@ -665,24 +698,26 @@ test('dispatch onBodySent not a function', (t) => { onHeaders () {}, onData () {}, onError (err) { - t.equal(err.code, 'UND_ERR_INVALID_ARG') - t.equal(err.message, 'invalid onBodySent method') + p.strictEqual(err.code, 'UND_ERR_INVALID_ARG') + p.strictEqual(err.message, 'invalid onBodySent method') } }) }) + + await p.completed }) -test('dispatch onBodySent buffer', (t) => { - t.plan(3) +test('dispatch onBodySent buffer', async (t) => { + const p = tspl(t, { plan: 3 }) const server = http.createServer((req, res) => { res.end('ad') }) - t.teardown(server.close.bind(server)) + t.after(closeServerAsPromise(server)) server.listen(0, () => { const client = new Pool(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + t.after(() => { return client.close() }) const body = 'hello 🚀' client.dispatch({ path: '/', @@ -690,10 +725,10 @@ test('dispatch onBodySent buffer', (t) => { body }, { onBodySent (chunk) { - t.equal(chunk.toString(), body) + p.strictEqual(chunk.toString(), body) }, onRequestSent () { - t.pass() + p.ok(1) }, onError (err) { throw err @@ -702,24 +737,26 @@ test('dispatch onBodySent buffer', (t) => { onHeaders () {}, onData () {}, onComplete () { - t.pass() + p.ok(1) } }) }) + + await p.completed }) -test('dispatch onBodySent stream', (t) => { - t.plan(8) +test('dispatch onBodySent stream', async (t) => { + const p = tspl(t, { plan: 8 }) const server = http.createServer((req, res) => { res.end('ad') }) - t.teardown(server.close.bind(server)) + t.after(closeServerAsPromise(server)) const chunks = ['he', 'llo', 'world', '🚀'] const toSendBytes = chunks.reduce((a, b) => a + Buffer.byteLength(b), 0) const body = stream.Readable.from(chunks) server.listen(0, () => { const client = new Pool(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + t.after(() => { return client.close() }) let sentBytes = 0 let currentChunk = 0 client.dispatch({ @@ -728,11 +765,11 @@ test('dispatch onBodySent stream', (t) => { body }, { onBodySent (chunk) { - t.equal(chunks[currentChunk++], chunk) + p.strictEqual(chunks[currentChunk++], chunk) sentBytes += Buffer.byteLength(chunk) }, onRequestSent () { - t.pass() + p.ok(1) }, onError (err) { throw err @@ -741,24 +778,26 @@ test('dispatch onBodySent stream', (t) => { onHeaders () {}, onData () {}, onComplete () { - t.equal(currentChunk, chunks.length) - t.equal(sentBytes, toSendBytes) - t.pass() + p.strictEqual(currentChunk, chunks.length) + p.strictEqual(sentBytes, toSendBytes) + p.ok(1) } }) }) + + await p.completed }) -test('dispatch onBodySent async-iterable', (t) => { +test('dispatch onBodySent async-iterable', (t, done) => { const server = http.createServer((req, res) => { res.end('ad') }) - t.teardown(server.close.bind(server)) + t.after(closeServerAsPromise(server)) const chunks = ['he', 'llo', 'world', '🚀'] const toSendBytes = chunks.reduce((a, b) => a + Buffer.byteLength(b), 0) server.listen(0, () => { const client = new Pool(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + t.after(() => { return client.close() }) let sentBytes = 0 let currentChunk = 0 client.dispatch({ @@ -767,7 +806,7 @@ test('dispatch onBodySent async-iterable', (t) => { body: chunks }, { onBodySent (chunk) { - t.equal(chunks[currentChunk++], chunk) + assert.strictEqual(chunks[currentChunk++], chunk) sentBytes += Buffer.byteLength(chunk) }, onError (err) { @@ -777,23 +816,23 @@ test('dispatch onBodySent async-iterable', (t) => { onHeaders () {}, onData () {}, onComplete () { - t.equal(currentChunk, chunks.length) - t.equal(sentBytes, toSendBytes) - t.end() + assert.strictEqual(currentChunk, chunks.length) + assert.strictEqual(sentBytes, toSendBytes) + done() } }) }) }) -test('dispatch onBodySent throws error', (t) => { +test('dispatch onBodySent throws error', (t, done) => { const server = http.createServer((req, res) => { res.end('ended') }) - t.teardown(server.close.bind(server)) + t.after(closeServerAsPromise(server)) server.listen(0, () => { const client = new Pool(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + t.after(() => { return client.close() }) const body = 'hello' client.dispatch({ path: '/', @@ -804,9 +843,9 @@ test('dispatch onBodySent throws error', (t) => { throw new Error('fail') }, onError (err) { - t.type(err, Error) - t.equal(err.message, 'fail') - t.end() + assert.ok(err instanceof Error) + assert.strictEqual(err.message, 'fail') + done() }, onConnect () {}, onHeaders () {}, @@ -816,17 +855,18 @@ test('dispatch onBodySent throws error', (t) => { }) }) -test('dispatches in expected order', (t) => { +test('dispatches in expected order', async (t) => { const server = http.createServer((req, res) => { res.end('ended') }) - t.teardown(server.close.bind(server)) + t.after(closeServerAsPromise(server)) + + const p = tspl(t, { plan: 1 }) server.listen(0, () => { const client = new Pool(`http://localhost:${server.address().port}`) - t.plan(1) - t.teardown(client.close.bind(client)) + t.after(() => { return client.close() }) const dispatches = [] @@ -852,16 +892,18 @@ test('dispatches in expected order', (t) => { }, onComplete () { dispatches.push('onComplete') - t.same(dispatches, ['onConnect', 'onBodySent', 'onResponseStarted', 'onHeaders', 'onData', 'onComplete']) + p.deepStrictEqual(dispatches, ['onConnect', 'onBodySent', 'onResponseStarted', 'onHeaders', 'onData', 'onComplete']) }, onError (err) { - t.error(err) + p.ifError(err) } }) }) + + await p.completed }) -test('dispatches in expected order for http2', (t) => { +test('dispatches in expected order for http2', async (t) => { const server = createSecureServer(pem) server.on('stream', (stream) => { stream.respond({ @@ -871,7 +913,7 @@ test('dispatches in expected order for http2', (t) => { stream.end('ended') }) - t.teardown(server.close.bind(server)) + const p = tspl(t, { plan: 1 }) server.listen(0, () => { const client = new Pool(`https://localhost:${server.address().port}`, { @@ -881,8 +923,7 @@ test('dispatches in expected order for http2', (t) => { allowH2: true }) - t.plan(1) - t.teardown(client.close.bind(client)) + t.after(closeClientAndServerAsPromise(client, server)) const dispatches = [] @@ -908,11 +949,13 @@ test('dispatches in expected order for http2', (t) => { }, onComplete () { dispatches.push('onComplete') - t.same(dispatches, ['onConnect', 'onBodySent', 'onResponseStarted', 'onHeaders', 'onData', 'onComplete']) + p.deepStrictEqual(dispatches, ['onConnect', 'onBodySent', 'onResponseStarted', 'onHeaders', 'onData', 'onComplete']) }, onError (err) { - t.error(err) + p.ifError(err) } }) }) + + await p.completed }) diff --git a/test/node-test/client-errors.js b/test/node-test/client-errors.js new file mode 100644 index 00000000000..72ae7c7a0fd --- /dev/null +++ b/test/node-test/client-errors.js @@ -0,0 +1,1310 @@ +'use strict' + +const { test } = require('node:test') +const assert = require('node:assert') +const { Client, Pool, errors } = require('../..') +const { createServer } = require('http') +const https = require('https') +const pem = require('https-pem') +const { Readable } = require('stream') +const { tspl } = require('@matteo.collina/tspl') + +const { kSocket } = require('../../lib/core/symbols') +const { wrapWithAsyncIterable, maybeWrapStream, consts } = require('../utils/async-iterators') + +const { closeServerAsPromise } = require('../utils/node-http') + +class IteratorError extends Error {} + +test('GET errors and reconnect with pipelining 1', async (t) => { + const p = tspl(t, { plan: 9 }) + + const server = createServer() + + server.once('request', (req, res) => { + // first request received, destroying + p.ok(1) + res.socket.destroy() + + server.once('request', (req, res) => { + p.strictEqual('/', req.url) + p.strictEqual('GET', req.method) + res.setHeader('content-type', 'text/plain') + res.end('hello') + }) + }) + t.after(closeServerAsPromise(server)) + + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`, { + pipelining: 1 + }) + t.after(client.destroy.bind(client)) + + client.request({ path: '/', method: 'GET', idempotent: false, opaque: 'asd' }, (err, data) => { + p.ok(err instanceof Error) // we are expecting an error + p.strictEqual(data.opaque, 'asd') + }) + + client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => { + p.ifError(err) + p.strictEqual(statusCode, 200) + p.strictEqual(headers['content-type'], 'text/plain') + const bufs = [] + body.on('data', (buf) => { + bufs.push(buf) + }) + body.on('end', () => { + p.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) + }) + }) + }) + + await p.completed +}) + +test('GET errors and reconnect with pipelining 3', async (t) => { + const server = createServer() + const requestsThatWillError = 3 + let requests = 0 + + const p = tspl(t, { plan: 6 + requestsThatWillError * 3 }) + + server.on('request', (req, res) => { + if (requests++ < requestsThatWillError) { + // request received, destroying + p.ok(1) + + // socket might not be there if it was destroyed by another + // pipelined request + if (res.socket) { + res.socket.destroy() + } + } else { + p.strictEqual('/', req.url) + p.strictEqual('GET', req.method) + res.setHeader('content-type', 'text/plain') + res.end('hello') + } + }) + t.after(closeServerAsPromise(server)) + + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`, { + pipelining: 3 + }) + t.after(client.destroy.bind(client)) + + // all of these will error + for (let i = 0; i < 3; i++) { + client.request({ path: '/', method: 'GET', idempotent: false, opaque: 'asd' }, (err, data) => { + p.ok(err instanceof Error) // we are expecting an error + p.strictEqual(data.opaque, 'asd') + }) + } + + // this will be queued up + client.request({ path: '/', method: 'GET', idempotent: false }, (err, { statusCode, headers, body }) => { + p.ifError(err) + p.strictEqual(statusCode, 200) + p.strictEqual(headers['content-type'], 'text/plain') + const bufs = [] + body.on('data', (buf) => { + bufs.push(buf) + }) + body.on('end', () => { + p.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) + }) + }) + }) + + await p.completed +}) + +function errorAndPipelining (type) { + test(`POST with a ${type} that errors and pipelining 1 should reconnect`, async (t) => { + const p = tspl(t, { plan: 12 }) + + const server = createServer() + server.once('request', (req, res) => { + p.strictEqual('/', req.url) + p.strictEqual('POST', req.method) + p.strictEqual('42', req.headers['content-length']) + + const bufs = [] + req.on('data', (buf) => { + bufs.push(buf) + }) + + req.on('aborted', () => { + // we will abruptly close the connection here + // but this will still end + p.strictEqual('a string', Buffer.concat(bufs).toString('utf8')) + }) + + server.once('request', (req, res) => { + p.strictEqual('/', req.url) + p.strictEqual('GET', req.method) + res.setHeader('content-type', 'text/plain') + res.end('hello') + }) + }) + t.after(closeServerAsPromise(server)) + + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`) + t.after(client.destroy.bind(client)) + + client.request({ + path: '/', + method: 'POST', + headers: { + // higher than the length of the string + 'content-length': 42 + }, + opaque: 'asd', + body: maybeWrapStream(new Readable({ + read () { + this.push('a string') + this.destroy(new Error('kaboom')) + } + }), type) + }, (err, data) => { + p.strictEqual(err.message, 'kaboom') + p.strictEqual(data.opaque, 'asd') + }) + + // this will be queued up + client.request({ path: '/', method: 'GET', idempotent: false }, (err, { statusCode, headers, body }) => { + p.ifError(err) + p.strictEqual(statusCode, 200) + p.strictEqual(headers['content-type'], 'text/plain') + const bufs = [] + body.on('data', (buf) => { + bufs.push(buf) + }) + body.on('end', () => { + p.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) + }) + }) + }) + + await p.completed + }) +} + +errorAndPipelining(consts.STREAM) +errorAndPipelining(consts.ASYNC_ITERATOR) + +function errorAndChunkedEncodingPipelining (type) { + test(`POST with chunked encoding, ${type} body that errors and pipelining 1 should reconnect`, async (t) => { + const p = tspl(t, { plan: 12 }) + + const server = createServer() + server.once('request', (req, res) => { + p.strictEqual('/', req.url) + p.strictEqual('POST', req.method) + p.strictEqual(req.headers['content-length'], undefined) + + const bufs = [] + req.on('data', (buf) => { + bufs.push(buf) + }) + + req.on('aborted', () => { + // we will abruptly close the connection here + // but this will still end + p.strictEqual('a string', Buffer.concat(bufs).toString('utf8')) + }) + + server.once('request', (req, res) => { + p.strictEqual('/', req.url) + p.strictEqual('GET', req.method) + res.setHeader('content-type', 'text/plain') + res.end('hello') + }) + }) + t.after(closeServerAsPromise(server)) + + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`) + t.after(client.destroy.bind(client)) + + client.request({ + path: '/', + method: 'POST', + opaque: 'asd', + body: maybeWrapStream(new Readable({ + read () { + this.push('a string') + this.destroy(new Error('kaboom')) + } + }), type) + }, (err, data) => { + p.strictEqual(err.message, 'kaboom') + p.strictEqual(data.opaque, 'asd') + }) + + // this will be queued up + client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => { + p.ifError(err) + p.strictEqual(statusCode, 200) + p.strictEqual(headers['content-type'], 'text/plain') + const bufs = [] + body.on('data', (buf) => { + bufs.push(buf) + }) + body.on('end', () => { + p.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) + }) + }) + }) + await p.completed + }) +} + +errorAndChunkedEncodingPipelining(consts.STREAM) +errorAndChunkedEncodingPipelining(consts.ASYNC_ITERATOR) + +test('invalid options throws', (t, done) => { + try { + new Client({ port: 'foobar', protocol: 'https:' }) // eslint-disable-line + assert.ok(0) + } catch (err) { + assert.ok(err instanceof errors.InvalidArgumentError) + assert.strictEqual(err.message, 'Invalid URL: port must be a valid integer or a string representation of an integer.') + } + + try { + new Client(new URL('http://asd:200/somepath')) // eslint-disable-line + assert.ok(0) + } catch (err) { + assert.ok(err instanceof errors.InvalidArgumentError) + assert.strictEqual(err.message, 'invalid url') + } + + try { + new Client(new URL('http://asd:200?q=asd')) // eslint-disable-line + assert.ok(0) + } catch (err) { + assert.ok(err instanceof errors.InvalidArgumentError) + assert.strictEqual(err.message, 'invalid url') + } + + try { + new Client(new URL('http://asd:200#asd')) // eslint-disable-line + assert.ok(0) + } catch (err) { + assert.ok(err instanceof errors.InvalidArgumentError) + assert.strictEqual(err.message, 'invalid url') + } + + try { + new Client(new URL('http://localhost:200'), { // eslint-disable-line + socketPath: 1 + }) + assert.ok(0) + } catch (err) { + assert.ok(err instanceof errors.InvalidArgumentError) + assert.strictEqual(err.message, 'invalid socketPath') + } + + try { + new Client(new URL('http://localhost:200'), { // eslint-disable-line + keepAliveTimeout: 'asd' + }) // eslint-disable-line + assert.ok(0) + } catch (err) { + assert.ok(err instanceof errors.InvalidArgumentError) + assert.strictEqual(err.message, 'invalid keepAliveTimeout') + } + + try { + new Client(new URL('http://localhost:200'), { // eslint-disable-line + localAddress: 123 + }) // eslint-disable-line + assert.ok(0) + } catch (err) { + assert.ok(err instanceof errors.InvalidArgumentError) + assert.strictEqual(err.message, 'localAddress must be valid string IP address') + } + + try { + new Client(new URL('http://localhost:200'), { // eslint-disable-line + localAddress: 'abcd123' + }) // eslint-disable-line + assert.ok(0) + } catch (err) { + assert.ok(err instanceof errors.InvalidArgumentError) + assert.strictEqual(err.message, 'localAddress must be valid string IP address') + } + + try { + new Client(new URL('http://localhost:200'), { // eslint-disable-line + keepAliveMaxTimeout: 'asd' + }) // eslint-disable-line + assert.ok(0) + } catch (err) { + assert.ok(err instanceof errors.InvalidArgumentError) + assert.strictEqual(err.message, 'invalid keepAliveMaxTimeout') + } + + try { + new Client(new URL('http://localhost:200'), { // eslint-disable-line + keepAliveMaxTimeout: 0 + }) // eslint-disable-line + assert.ok(0) + } catch (err) { + assert.ok(err instanceof errors.InvalidArgumentError) + assert.strictEqual(err.message, 'invalid keepAliveMaxTimeout') + } + + try { + new Client(new URL('http://localhost:200'), { // eslint-disable-line + keepAliveTimeoutThreshold: 'asd' + }) // eslint-disable-line + assert.ok(0) + } catch (err) { + assert.ok(err instanceof errors.InvalidArgumentError) + assert.strictEqual(err.message, 'invalid keepAliveTimeoutThreshold') + } + + try { + new Client({ // eslint-disable-line + protocol: 'asd' + }) + assert.ok(0) + } catch (err) { + assert.ok(err instanceof errors.InvalidArgumentError) + assert.strictEqual(err.message, 'Invalid URL protocol: the URL must start with `http:` or `https:`.') + } + + try { + new Client({ // eslint-disable-line + hostname: 1 + }) + assert.ok(0) + } catch (err) { + assert.ok(err instanceof errors.InvalidArgumentError) + assert.strictEqual(err.message, 'Invalid URL protocol: the URL must start with `http:` or `https:`.') + } + + try { + new Client(new URL('http://localhost:200'), { // eslint-disable-line + maxHeaderSize: 'asd' + }) + assert.ok(0) + } catch (err) { + assert.ok(err instanceof errors.InvalidArgumentError) + assert.strictEqual(err.message, 'invalid maxHeaderSize') + } + + try { + new Client(1) // eslint-disable-line + assert.ok(0) + } catch (err) { + assert.ok(err instanceof errors.InvalidArgumentError) + assert.strictEqual(err.message, 'Invalid URL: The URL argument must be a non-null object.') + } + + try { + const client = new Client(new URL('http://localhost:200')) // eslint-disable-line + client.destroy(null, null) + assert.ok(0) + } catch (err) { + assert.ok(err instanceof errors.InvalidArgumentError) + assert.strictEqual(err.message, 'invalid callback') + } + + try { + const client = new Client(new URL('http://localhost:200')) // eslint-disable-line + client.close(null, null) + assert.ok(0) + } catch (err) { + assert.ok(err instanceof errors.InvalidArgumentError) + assert.strictEqual(err.message, 'invalid callback') + } + + try { + new Client(new URL('http://localhost:200'), { maxKeepAliveTimeout: 1e3 }) // eslint-disable-line + assert.ok(0) + } catch (err) { + assert.ok(err instanceof errors.InvalidArgumentError) + assert.strictEqual(err.message, 'unsupported maxKeepAliveTimeout, use keepAliveMaxTimeout instead') + } + + try { + new Client(new URL('http://localhost:200'), { keepAlive: false }) // eslint-disable-line + assert.ok(0) + } catch (err) { + assert.ok(err instanceof errors.InvalidArgumentError) + assert.strictEqual(err.message, 'unsupported keepAlive, use pipelining=0 instead') + } + + try { + new Client(new URL('http://localhost:200'), { idleTimeout: 30e3 }) // eslint-disable-line + assert.ok(0) + } catch (err) { + assert.ok(err instanceof errors.InvalidArgumentError) + assert.strictEqual(err.message, 'unsupported idleTimeout, use keepAliveTimeout instead') + } + + try { + new Client(new URL('http://localhost:200'), { socketTimeout: 30e3 }) // eslint-disable-line + assert.ok(0) + } catch (err) { + assert.ok(err instanceof errors.InvalidArgumentError) + assert.strictEqual(err.message, 'unsupported socketTimeout, use headersTimeout & bodyTimeout instead') + } + + try { + new Client(new URL('http://localhost:200'), { requestTimeout: 30e3 }) // eslint-disable-line + assert.ok(0) + } catch (err) { + assert.ok(err instanceof errors.InvalidArgumentError) + assert.strictEqual(err.message, 'unsupported requestTimeout, use headersTimeout & bodyTimeout instead') + } + + try { + new Client(new URL('http://localhost:200'), { connectTimeout: -1 }) // eslint-disable-line + assert.ok(0) + } catch (err) { + assert.ok(err instanceof errors.InvalidArgumentError) + assert.strictEqual(err.message, 'invalid connectTimeout') + } + + try { + new Client(new URL('http://localhost:200'), { connectTimeout: Infinity }) // eslint-disable-line + assert.ok(0) + } catch (err) { + assert.ok(err instanceof errors.InvalidArgumentError) + assert.strictEqual(err.message, 'invalid connectTimeout') + } + + try { + new Client(new URL('http://localhost:200'), { connectTimeout: 'asd' }) // eslint-disable-line + assert.ok(0) + } catch (err) { + assert.ok(err instanceof errors.InvalidArgumentError) + assert.strictEqual(err.message, 'invalid connectTimeout') + } + + try { + new Client(new URL('http://localhost:200'), { connect: 'asd' }) // eslint-disable-line + assert.ok(0) + } catch (err) { + assert.ok(err instanceof errors.InvalidArgumentError) + assert.strictEqual(err.message, 'connect must be a function or an object') + } + + try { + new Client(new URL('http://localhost:200'), { connect: -1 }) // eslint-disable-line + assert.ok(0) + } catch (err) { + assert.ok(err instanceof errors.InvalidArgumentError) + assert.strictEqual(err.message, 'connect must be a function or an object') + } + + try { + new Pool(new URL('http://localhost:200'), { connect: 'asd' }) // eslint-disable-line + assert.ok(0) + } catch (err) { + assert.ok(err instanceof errors.InvalidArgumentError) + assert.strictEqual(err.message, 'connect must be a function or an object') + } + + try { + new Pool(new URL('http://localhost:200'), { connect: -1 }) // eslint-disable-line + assert.ok(0) + } catch (err) { + assert.ok(err instanceof errors.InvalidArgumentError) + assert.strictEqual(err.message, 'connect must be a function or an object') + } + + try { + new Client(new URL('http://localhost:200'), { maxCachedSessions: -10 }) // eslint-disable-line + assert.ok(0) + } catch (err) { + assert.ok(err instanceof errors.InvalidArgumentError) + assert.strictEqual(err.message, 'maxCachedSessions must be a positive integer or zero') + } + + try { + new Client(new URL('http://localhost:200'), { maxCachedSessions: 'foo' }) // eslint-disable-line + assert.ok(0) + } catch (err) { + assert.ok(err instanceof errors.InvalidArgumentError) + assert.strictEqual(err.message, 'maxCachedSessions must be a positive integer or zero') + } + + try { + new Client(new URL('http://localhost:200'), { maxRequestsPerClient: 'foo' }) // eslint-disable-line + assert.ok(0) + } catch (err) { + assert.ok(err instanceof errors.InvalidArgumentError) + assert.strictEqual(err.message, 'maxRequestsPerClient must be a positive number') + } + + try { + new Client(new URL('http://localhost:200'), { autoSelectFamilyAttemptTimeout: 'foo' }) // eslint-disable-line + assert.ok(0) + } catch (err) { + assert.ok(err instanceof errors.InvalidArgumentError) + assert.strictEqual(err.message, 'autoSelectFamilyAttemptTimeout must be a positive number') + } + + done() +}) + +test('POST which fails should error response', async (t) => { + const p = tspl(t, { plan: 6 }) + + const server = createServer() + server.on('request', (req, res) => { + req.once('data', () => { + res.destroy() + }) + }) + t.after(closeServerAsPromise(server)) + + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`) + t.after(client.destroy.bind(client)) + + function checkError (err) { + // Different platforms error with different codes... + p.ok( + err.code === 'EPIPE' || + err.code === 'ECONNRESET' || + err.code === 'UND_ERR_SOCKET' || + err.message === 'other side closed' + ) + } + + { + const body = new Readable({ read () {} }) + body.push('asd') + body.on('error', (err) => { + checkError(err) + }) + + client.request({ + path: '/', + method: 'POST', + body + }, (err) => { + checkError(err) + }) + } + + { + const body = new Readable({ read () {} }) + body.push('asd') + body.on('error', (err) => { + checkError(err) + }) + + client.request({ + path: '/', + method: 'POST', + headers: { + 'content-length': 100 + }, + body + }, (err) => { + checkError(err) + }) + } + + { + const body = wrapWithAsyncIterable(['asd'], true) + + client.request({ + path: '/', + method: 'POST', + body + }, (err) => { + checkError(err) + }) + } + + { + const body = wrapWithAsyncIterable(['asd'], true) + + client.request({ + path: '/', + method: 'POST', + headers: { + 'content-length': 100 + }, + body + }, (err) => { + checkError(err) + }) + } + }) + + await p.completed +}) + +test('client destroy cleanup', async (t) => { + const p = tspl(t, { plan: 3 }) + + const _err = new Error('kaboom') + let client + const server = createServer() + server.once('request', (req, res) => { + req.once('data', () => { + client.destroy(_err, (err) => { + p.ifError(err) + }) + }) + }) + t.after(closeServerAsPromise(server)) + + server.listen(0, () => { + client = new Client(`http://localhost:${server.address().port}`) + t.after(client.destroy.bind(client)) + + const body = new Readable({ read () {} }) + body.push('asd') + body.on('error', (err) => { + p.strictEqual(err, _err) + }) + + client.request({ + path: '/', + method: 'POST', + body + }, (err, data) => { + p.strictEqual(err, _err) + }) + }) + + await p.completed +}) + +test('throwing async-iterator causes error', async (t) => { + const p = tspl(t, { plan: 1 }) + + const server = createServer((req, res) => { + res.end(Buffer.alloc(4 + 1, 'a')) + }) + t.after(closeServerAsPromise(server)) + + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`) + t.after(client.destroy.bind(client)) + + client.request({ + method: 'POST', + path: '/', + body: (async function * () { + yield 'hello' + throw new IteratorError('bad iterator') + })() + }, (err) => { + p.ok(err instanceof IteratorError) + }) + }) + + await p.completed +}) + +test('client async-iterator destroy cleanup', async (t) => { + const p = tspl(t, { plan: 2 }) + + const _err = new Error('kaboom') + let client + const server = createServer() + server.once('request', (req, res) => { + req.once('data', () => { + client.destroy(_err, (err) => { + p.ifError(err) + }) + }) + }) + t.after(closeServerAsPromise(server)) + + server.listen(0, () => { + client = new Client(`http://localhost:${server.address().port}`) + t.after(client.destroy.bind(client)) + + const body = wrapWithAsyncIterable(['asd'], true) + + client.request({ + path: '/', + method: 'POST', + body + }, (err, data) => { + p.strictEqual(err, _err) + }) + }) + + await p.completed +}) + +test('GET errors body', async (t) => { + const p = tspl(t, { plan: 2 }) + + const server = createServer() + server.once('request', (req, res) => { + res.write('asd') + setTimeout(() => { + res.destroy() + }, 19) + }) + t.after(closeServerAsPromise(server)) + + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`) + t.after(client.destroy.bind(client)) + + client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => { + p.ifError(err) + body.resume() + body.on('error', err => ( + p.ok(err) + )) + }) + }) + + await p.completed +}) + +test('validate request body', async (t) => { + const p = tspl(t, { plan: 6 }) + + const server = createServer((req, res) => { + res.end('asd') + }) + t.after(closeServerAsPromise(server)) + + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`) + t.after(() => { return client.close() }) + + client.request({ + path: '/', + method: 'POST', + body: /asdasd/ + }, (err, data) => { + p.ok(err instanceof errors.InvalidArgumentError) + }) + + client.request({ + path: '/', + method: 'POST', + body: 0 + }, (err, data) => { + p.ok(err instanceof errors.InvalidArgumentError) + }) + + client.request({ + path: '/', + method: 'POST', + body: false + }, (err, data) => { + p.ok(err instanceof errors.InvalidArgumentError) + }) + + client.request({ + path: '/', + method: 'POST', + body: '' + }, (err, data) => { + p.ifError(err) + data.body.resume() + }) + + client.request({ + path: '/', + method: 'POST', + body: new Uint8Array() + }, (err, data) => { + p.ifError(err) + data.body.resume() + }) + + client.request({ + path: '/', + method: 'POST', + body: Buffer.alloc(10) + }, (err, data) => { + p.ifError(err) + data.body.resume() + }) + }) + + await p.completed +}) + +function socketFailWrite (type) { + test(`socket fail while writing ${type} request body`, async (t) => { + const p = tspl(t, { plan: 2 }) + + const server = createServer() + server.once('request', (req, res) => { + }) + t.after(closeServerAsPromise(server)) + + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`) + t.after(client.destroy.bind(client)) + + const preBody = new Readable({ read () {} }) + preBody.push('asd') + const body = maybeWrapStream(preBody, type) + client.on('connect', () => { + process.nextTick(() => { + client[kSocket].destroy('kaboom') + }) + }) + + client.request({ + path: '/', + method: 'POST', + body + }, (err) => { + p.ok(err) + }) + client.close((err) => { + p.ifError(err) + }) + }) + + await p.completed + }) +} +socketFailWrite(consts.STREAM) +socketFailWrite(consts.ASYNC_ITERATOR) + +function socketFailEndWrite (type) { + test(`socket fail while ending ${type} request body`, async (t) => { + const p = tspl(t, { plan: 3 }) + + const server = createServer() + server.once('request', (req, res) => { + res.end() + }) + t.after(closeServerAsPromise(server)) + + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`, { + pipelining: 2 + }) + t.after(client.destroy.bind(client)) + + const _err = new Error('kaboom') + client.on('connect', () => { + process.nextTick(() => { + client[kSocket].destroy(_err) + }) + }) + const preBody = new Readable({ read () {} }) + preBody.push(null) + const body = maybeWrapStream(preBody, type) + + client.request({ + path: '/', + method: 'POST', + body + }, (err) => { + p.strictEqual(err, _err) + }) + client.close((err) => { + p.ifError(err) + client.close((err) => { + p.ok(err instanceof errors.ClientDestroyedError) + }) + }) + }) + + await p.completed + }) +} + +socketFailEndWrite(consts.STREAM) +socketFailEndWrite(consts.ASYNC_ITERATOR) + +test('queued request should not fail on socket destroy', async (t) => { + const p = tspl(t, { plan: 4 }) + + const server = createServer() + server.on('request', (req, res) => { + res.end() + }) + t.after(closeServerAsPromise(server)) + + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`, { + pipelining: 1 + }) + t.after(client.destroy.bind(client)) + + client.request({ + path: '/', + method: 'GET' + }, (err, data) => { + p.ifError(err) + data.body.resume().on('error', () => { + p.ok(1) + }) + client[kSocket].destroy() + client.request({ + path: '/', + method: 'GET' + }, (err, data) => { + p.ifError(err) + data.body.resume().on('end', () => { + p.ok(1) + }) + }) + }) + }) + + await p.completed +}) + +test('queued request should fail on client destroy', async (t) => { + const p = tspl(t, { plan: 6 }) + + const server = createServer() + server.on('request', (req, res) => { + res.end() + }) + t.after(closeServerAsPromise(server)) + + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`, { + pipelining: 1 + }) + t.after(client.destroy.bind(client)) + + let requestErrored = false + client.request({ + path: '/', + method: 'GET' + }, (err, data) => { + p.ifError(err) + data.body.resume() + .on('error', () => { + p.ok(1) + }) + client.destroy((err) => { + p.ifError(err) + p.strictEqual(requestErrored, true) + }) + }) + client.request({ + path: '/', + method: 'GET', + opaque: 'asd' + }, (err, data) => { + requestErrored = true + p.ok(err) + p.strictEqual(data.opaque, 'asd') + }) + }) + + await p.completed +}) + +test('retry idempotent inflight', async (t) => { + const p = tspl(t, { plan: 3 }) + + const server = createServer() + server.on('request', (req, res) => { + res.end() + }) + t.after(closeServerAsPromise(server)) + + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`, { + pipelining: 3 + }) + t.after(() => { return client.close() }) + + client.request({ + path: '/', + method: 'POST', + body: new Readable({ + read () { + this.destroy(new Error('kaboom')) + } + }) + }, (err) => { + p.ok(err) + }) + client.request({ + path: '/', + method: 'GET' + }, (err, data) => { + p.ifError(err) + data.body.resume() + }) + client.request({ + path: '/', + method: 'GET' + }, (err, data) => { + p.ifError(err) + data.body.resume() + }) + }) + + await p.completed +}) + +test('invalid opts', async (t) => { + const p = tspl(t, { plan: 2 }) + + const client = new Client('http://localhost:5000') + client.request(null, (err) => { + p.ok(err instanceof errors.InvalidArgumentError) + }) + client.pipeline(null).on('error', (err) => { + p.ok(err instanceof errors.InvalidArgumentError) + }) + + await p.completed +}) + +test('default port for http and https', async (t) => { + const p = tspl(t, { plan: 4 }) + + try { + new Client(new URL('http://localhost:80')) // eslint-disable-line + p.ok('Should not throw') + } catch (err) { + p.fail(err) + } + + try { + new Client(new URL('http://localhost')) // eslint-disable-line + p.ok('Should not throw') + } catch (err) { + p.fail(err) + } + + try { + new Client(new URL('https://localhost:443')) // eslint-disable-line + p.ok('Should not throw') + } catch (err) { + p.fail(err) + } + + try { + new Client(new URL('https://localhost')) // eslint-disable-line + p.ok('Should not throw') + } catch (err) { + p.fail(err) + } +}) + +test('CONNECT throws in next tick', async (t) => { + const p = tspl(t, { plan: 3 }) + + const server = createServer() + server.on('request', (req, res) => { + res.end() + }) + t.after(closeServerAsPromise(server)) + + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`) + t.after(client.destroy.bind(client)) + + client.request({ + path: '/', + method: 'GET' + }, (err, data) => { + p.ifError(err) + data.body + .on('end', () => { + let ticked = false + client.request({ + path: '/', + method: 'CONNECT' + }, (err) => { + p.ok(err) + p.strictEqual(ticked, true) + }) + ticked = true + }) + .resume() + }) + }) + + await p.completed +}) + +test('invalid signal', async (t) => { + const p = tspl(t, { plan: 8 }) + + const client = new Client('http://localhost:3333') + t.after(client.destroy.bind(client)) + + let ticked = false + client.request({ path: '/', method: 'GET', signal: {}, opaque: 'asd' }, (err, { opaque }) => { + p.strictEqual(ticked, true) + p.strictEqual(opaque, 'asd') + p.ok(err instanceof errors.InvalidArgumentError) + }) + client.pipeline({ path: '/', method: 'GET', signal: {} }, () => {}) + .on('error', (err) => { + p.strictEqual(ticked, true) + p.ok(err instanceof errors.InvalidArgumentError) + }) + client.stream({ path: '/', method: 'GET', signal: {}, opaque: 'asd' }, () => {}, (err, { opaque }) => { + p.strictEqual(ticked, true) + p.strictEqual(opaque, 'asd') + p.ok(err instanceof errors.InvalidArgumentError) + }) + ticked = true + + await p.completed +}) + +test('invalid body chunk does not crash', async (t) => { + const p = tspl(t, { plan: 1 }) + + const server = createServer() + server.on('request', (req, res) => { + res.end() + }) + t.after(closeServerAsPromise(server)) + + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`) + t.after(client.destroy.bind(client)) + + client.request({ + path: '/', + body: new Readable({ + objectMode: true, + read () { + this.push({}) + } + }), + method: 'GET' + }, (err) => { + p.strictEqual(err.code, 'ERR_INVALID_ARG_TYPE') + }) + }) + + await p.completed +}) + +test('socket errors', async (t) => { + const p = tspl(t, { plan: 2 }) + const client = new Client('http://localhost:5554') + t.after(client.destroy.bind(client)) + + client.request({ path: '/', method: 'GET' }, (err, data) => { + p.ok(err) + // TODO: Why UND_ERR_SOCKET? + p.ok(err.code === 'ECONNREFUSED' || err.code === 'UND_ERR_SOCKET', err.code) + p.end() + }) + + await p.completed +}) + +test('headers overflow', (t, done) => { + const p = tspl(t, { plan: 2 }) + const server = createServer() + server.on('request', (req, res) => { + res.writeHead(200, { + 'x-test-1': '1', + 'x-test-2': '2' + }) + res.end() + }) + t.after(closeServerAsPromise(server)) + + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`, { + maxHeaderSize: 10 + }) + t.after(client.destroy.bind(client)) + + client.request({ path: '/', method: 'GET' }, (err, data) => { + p.ok(err) + p.strictEqual(err.code, 'UND_ERR_HEADERS_OVERFLOW') + done() + }) + }) +}) + +test('SocketError should expose socket details (net)', async (t) => { + const p = tspl(t, { plan: 8 }) + + const server = createServer() + + server.once('request', (req, res) => { + res.destroy() + }) + t.after(closeServerAsPromise(server)) + + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`) + t.after(client.destroy.bind(client)) + + client.request({ path: '/', method: 'GET' }, (err, data) => { + p.ok(err instanceof errors.SocketError) + if (err.socket.remoteFamily === 'IPv4') { + p.strictEqual(err.socket.remoteFamily, 'IPv4') + p.strictEqual(err.socket.localAddress, '127.0.0.1') + p.strictEqual(err.socket.remoteAddress, '127.0.0.1') + } else { + p.strictEqual(err.socket.remoteFamily, 'IPv6') + p.strictEqual(err.socket.localAddress, '::1') + p.strictEqual(err.socket.remoteAddress, '::1') + } + p.ok(typeof err.socket.localPort === 'number') + p.ok(typeof err.socket.remotePort === 'number') + p.ok(typeof err.socket.bytesWritten === 'number') + p.ok(typeof err.socket.bytesRead === 'number') + }) + }) + await p.completed +}) + +test('SocketError should expose socket details (tls)', async (t) => { + const p = tspl(t, { plan: 8 }) + + const server = https.createServer(pem) + + server.once('request', (req, res) => { + res.destroy() + }) + t.after(closeServerAsPromise(server)) + + server.listen(0, () => { + const client = new Client(`https://localhost:${server.address().port}`, { + tls: { + rejectUnauthorized: false + } + }) + t.after(client.destroy.bind(client)) + + client.request({ path: '/', method: 'GET' }, (err, data) => { + p.ok(err instanceof errors.SocketError) + if (err.socket.remoteFamily === 'IPv4') { + p.strictEqual(err.socket.remoteFamily, 'IPv4') + p.strictEqual(err.socket.localAddress, '127.0.0.1') + p.strictEqual(err.socket.remoteAddress, '127.0.0.1') + } else { + p.strictEqual(err.socket.remoteFamily, 'IPv6') + p.strictEqual(err.socket.localAddress, '::1') + p.strictEqual(err.socket.remoteAddress, '::1') + } + p.ok(typeof err.socket.localPort === 'number') + p.ok(typeof err.socket.remotePort === 'number') + p.ok(typeof err.socket.bytesWritten === 'number') + p.ok(typeof err.socket.bytesRead === 'number') + }) + }) + + await p.completed +})