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