strong-tie/inbound-calls
0
1'use strict'2 3const net = require('node:net')4const http = require('node:http')5const { test } = require('tap')6const Fastify = require('..')7const { Client } = require('undici')8const split = require('split2')9const { sleep } = require('./helper')10 11test('close callback', t => {12 t.plan(7)13 const fastify = Fastify()14 fastify.addHook('onClose', onClose)15 function onClose (instance, done) {16 t.type(fastify, this)17 t.type(fastify, instance)18 t.equal(fastify, this)19 t.equal(fastify, instance)20 done()21 }22 23 fastify.listen({ port: 0 }, err => {24 t.error(err)25 26 fastify.close((err) => {27 t.error(err)28 t.ok('close callback')29 })30 })31})32 33test('inside register', t => {34 t.plan(5)35 const fastify = Fastify()36 fastify.register(function (f, opts, done) {37 f.addHook('onClose', onClose)38 function onClose (instance, done) {39 t.ok(instance.prototype === fastify.prototype)40 t.equal(instance, f)41 done()42 }43 44 done()45 })46 47 fastify.listen({ port: 0 }, err => {48 t.error(err)49 50 fastify.close((err) => {51 t.error(err)52 t.ok('close callback')53 })54 })55})56 57test('close order', t => {58 t.plan(5)59 const fastify = Fastify()60 const order = [1, 2, 3]61 62 fastify.register(function (f, opts, done) {63 f.addHook('onClose', (instance, done) => {64 t.equal(order.shift(), 1)65 done()66 })67 68 done()69 })70 71 fastify.addHook('onClose', (instance, done) => {72 t.equal(order.shift(), 2)73 done()74 })75 76 fastify.listen({ port: 0 }, err => {77 t.error(err)78 79 fastify.close((err) => {80 t.error(err)81 t.equal(order.shift(), 3)82 })83 })84})85 86test('close order - async', async t => {87 t.plan(3)88 const fastify = Fastify()89 const order = [1, 2, 3]90 91 fastify.register(function (f, opts, done) {92 f.addHook('onClose', async instance => {93 t.equal(order.shift(), 1)94 })95 96 done()97 })98 99 fastify.addHook('onClose', () => {100 t.equal(order.shift(), 2)101 })102 103 await fastify.listen({ port: 0 })104 await fastify.close()105 106 t.equal(order.shift(), 3)107})108 109test('should not throw an error if the server is not listening', t => {110 t.plan(2)111 const fastify = Fastify()112 fastify.addHook('onClose', onClose)113 function onClose (instance, done) {114 t.type(fastify, instance)115 done()116 }117 118 fastify.close((err) => {119 t.error(err)120 })121})122 123test('onClose should keep the context', t => {124 t.plan(4)125 const fastify = Fastify()126 fastify.register(plugin)127 128 function plugin (instance, opts, done) {129 instance.decorate('test', true)130 instance.addHook('onClose', onClose)131 t.ok(instance.prototype === fastify.prototype)132 133 function onClose (i, done) {134 t.ok(i.test)135 t.equal(i, instance)136 done()137 }138 139 done()140 }141 142 fastify.close((err) => {143 t.error(err)144 })145})146 147test('Should return error while closing (promise) - injection', t => {148 t.plan(4)149 const fastify = Fastify()150 151 fastify.addHook('onClose', (instance, done) => { done() })152 153 fastify.get('/', (req, reply) => {154 reply.send({ hello: 'world' })155 })156 157 fastify.inject({158 method: 'GET',159 url: '/'160 }, (err, res) => {161 t.error(err)162 t.equal(res.statusCode, 200)163 fastify.close()164 165 process.nextTick(() => {166 fastify.inject({167 method: 'GET',168 url: '/'169 }).catch(err => {170 t.ok(err)171 t.equal(err.code, 'FST_ERR_REOPENED_CLOSE_SERVER')172 })173 }, 100)174 })175})176 177test('Should return error while closing (callback) - injection', t => {178 t.plan(4)179 const fastify = Fastify()180 181 fastify.addHook('onClose', (instance, done) => {182 setTimeout(done, 150)183 })184 185 fastify.get('/', (req, reply) => {186 reply.send({ hello: 'world' })187 })188 189 fastify.inject({190 method: 'GET',191 url: '/'192 }, (err, res) => {193 t.error(err)194 t.equal(res.statusCode, 200)195 fastify.close()196 197 setTimeout(() => {198 fastify.inject({199 method: 'GET',200 url: '/'201 }, (err, res) => {202 t.ok(err)203 t.equal(err.code, 'FST_ERR_REOPENED_CLOSE_SERVER')204 })205 }, 100)206 })207})208 209test('Current opened connection should NOT continue to work after closing and return "connection: close" header - return503OnClosing: false', t => {210 t.plan(4)211 const fastify = Fastify({212 return503OnClosing: false,213 forceCloseConnections: false214 })215 216 fastify.get('/', (req, reply) => {217 fastify.close()218 reply.send({ hello: 'world' })219 })220 221 fastify.listen({ port: 0 }, err => {222 t.error(err)223 224 const port = fastify.server.address().port225 const client = net.createConnection({ port }, () => {226 client.write('GET / HTTP/1.1\r\nHost: example.com\r\n\r\n')227 228 client.on('error', function () {229 // Depending on the Operating System230 // the socket could error or not.231 // However, it will always be closed.232 })233 234 client.on('close', function () {235 t.pass('close')236 })237 238 client.once('data', data => {239 t.match(data.toString(), /Connection:\s*keep-alive/i)240 t.match(data.toString(), /200 OK/i)241 242 client.write('GET / HTTP/1.1\r\nHost: example.com\r\n\r\n')243 })244 })245 })246})247 248test('Current opened connection should not accept new incoming connections', t => {249 t.plan(3)250 const fastify = Fastify({ forceCloseConnections: false })251 fastify.get('/', (req, reply) => {252 fastify.close()253 setTimeout(() => {254 reply.send({ hello: 'world' })255 }, 250)256 })257 258 fastify.listen({ port: 0 }, err => {259 t.error(err)260 const instance = new Client('http://localhost:' + fastify.server.address().port)261 instance.request({ path: '/', method: 'GET' }).then(data => {262 t.equal(data.statusCode, 200)263 })264 instance.request({ path: '/', method: 'GET' }).then(data => {265 t.equal(data.statusCode, 503)266 })267 })268})269 270test('rejected incoming connections should be logged', t => {271 t.plan(2)272 const stream = split(JSON.parse)273 const fastify = Fastify({274 forceCloseConnections: false,275 logger: {276 stream,277 level: 'info'278 }279 })280 281 const messages = []282 stream.on('data', message => {283 messages.push(message)284 })285 fastify.get('/', (req, reply) => {286 fastify.close()287 setTimeout(() => {288 reply.send({ hello: 'world' })289 }, 250)290 })291 292 fastify.listen({ port: 0 }, err => {293 t.error(err)294 const instance = new Client('http://localhost:' + fastify.server.address().port)295 // initial request to trigger close296 instance.request({ path: '/', method: 'GET' })297 // subsequent request should be rejected298 instance.request({ path: '/', method: 'GET' }).then(() => {299 t.ok(messages.find(message => message.msg.includes('request aborted')))300 })301 })302})303 304test('Cannot be reopened the closed server without listen callback', async t => {305 t.plan(2)306 const fastify = Fastify()307 308 await fastify.listen({ port: 0 })309 await fastify.close()310 311 try {312 await fastify.listen({ port: 0 })313 } catch (err) {314 t.ok(err)315 t.equal(err.code, 'FST_ERR_REOPENED_CLOSE_SERVER')316 }317})318 319test('Cannot be reopened the closed server has listen callback', async t => {320 t.plan(2)321 const fastify = Fastify()322 323 await fastify.listen({ port: 0 })324 await fastify.close()325 326 await new Promise((resolve, reject) => {327 fastify.listen({ port: 0 }, err => {328 reject(err)329 })330 }).catch(err => {331 t.equal(err.code, 'FST_ERR_REOPENED_CLOSE_SERVER')332 t.ok(err)333 })334})335 336const server = http.createServer()337const noSupport = typeof server.closeAllConnections !== 'function'338 339test('shutsdown while keep-alive connections are active (non-async, native)', { skip: noSupport }, t => {340 t.plan(5)341 342 const timeoutTime = 2 * 60 * 1000343 const fastify = Fastify({ forceCloseConnections: true })344 345 fastify.server.setTimeout(timeoutTime)346 fastify.server.keepAliveTimeout = timeoutTime347 348 fastify.get('/', (req, reply) => {349 reply.send({ hello: 'world' })350 })351 352 fastify.listen({ port: 0 }, (err, address) => {353 t.error(err)354 355 const client = new Client(356 'http://localhost:' + fastify.server.address().port,357 { keepAliveTimeout: 1 * 60 * 1000 }358 )359 client.request({ path: '/', method: 'GET' }, (err, response) => {360 t.error(err)361 t.equal(client.closed, false)362 363 fastify.close((err) => {364 t.error(err)365 366 // Due to the nature of the way we reap these keep-alive connections,367 // there hasn't been enough time before the server fully closed in order368 // for the client to have seen the socket get destroyed. The mere fact369 // that we have reached this callback is enough indication that the370 // feature being tested works as designed.371 t.equal(client.closed, false)372 })373 })374 })375})376 377test('shutsdown while keep-alive connections are active (non-async, idle, native)', { skip: noSupport }, t => {378 t.plan(5)379 380 const timeoutTime = 2 * 60 * 1000381 const fastify = Fastify({ forceCloseConnections: 'idle' })382 383 fastify.server.setTimeout(timeoutTime)384 fastify.server.keepAliveTimeout = timeoutTime385 386 fastify.get('/', (req, reply) => {387 reply.send({ hello: 'world' })388 })389 390 fastify.listen({ port: 0 }, (err, address) => {391 t.error(err)392 393 const client = new Client(394 'http://localhost:' + fastify.server.address().port,395 { keepAliveTimeout: 1 * 60 * 1000 }396 )397 client.request({ path: '/', method: 'GET' }, (err, response) => {398 t.error(err)399 t.equal(client.closed, false)400 401 fastify.close((err) => {402 t.error(err)403 404 // Due to the nature of the way we reap these keep-alive connections,405 // there hasn't been enough time before the server fully closed in order406 // for the client to have seen the socket get destroyed. The mere fact407 // that we have reached this callback is enough indication that the408 // feature being tested works as designed.409 t.equal(client.closed, false)410 })411 })412 })413})414 415test('triggers on-close hook in the right order with multiple bindings', async t => {416 const expectedOrder = [1, 2, 3]417 const order = []418 const fastify = Fastify()419 420 t.plan(1)421 422 // Follows LIFO423 fastify.addHook('onClose', () => {424 order.push(2)425 })426 427 fastify.addHook('onClose', () => {428 order.push(1)429 })430 431 await fastify.listen({ port: 0 })432 433 await new Promise((resolve, reject) => {434 setTimeout(() => {435 fastify.close(err => {436 order.push(3)437 t.match(order, expectedOrder)438 439 if (err) t.error(err)440 else resolve()441 })442 }, 2000)443 })444})445 446test('triggers on-close hook in the right order with multiple bindings (forceCloseConnections - idle)', { skip: noSupport }, async t => {447 const expectedPayload = { hello: 'world' }448 const timeoutTime = 2 * 60 * 1000449 const expectedOrder = [1, 2]450 const order = []451 const fastify = Fastify({ forceCloseConnections: 'idle' })452 453 fastify.server.setTimeout(timeoutTime)454 fastify.server.keepAliveTimeout = timeoutTime455 456 fastify.get('/', async (req, reply) => {457 await new Promise((resolve) => {458 setTimeout(resolve, 1000)459 })460 461 return expectedPayload462 })463 464 fastify.addHook('onClose', () => {465 order.push(1)466 })467 468 await fastify.listen({ port: 0 })469 const addresses = fastify.addresses()470 const testPlan = (addresses.length * 2) + 1471 472 t.plan(testPlan)473 474 for (const addr of addresses) {475 const { family, address, port } = addr476 const host = family === 'IPv6' ? `[${address}]` : address477 const client = new Client(`http://${host}:${port}`, {478 keepAliveTimeout: 1 * 60 * 1000479 })480 481 client.request({ path: '/', method: 'GET' })482 .then((res) => res.body.json(), err => t.error(err))483 .then(json => {484 t.match(json, expectedPayload, 'should payload match')485 t.notOk(client.closed, 'should client not be closed')486 }, err => t.error(err))487 }488 489 await new Promise((resolve, reject) => {490 setTimeout(() => {491 fastify.close(err => {492 order.push(2)493 t.match(order, expectedOrder)494 495 if (err) t.error(err)496 else resolve()497 })498 }, 2000)499 })500})501 502test('triggers on-close hook in the right order with multiple bindings (forceCloseConnections - true)', { skip: noSupport }, async t => {503 const expectedPayload = { hello: 'world' }504 const timeoutTime = 2 * 60 * 1000505 const expectedOrder = [1, 2]506 const order = []507 const fastify = Fastify({ forceCloseConnections: true })508 509 fastify.server.setTimeout(timeoutTime)510 fastify.server.keepAliveTimeout = timeoutTime511 512 fastify.get('/', async (req, reply) => {513 await new Promise((resolve) => {514 setTimeout(resolve, 1000)515 })516 517 return expectedPayload518 })519 520 fastify.addHook('onClose', () => {521 order.push(1)522 })523 524 await fastify.listen({ port: 0 })525 const addresses = fastify.addresses()526 const testPlan = (addresses.length * 2) + 1527 528 t.plan(testPlan)529 530 for (const addr of addresses) {531 const { family, address, port } = addr532 const host = family === 'IPv6' ? `[${address}]` : address533 const client = new Client(`http://${host}:${port}`, {534 keepAliveTimeout: 1 * 60 * 1000535 })536 537 client.request({ path: '/', method: 'GET' })538 .then((res) => res.body.json(), err => t.error(err))539 .then(json => {540 t.match(json, expectedPayload, 'should payload match')541 t.notOk(client.closed, 'should client not be closed')542 }, err => t.error(err))543 }544 545 await new Promise((resolve, reject) => {546 setTimeout(() => {547 fastify.close(err => {548 order.push(2)549 t.match(order, expectedOrder)550 551 if (err) t.error(err)552 else resolve()553 })554 }, 2000)555 })556})557 558test('shutsdown while keep-alive connections are active (non-async, custom)', t => {559 t.plan(5)560 561 const timeoutTime = 2 * 60 * 1000562 const fastify = Fastify({563 forceCloseConnections: true,564 serverFactory (handler) {565 const server = http.createServer(handler)566 567 server.closeAllConnections = null568 569 return server570 }571 })572 573 fastify.server.setTimeout(timeoutTime)574 fastify.server.keepAliveTimeout = timeoutTime575 576 fastify.get('/', (req, reply) => {577 reply.send({ hello: 'world' })578 })579 580 fastify.listen({ port: 0 }, (err, address) => {581 t.error(err)582 583 const client = new Client(584 'http://localhost:' + fastify.server.address().port,585 { keepAliveTimeout: 1 * 60 * 1000 }586 )587 client.request({ path: '/', method: 'GET' }, (err, response) => {588 t.error(err)589 t.equal(client.closed, false)590 591 fastify.close((err) => {592 t.error(err)593 594 // Due to the nature of the way we reap these keep-alive connections,595 // there hasn't been enough time before the server fully closed in order596 // for the client to have seen the socket get destroyed. The mere fact597 // that we have reached this callback is enough indication that the598 // feature being tested works as designed.599 t.equal(client.closed, false)600 })601 })602 })603})604 605test('preClose callback', t => {606 t.plan(5)607 const fastify = Fastify()608 fastify.addHook('onClose', onClose)609 let preCloseCalled = false610 function onClose (instance, done) {611 t.equal(preCloseCalled, true)612 done()613 }614 fastify.addHook('preClose', preClose)615 616 function preClose (done) {617 t.type(this, fastify)618 preCloseCalled = true619 done()620 }621 622 fastify.listen({ port: 0 }, err => {623 t.error(err)624 625 fastify.close((err) => {626 t.error(err)627 t.ok('close callback')628 })629 })630})631 632test('preClose async', async t => {633 t.plan(2)634 const fastify = Fastify()635 fastify.addHook('onClose', onClose)636 let preCloseCalled = false637 async function onClose () {638 t.equal(preCloseCalled, true)639 }640 fastify.addHook('preClose', preClose)641 642 async function preClose () {643 preCloseCalled = true644 t.type(this, fastify)645 }646 647 await fastify.listen({ port: 0 })648 649 await fastify.close()650})651 652test('preClose execution order', t => {653 t.plan(4)654 const fastify = Fastify()655 const order = []656 fastify.addHook('onClose', onClose)657 function onClose (instance, done) {658 t.same(order, [1, 2, 3])659 done()660 }661 662 fastify.addHook('preClose', (done) => {663 setTimeout(function () {664 order.push(1)665 done()666 }, 200)667 })668 669 fastify.addHook('preClose', async () => {670 await sleep(100)671 order.push(2)672 })673 674 fastify.addHook('preClose', (done) => {675 setTimeout(function () {676 order.push(3)677 done()678 }, 100)679 })680 681 fastify.listen({ port: 0 }, err => {682 t.error(err)683 684 fastify.close((err) => {685 t.error(err)686 t.ok('close callback')687 })688 })689})690 