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