fastify
Version:
Fast and low overhead web framework, for Node.js
727 lines (592 loc) • 17.8 kB
JavaScript
const net = require('node:net')
const http = require('node:http')
const { test } = require('tap')
const Fastify = require('..')
const { Client } = require('undici')
const semver = require('semver')
const split = require('split2')
const { sleep } = require('./helper')
test('close callback', t => {
t.plan(4)
const fastify = Fastify()
fastify.addHook('onClose', onClose)
function onClose (instance, done) {
t.type(fastify, instance)
done()
}
fastify.listen({ port: 0 }, err => {
t.error(err)
fastify.close((err) => {
t.error(err)
t.ok('close callback')
})
})
})
test('inside register', t => {
t.plan(5)
const fastify = Fastify()
fastify.register(function (f, opts, done) {
f.addHook('onClose', onClose)
function onClose (instance, done) {
t.ok(instance.prototype === fastify.prototype)
t.equal(instance, f)
done()
}
done()
})
fastify.listen({ port: 0 }, err => {
t.error(err)
fastify.close((err) => {
t.error(err)
t.ok('close callback')
})
})
})
test('close order', t => {
t.plan(5)
const fastify = Fastify()
const order = [1, 2, 3]
fastify.register(function (f, opts, done) {
f.addHook('onClose', (instance, done) => {
t.equal(order.shift(), 1)
done()
})
done()
})
fastify.addHook('onClose', (instance, done) => {
t.equal(order.shift(), 2)
done()
})
fastify.listen({ port: 0 }, err => {
t.error(err)
fastify.close((err) => {
t.error(err)
t.equal(order.shift(), 3)
})
})
})
test('close order - async', async t => {
t.plan(3)
const fastify = Fastify()
const order = [1, 2, 3]
fastify.register(function (f, opts, done) {
f.addHook('onClose', async instance => {
t.equal(order.shift(), 1)
})
done()
})
fastify.addHook('onClose', () => {
t.equal(order.shift(), 2)
})
await fastify.listen({ port: 0 })
await fastify.close()
t.equal(order.shift(), 3)
})
test('should not throw an error if the server is not listening', t => {
t.plan(2)
const fastify = Fastify()
fastify.addHook('onClose', onClose)
function onClose (instance, done) {
t.type(fastify, instance)
done()
}
fastify.close((err) => {
t.error(err)
})
})
test('onClose should keep the context', t => {
t.plan(4)
const fastify = Fastify()
fastify.register(plugin)
function plugin (instance, opts, done) {
instance.decorate('test', true)
instance.addHook('onClose', onClose)
t.ok(instance.prototype === fastify.prototype)
function onClose (i, done) {
t.ok(i.test)
t.equal(i, instance)
done()
}
done()
}
fastify.close((err) => {
t.error(err)
})
})
test('Should return error while closing (promise) - injection', t => {
t.plan(4)
const fastify = Fastify()
fastify.addHook('onClose', (instance, done) => { done() })
fastify.get('/', (req, reply) => {
reply.send({ hello: 'world' })
})
fastify.inject({
method: 'GET',
url: '/'
}, (err, res) => {
t.error(err)
t.equal(res.statusCode, 200)
fastify.close()
process.nextTick(() => {
fastify.inject({
method: 'GET',
url: '/'
}).catch(err => {
t.ok(err)
t.equal(err.code, 'FST_ERR_REOPENED_CLOSE_SERVER')
})
}, 100)
})
})
test('Should return error while closing (callback) - injection', t => {
t.plan(4)
const fastify = Fastify()
fastify.addHook('onClose', (instance, done) => {
setTimeout(done, 150)
})
fastify.get('/', (req, reply) => {
reply.send({ hello: 'world' })
})
fastify.inject({
method: 'GET',
url: '/'
}, (err, res) => {
t.error(err)
t.equal(res.statusCode, 200)
fastify.close()
setTimeout(() => {
fastify.inject({
method: 'GET',
url: '/'
}, (err, res) => {
t.ok(err)
t.equal(err.code, 'FST_ERR_REOPENED_CLOSE_SERVER')
})
}, 100)
})
})
const isNodeVersionGte1819 = semver.gte(process.version, '18.19.0')
test('Current opened connection should continue to work after closing and return "connection: close" header - return503OnClosing: false, skip Node >= v18.19.x', { skip: isNodeVersionGte1819 }, t => {
const fastify = Fastify({
return503OnClosing: false,
forceCloseConnections: false
})
fastify.get('/', (req, reply) => {
fastify.close()
reply.send({ hello: 'world' })
})
fastify.listen({ port: 0 }, err => {
t.error(err)
const port = fastify.server.address().port
const client = net.createConnection({ port }, () => {
client.write('GET / HTTP/1.1\r\nHost: example.com\r\n\r\n')
client.once('data', data => {
t.match(data.toString(), /Connection:\s*keep-alive/i)
t.match(data.toString(), /200 OK/i)
client.write('GET / HTTP/1.1\r\nHost: example.com\r\n\r\n')
client.once('data', data => {
t.match(data.toString(), /Connection:\s*close/i)
t.match(data.toString(), /200 OK/i)
// Test that fastify closes the TCP connection
client.once('close', () => {
t.end()
})
})
})
})
})
})
test('Current opened connection should NOT continue to work after closing and return "connection: close" header - return503OnClosing: false, skip Node < v18.19.x', { skip: !isNodeVersionGte1819 }, t => {
t.plan(4)
const fastify = Fastify({
return503OnClosing: false,
forceCloseConnections: false
})
fastify.get('/', (req, reply) => {
fastify.close()
reply.send({ hello: 'world' })
})
fastify.listen({ port: 0 }, err => {
t.error(err)
const port = fastify.server.address().port
const client = net.createConnection({ port }, () => {
client.write('GET / HTTP/1.1\r\nHost: example.com\r\n\r\n')
client.on('error', function () {
// Depending on the Operating System
// the socket could error or not.
// However, it will always be closed.
})
client.on('close', function () {
t.pass('close')
})
client.once('data', data => {
t.match(data.toString(), /Connection:\s*keep-alive/i)
t.match(data.toString(), /200 OK/i)
client.write('GET / HTTP/1.1\r\nHost: example.com\r\n\r\n')
})
})
})
})
test('Current opened connection should not accept new incoming connections', t => {
t.plan(3)
const fastify = Fastify({ forceCloseConnections: false })
fastify.get('/', (req, reply) => {
fastify.close()
setTimeout(() => {
reply.send({ hello: 'world' })
}, 250)
})
fastify.listen({ port: 0 }, err => {
t.error(err)
const instance = new Client('http://localhost:' + fastify.server.address().port)
instance.request({ path: '/', method: 'GET' }).then(data => {
t.equal(data.statusCode, 200)
})
instance.request({ path: '/', method: 'GET' }).then(data => {
t.equal(data.statusCode, 503)
})
})
})
test('rejected incoming connections should be logged', t => {
t.plan(2)
const stream = split(JSON.parse)
const fastify = Fastify({
forceCloseConnections: false,
logger: {
stream,
level: 'info'
}
})
const messages = []
stream.on('data', message => {
messages.push(message)
})
fastify.get('/', (req, reply) => {
fastify.close()
setTimeout(() => {
reply.send({ hello: 'world' })
}, 250)
})
fastify.listen({ port: 0 }, err => {
t.error(err)
const instance = new Client('http://localhost:' + fastify.server.address().port)
// initial request to trigger close
instance.request({ path: '/', method: 'GET' })
// subsequent request should be rejected
instance.request({ path: '/', method: 'GET' }).then(() => {
t.ok(messages.find(message => message.msg.includes('request aborted')))
})
})
})
test('Cannot be reopened the closed server without listen callback', async t => {
t.plan(2)
const fastify = Fastify()
await fastify.listen({ port: 0 })
await fastify.close()
try {
await fastify.listen({ port: 0 })
} catch (err) {
t.ok(err)
t.equal(err.code, 'FST_ERR_REOPENED_CLOSE_SERVER')
}
})
test('Cannot be reopened the closed server has listen callback', async t => {
t.plan(2)
const fastify = Fastify()
await fastify.listen({ port: 0 })
await fastify.close()
await new Promise((resolve, reject) => {
fastify.listen({ port: 0 }, err => {
reject(err)
})
}).catch(err => {
t.equal(err.code, 'FST_ERR_REOPENED_CLOSE_SERVER')
t.ok(err)
})
})
const server = http.createServer()
const noSupport = typeof server.closeAllConnections !== 'function'
test('shutsdown while keep-alive connections are active (non-async, native)', { skip: noSupport }, t => {
t.plan(5)
const timeoutTime = 2 * 60 * 1000
const fastify = Fastify({ forceCloseConnections: true })
fastify.server.setTimeout(timeoutTime)
fastify.server.keepAliveTimeout = timeoutTime
fastify.get('/', (req, reply) => {
reply.send({ hello: 'world' })
})
fastify.listen({ port: 0 }, (err, address) => {
t.error(err)
const client = new Client(
'http://localhost:' + fastify.server.address().port,
{ keepAliveTimeout: 1 * 60 * 1000 }
)
client.request({ path: '/', method: 'GET' }, (err, response) => {
t.error(err)
t.equal(client.closed, false)
fastify.close((err) => {
t.error(err)
// Due to the nature of the way we reap these keep-alive connections,
// there hasn't been enough time before the server fully closed in order
// for the client to have seen the socket get destroyed. The mere fact
// that we have reached this callback is enough indication that the
// feature being tested works as designed.
t.equal(client.closed, false)
})
})
})
})
test('shutsdown while keep-alive connections are active (non-async, idle, native)', { skip: noSupport }, t => {
t.plan(5)
const timeoutTime = 2 * 60 * 1000
const fastify = Fastify({ forceCloseConnections: 'idle' })
fastify.server.setTimeout(timeoutTime)
fastify.server.keepAliveTimeout = timeoutTime
fastify.get('/', (req, reply) => {
reply.send({ hello: 'world' })
})
fastify.listen({ port: 0 }, (err, address) => {
t.error(err)
const client = new Client(
'http://localhost:' + fastify.server.address().port,
{ keepAliveTimeout: 1 * 60 * 1000 }
)
client.request({ path: '/', method: 'GET' }, (err, response) => {
t.error(err)
t.equal(client.closed, false)
fastify.close((err) => {
t.error(err)
// Due to the nature of the way we reap these keep-alive connections,
// there hasn't been enough time before the server fully closed in order
// for the client to have seen the socket get destroyed. The mere fact
// that we have reached this callback is enough indication that the
// feature being tested works as designed.
t.equal(client.closed, false)
})
})
})
})
test('triggers on-close hook in the right order with multiple bindings', async t => {
const expectedOrder = [1, 2, 3]
const order = []
const fastify = Fastify()
t.plan(1)
// Follows LIFO
fastify.addHook('onClose', () => {
order.push(2)
})
fastify.addHook('onClose', () => {
order.push(1)
})
await fastify.listen({ port: 0 })
await new Promise((resolve, reject) => {
setTimeout(() => {
fastify.close(err => {
order.push(3)
t.match(order, expectedOrder)
if (err) t.error(err)
else resolve()
})
}, 2000)
})
})
test('triggers on-close hook in the right order with multiple bindings (forceCloseConnections - idle)', { skip: noSupport }, async t => {
const expectedPayload = { hello: 'world' }
const timeoutTime = 2 * 60 * 1000
const expectedOrder = [1, 2]
const order = []
const fastify = Fastify({ forceCloseConnections: 'idle' })
fastify.server.setTimeout(timeoutTime)
fastify.server.keepAliveTimeout = timeoutTime
fastify.get('/', async (req, reply) => {
await new Promise((resolve) => {
setTimeout(resolve, 1000)
})
return expectedPayload
})
fastify.addHook('onClose', () => {
order.push(1)
})
await fastify.listen({ port: 0 })
const addresses = fastify.addresses()
const testPlan = (addresses.length * 2) + 1
t.plan(testPlan)
for (const addr of addresses) {
const { family, address, port } = addr
const host = family === 'IPv6' ? `[${address}]` : address
const client = new Client(`http://${host}:${port}`, {
keepAliveTimeout: 1 * 60 * 1000
})
client.request({ path: '/', method: 'GET' })
.then((res) => res.body.json(), err => t.error(err))
.then(json => {
t.match(json, expectedPayload, 'should payload match')
t.notOk(client.closed, 'should client not be closed')
}, err => t.error(err))
}
await new Promise((resolve, reject) => {
setTimeout(() => {
fastify.close(err => {
order.push(2)
t.match(order, expectedOrder)
if (err) t.error(err)
else resolve()
})
}, 2000)
})
})
test('triggers on-close hook in the right order with multiple bindings (forceCloseConnections - true)', { skip: noSupport }, async t => {
const expectedPayload = { hello: 'world' }
const timeoutTime = 2 * 60 * 1000
const expectedOrder = [1, 2]
const order = []
const fastify = Fastify({ forceCloseConnections: true })
fastify.server.setTimeout(timeoutTime)
fastify.server.keepAliveTimeout = timeoutTime
fastify.get('/', async (req, reply) => {
await new Promise((resolve) => {
setTimeout(resolve, 1000)
})
return expectedPayload
})
fastify.addHook('onClose', () => {
order.push(1)
})
await fastify.listen({ port: 0 })
const addresses = fastify.addresses()
const testPlan = (addresses.length * 2) + 1
t.plan(testPlan)
for (const addr of addresses) {
const { family, address, port } = addr
const host = family === 'IPv6' ? `[${address}]` : address
const client = new Client(`http://${host}:${port}`, {
keepAliveTimeout: 1 * 60 * 1000
})
client.request({ path: '/', method: 'GET' })
.then((res) => res.body.json(), err => t.error(err))
.then(json => {
t.match(json, expectedPayload, 'should payload match')
t.notOk(client.closed, 'should client not be closed')
}, err => t.error(err))
}
await new Promise((resolve, reject) => {
setTimeout(() => {
fastify.close(err => {
order.push(2)
t.match(order, expectedOrder)
if (err) t.error(err)
else resolve()
})
}, 2000)
})
})
test('shutsdown while keep-alive connections are active (non-async, custom)', t => {
t.plan(5)
const timeoutTime = 2 * 60 * 1000
const fastify = Fastify({
forceCloseConnections: true,
serverFactory (handler) {
const server = http.createServer(handler)
server.closeAllConnections = null
return server
}
})
fastify.server.setTimeout(timeoutTime)
fastify.server.keepAliveTimeout = timeoutTime
fastify.get('/', (req, reply) => {
reply.send({ hello: 'world' })
})
fastify.listen({ port: 0 }, (err, address) => {
t.error(err)
const client = new Client(
'http://localhost:' + fastify.server.address().port,
{ keepAliveTimeout: 1 * 60 * 1000 }
)
client.request({ path: '/', method: 'GET' }, (err, response) => {
t.error(err)
t.equal(client.closed, false)
fastify.close((err) => {
t.error(err)
// Due to the nature of the way we reap these keep-alive connections,
// there hasn't been enough time before the server fully closed in order
// for the client to have seen the socket get destroyed. The mere fact
// that we have reached this callback is enough indication that the
// feature being tested works as designed.
t.equal(client.closed, false)
})
})
})
})
test('preClose callback', t => {
t.plan(5)
const fastify = Fastify()
fastify.addHook('onClose', onClose)
let preCloseCalled = false
function onClose (instance, done) {
t.equal(preCloseCalled, true)
done()
}
fastify.addHook('preClose', preClose)
function preClose (done) {
t.type(this, fastify)
preCloseCalled = true
done()
}
fastify.listen({ port: 0 }, err => {
t.error(err)
fastify.close((err) => {
t.error(err)
t.ok('close callback')
})
})
})
test('preClose async', async t => {
t.plan(2)
const fastify = Fastify()
fastify.addHook('onClose', onClose)
let preCloseCalled = false
async function onClose () {
t.equal(preCloseCalled, true)
}
fastify.addHook('preClose', preClose)
async function preClose () {
preCloseCalled = true
t.type(this, fastify)
}
await fastify.listen({ port: 0 })
await fastify.close()
})
test('preClose execution order', t => {
t.plan(4)
const fastify = Fastify()
const order = []
fastify.addHook('onClose', onClose)
function onClose (instance, done) {
t.same(order, [1, 2, 3])
done()
}
fastify.addHook('preClose', (done) => {
setTimeout(function () {
order.push(1)
done()
}, 200)
})
fastify.addHook('preClose', async () => {
await sleep(100)
order.push(2)
})
fastify.addHook('preClose', (done) => {
setTimeout(function () {
order.push(3)
done()
}, 100)
})
fastify.listen({ port: 0 }, err => {
t.error(err)
fastify.close((err) => {
t.error(err)
t.ok('close callback')
})
})
})