summaryrefslogtreecommitdiffstats
path: root/test/request-timeout.js
diff options
context:
space:
mode:
Diffstat (limited to 'test/request-timeout.js')
-rw-r--r--test/request-timeout.js820
1 files changed, 820 insertions, 0 deletions
diff --git a/test/request-timeout.js b/test/request-timeout.js
new file mode 100644
index 0000000..3ec5c10
--- /dev/null
+++ b/test/request-timeout.js
@@ -0,0 +1,820 @@
+'use strict'
+
+const { test } = require('tap')
+const { createReadStream, writeFileSync, unlinkSync } = require('fs')
+const { Client, errors } = require('..')
+const { kConnect } = require('../lib/core/symbols')
+const { nodeMajor } = require('../lib/core/util')
+const timers = require('../lib/timers')
+const { createServer } = require('http')
+const EventEmitter = require('events')
+const FakeTimers = require('@sinonjs/fake-timers')
+const { AbortController } = require('abort-controller')
+const {
+ pipeline,
+ Readable,
+ Writable,
+ PassThrough
+} = require('stream')
+
+test('request timeout', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ setTimeout(() => {
+ res.end('hello')
+ }, 1000)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, { headersTimeout: 500 })
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET' }, (err, response) => {
+ t.type(err, errors.HeadersTimeoutError)
+ })
+ })
+})
+
+test('request timeout with readable body', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ })
+ t.teardown(server.close.bind(server))
+
+ const tempfile = `${__filename}.10mb.txt`
+ writeFileSync(tempfile, Buffer.alloc(10 * 1024 * 1024))
+ t.teardown(() => unlinkSync(tempfile))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, { headersTimeout: 1e3 })
+ t.teardown(client.destroy.bind(client))
+
+ const body = createReadStream(tempfile)
+ client.request({ path: '/', method: 'POST', body }, (err, response) => {
+ t.type(err, errors.HeadersTimeoutError)
+ })
+ })
+}, { skip: nodeMajor < 14 })
+
+test('body timeout', (t) => {
+ t.plan(2)
+
+ const clock = FakeTimers.install()
+ t.teardown(clock.uninstall.bind(clock))
+
+ const orgTimers = { ...timers }
+ Object.assign(timers, { setTimeout, clearTimeout })
+ t.teardown(() => {
+ Object.assign(timers, orgTimers)
+ })
+
+ const server = createServer((req, res) => {
+ res.write('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, { bodyTimeout: 50 })
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET' }, (err, { body }) => {
+ t.error(err)
+ body.on('data', () => {
+ clock.tick(100)
+ }).on('error', (err) => {
+ t.type(err, errors.BodyTimeoutError)
+ })
+ })
+
+ clock.tick(50)
+ })
+})
+
+test('overridden request timeout', (t) => {
+ t.plan(1)
+
+ const clock = FakeTimers.install()
+ t.teardown(clock.uninstall.bind(clock))
+
+ const orgTimers = { ...timers }
+ Object.assign(timers, { setTimeout, clearTimeout })
+ t.teardown(() => {
+ Object.assign(timers, orgTimers)
+ })
+
+ const server = createServer((req, res) => {
+ setTimeout(() => {
+ res.end('hello')
+ }, 100)
+ clock.tick(100)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, { headersTimeout: 500 })
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET', headersTimeout: 50 }, (err, response) => {
+ t.type(err, errors.HeadersTimeoutError)
+ })
+
+ clock.tick(50)
+ })
+})
+
+test('overridden body timeout', (t) => {
+ t.plan(2)
+
+ const clock = FakeTimers.install()
+ t.teardown(clock.uninstall.bind(clock))
+
+ const orgTimers = { ...timers }
+ Object.assign(timers, { setTimeout, clearTimeout })
+ t.teardown(() => {
+ Object.assign(timers, orgTimers)
+ })
+
+ const server = createServer((req, res) => {
+ res.write('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, { bodyTimeout: 500 })
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET', bodyTimeout: 50 }, (err, { body }) => {
+ t.error(err)
+ body.on('data', () => {
+ clock.tick(100)
+ }).on('error', (err) => {
+ t.type(err, errors.BodyTimeoutError)
+ })
+ })
+
+ clock.tick(50)
+ })
+})
+
+test('With EE signal', (t) => {
+ t.plan(1)
+
+ const clock = FakeTimers.install()
+ t.teardown(clock.uninstall.bind(clock))
+
+ const orgTimers = { ...timers }
+ Object.assign(timers, { setTimeout, clearTimeout })
+ t.teardown(() => {
+ Object.assign(timers, orgTimers)
+ })
+
+ const server = createServer((req, res) => {
+ setTimeout(() => {
+ res.end('hello')
+ }, 100)
+ clock.tick(100)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ headersTimeout: 50
+ })
+ const ee = new EventEmitter()
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET', signal: ee }, (err, response) => {
+ t.type(err, errors.HeadersTimeoutError)
+ })
+
+ clock.tick(50)
+ })
+})
+
+test('With abort-controller signal', (t) => {
+ t.plan(1)
+
+ const clock = FakeTimers.install()
+ t.teardown(clock.uninstall.bind(clock))
+
+ const orgTimers = { ...timers }
+ Object.assign(timers, { setTimeout, clearTimeout })
+ t.teardown(() => {
+ Object.assign(timers, orgTimers)
+ })
+
+ const server = createServer((req, res) => {
+ setTimeout(() => {
+ res.end('hello')
+ }, 100)
+ clock.tick(100)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ headersTimeout: 50
+ })
+ const abortController = new AbortController()
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET', signal: abortController.signal }, (err, response) => {
+ t.type(err, errors.HeadersTimeoutError)
+ })
+
+ clock.tick(50)
+ })
+})
+
+test('Abort before timeout (EE)', (t) => {
+ t.plan(1)
+
+ const clock = FakeTimers.install()
+ t.teardown(clock.uninstall.bind(clock))
+
+ const orgTimers = { ...timers }
+ Object.assign(timers, { setTimeout, clearTimeout })
+ t.teardown(() => {
+ Object.assign(timers, orgTimers)
+ })
+
+ const ee = new EventEmitter()
+ const server = createServer((req, res) => {
+ setTimeout(() => {
+ res.end('hello')
+ }, 100)
+ ee.emit('abort')
+ clock.tick(50)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ headersTimeout: 50
+ })
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET', signal: ee }, (err, response) => {
+ t.type(err, errors.RequestAbortedError)
+ clock.tick(100)
+ })
+ })
+})
+
+test('Abort before timeout (abort-controller)', (t) => {
+ t.plan(1)
+
+ const clock = FakeTimers.install()
+ t.teardown(clock.uninstall.bind(clock))
+
+ const orgTimers = { ...timers }
+ Object.assign(timers, { setTimeout, clearTimeout })
+ t.teardown(() => {
+ Object.assign(timers, orgTimers)
+ })
+
+ const abortController = new AbortController()
+ const server = createServer((req, res) => {
+ setTimeout(() => {
+ res.end('hello')
+ }, 100)
+ abortController.abort()
+ clock.tick(50)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ headersTimeout: 50
+ })
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET', signal: abortController.signal }, (err, response) => {
+ t.type(err, errors.RequestAbortedError)
+ clock.tick(100)
+ })
+ })
+})
+
+test('Timeout with pipelining', (t) => {
+ t.plan(3)
+
+ const clock = FakeTimers.install()
+ t.teardown(clock.uninstall.bind(clock))
+
+ const orgTimers = { ...timers }
+ Object.assign(timers, { setTimeout, clearTimeout })
+ t.teardown(() => {
+ Object.assign(timers, orgTimers)
+ })
+
+ const server = createServer((req, res) => {
+ setTimeout(() => {
+ res.end('hello')
+ }, 100)
+ clock.tick(50)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ pipelining: 10,
+ headersTimeout: 50
+ })
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET' }, (err, response) => {
+ t.type(err, errors.HeadersTimeoutError)
+ })
+
+ client.request({ path: '/', method: 'GET' }, (err, response) => {
+ t.type(err, errors.HeadersTimeoutError)
+ })
+
+ client.request({ path: '/', method: 'GET' }, (err, response) => {
+ t.type(err, errors.HeadersTimeoutError)
+ })
+ })
+})
+
+test('Global option', (t) => {
+ t.plan(1)
+
+ const clock = FakeTimers.install()
+ t.teardown(clock.uninstall.bind(clock))
+
+ const orgTimers = { ...timers }
+ Object.assign(timers, { setTimeout, clearTimeout })
+ t.teardown(() => {
+ Object.assign(timers, orgTimers)
+ })
+
+ const server = createServer((req, res) => {
+ setTimeout(() => {
+ res.end('hello')
+ }, 100)
+ clock.tick(100)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ headersTimeout: 50
+ })
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET' }, (err, response) => {
+ t.type(err, errors.HeadersTimeoutError)
+ })
+
+ clock.tick(50)
+ })
+})
+
+test('Request options overrides global option', (t) => {
+ t.plan(1)
+
+ const clock = FakeTimers.install()
+ t.teardown(clock.uninstall.bind(clock))
+
+ const orgTimers = { ...timers }
+ Object.assign(timers, { setTimeout, clearTimeout })
+ t.teardown(() => {
+ Object.assign(timers, orgTimers)
+ })
+
+ const server = createServer((req, res) => {
+ setTimeout(() => {
+ res.end('hello')
+ }, 100)
+ clock.tick(100)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ headersTimeout: 50
+ })
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET' }, (err, response) => {
+ t.type(err, errors.HeadersTimeoutError)
+ })
+
+ clock.tick(50)
+ })
+})
+
+test('client.destroy should cancel the timeout', (t) => {
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ headersTimeout: 100
+ })
+
+ client.request({ path: '/', method: 'GET' }, (err, response) => {
+ t.type(err, errors.ClientDestroyedError)
+ })
+
+ client.destroy(err => {
+ t.error(err)
+ })
+ })
+})
+
+test('client.close should wait for the timeout', (t) => {
+ t.plan(2)
+
+ const clock = FakeTimers.install()
+ t.teardown(clock.uninstall.bind(clock))
+
+ const orgTimers = { ...timers }
+ Object.assign(timers, { setTimeout, clearTimeout })
+ t.teardown(() => {
+ Object.assign(timers, orgTimers)
+ })
+
+ const server = createServer((req, res) => {
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ headersTimeout: 100
+ })
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET' }, (err, response) => {
+ t.type(err, errors.HeadersTimeoutError)
+ })
+
+ client.close((err) => {
+ t.error(err)
+ })
+
+ client.on('connect', () => {
+ process.nextTick(() => {
+ clock.tick(100)
+ })
+ })
+ })
+})
+
+test('Validation', (t) => {
+ t.plan(4)
+
+ try {
+ const client = new Client('http://localhost:3000', {
+ headersTimeout: 'foobar'
+ })
+ t.teardown(client.destroy.bind(client))
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ }
+
+ try {
+ const client = new Client('http://localhost:3000', {
+ headersTimeout: -1
+ })
+ t.teardown(client.destroy.bind(client))
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ }
+
+ try {
+ const client = new Client('http://localhost:3000', {
+ bodyTimeout: 'foobar'
+ })
+ t.teardown(client.destroy.bind(client))
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ }
+
+ try {
+ const client = new Client('http://localhost:3000', {
+ bodyTimeout: -1
+ })
+ t.teardown(client.destroy.bind(client))
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ }
+})
+
+test('Disable request timeout', (t) => {
+ t.plan(2)
+
+ const clock = FakeTimers.install()
+ t.teardown(clock.uninstall.bind(clock))
+
+ const orgTimers = { ...timers }
+ Object.assign(timers, { setTimeout, clearTimeout })
+ t.teardown(() => {
+ Object.assign(timers, orgTimers)
+ })
+
+ const server = createServer((req, res) => {
+ setTimeout(() => {
+ res.end('hello')
+ }, 32e3)
+ clock.tick(33e3)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ headersTimeout: 0,
+ connectTimeout: 0
+ })
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET' }, (err, response) => {
+ t.error(err)
+ const bufs = []
+ response.body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ response.body.on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+
+ clock.tick(31e3)
+ })
+})
+
+test('Disable request timeout for a single request', (t) => {
+ t.plan(2)
+
+ const clock = FakeTimers.install()
+ t.teardown(clock.uninstall.bind(clock))
+
+ const orgTimers = { ...timers }
+ Object.assign(timers, { setTimeout, clearTimeout })
+ t.teardown(() => {
+ Object.assign(timers, orgTimers)
+ })
+
+ const server = createServer((req, res) => {
+ setTimeout(() => {
+ res.end('hello')
+ }, 32e3)
+ clock.tick(33e3)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ headersTimeout: 0,
+ connectTimeout: 0
+ })
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET' }, (err, response) => {
+ t.error(err)
+ const bufs = []
+ response.body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ response.body.on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+
+ clock.tick(31e3)
+ })
+})
+
+test('stream timeout', (t) => {
+ t.plan(1)
+
+ const clock = FakeTimers.install()
+ t.teardown(clock.uninstall.bind(clock))
+
+ const orgTimers = { ...timers }
+ Object.assign(timers, { setTimeout, clearTimeout })
+ t.teardown(() => {
+ Object.assign(timers, orgTimers)
+ })
+
+ const server = createServer((req, res) => {
+ setTimeout(() => {
+ res.end('hello')
+ }, 301e3)
+ clock.tick(301e3)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, { connectTimeout: 0 })
+ t.teardown(client.destroy.bind(client))
+
+ client.stream({
+ path: '/',
+ method: 'GET',
+ opaque: new PassThrough()
+ }, (result) => {
+ t.fail('Should not be called')
+ }, (err) => {
+ t.type(err, errors.HeadersTimeoutError)
+ })
+ })
+})
+
+test('stream custom timeout', (t) => {
+ t.plan(1)
+
+ const clock = FakeTimers.install()
+ t.teardown(clock.uninstall.bind(clock))
+
+ const orgTimers = { ...timers }
+ Object.assign(timers, { setTimeout, clearTimeout })
+ t.teardown(() => {
+ Object.assign(timers, orgTimers)
+ })
+
+ const server = createServer((req, res) => {
+ setTimeout(() => {
+ res.end('hello')
+ }, 31e3)
+ clock.tick(31e3)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ headersTimeout: 30e3
+ })
+ t.teardown(client.destroy.bind(client))
+
+ client.stream({
+ path: '/',
+ method: 'GET',
+ opaque: new PassThrough()
+ }, (result) => {
+ t.fail('Should not be called')
+ }, (err) => {
+ t.type(err, errors.HeadersTimeoutError)
+ })
+ })
+})
+
+test('pipeline timeout', (t) => {
+ t.plan(1)
+
+ const clock = FakeTimers.install()
+ t.teardown(clock.uninstall.bind(clock))
+
+ const orgTimers = { ...timers }
+ Object.assign(timers, { setTimeout, clearTimeout })
+ t.teardown(() => {
+ Object.assign(timers, orgTimers)
+ })
+
+ const server = createServer((req, res) => {
+ setTimeout(() => {
+ req.pipe(res)
+ }, 301e3)
+ clock.tick(301e3)
+ })
+ 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 buf = Buffer.alloc(1e6).toString()
+ pipeline(
+ new Readable({
+ read () {
+ this.push(buf)
+ this.push(null)
+ }
+ }),
+ client.pipeline({
+ path: '/',
+ method: 'PUT'
+ }, (result) => {
+ t.fail('Should not be called')
+ }, (e) => {
+ t.fail('Should not be called')
+ }),
+ new Writable({
+ write (chunk, encoding, callback) {
+ callback()
+ },
+ final (callback) {
+ callback()
+ }
+ }),
+ (err) => {
+ t.type(err, errors.HeadersTimeoutError)
+ }
+ )
+ })
+})
+
+test('pipeline timeout', (t) => {
+ t.plan(1)
+
+ const clock = FakeTimers.install()
+ t.teardown(clock.uninstall.bind(clock))
+
+ const orgTimers = { ...timers }
+ Object.assign(timers, { setTimeout, clearTimeout })
+ t.teardown(() => {
+ Object.assign(timers, orgTimers)
+ })
+
+ const server = createServer((req, res) => {
+ setTimeout(() => {
+ req.pipe(res)
+ }, 31e3)
+ clock.tick(31e3)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ headersTimeout: 30e3
+ })
+ t.teardown(client.destroy.bind(client))
+
+ const buf = Buffer.alloc(1e6).toString()
+ pipeline(
+ new Readable({
+ read () {
+ this.push(buf)
+ this.push(null)
+ }
+ }),
+ client.pipeline({
+ path: '/',
+ method: 'PUT'
+ }, (result) => {
+ t.fail('Should not be called')
+ }, (e) => {
+ t.fail('Should not be called')
+ }),
+ new Writable({
+ write (chunk, encoding, callback) {
+ callback()
+ },
+ final (callback) {
+ callback()
+ }
+ }),
+ (err) => {
+ t.type(err, errors.HeadersTimeoutError)
+ }
+ )
+ })
+})
+
+test('client.close should not deadlock', (t) => {
+ t.plan(2)
+
+ const clock = FakeTimers.install()
+ t.teardown(clock.uninstall.bind(clock))
+
+ const orgTimers = { ...timers }
+ Object.assign(timers, { setTimeout, clearTimeout })
+ t.teardown(() => {
+ Object.assign(timers, orgTimers)
+ })
+
+ const server = createServer((req, res) => {
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ bodyTimeout: 200,
+ headersTimeout: 100
+ })
+ t.teardown(client.destroy.bind(client))
+
+ client[kConnect](() => {
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, response) => {
+ t.type(err, errors.HeadersTimeoutError)
+ })
+
+ client.close((err) => {
+ t.error(err)
+ })
+
+ clock.tick(100)
+ })
+ })
+})