'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 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() }) }) }) test('parser error', (t) => { t.plan(2) const server = net.createServer() server.once('connection', (socket) => { socket.write('asd\n\r213123') }) 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) => { t.ok(err) client.close((err) => { t.error(err) }) }) }) }) 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') }) }) })