Back to Repositories

Testing HTTP Pipelining Server Shutdown Behavior in Fastify

This test suite validates Fastify’s server closing behavior during pipelined HTTP requests. It specifically examines how the server handles in-flight requests and connection management when the server is shutting down.

Test Coverage Overview

The test suite covers critical server shutdown scenarios with HTTP pipelining enabled. Key areas tested include:

  • 503 response generation during server shutdown
  • Socket closure behavior with different configuration settings
  • Multiple concurrent request handling during shutdown
  • Response status code verification for pipelined requests

Implementation Analysis

The testing approach uses Node’s native test framework combined with undici HTTP client for pipelined requests. It implements two main test cases examining different server configurations:

  • return503OnClosing enabled scenario with status code verification
  • Abrupt connection closure testing with return503OnClosing disabled

Technical Details

Testing infrastructure includes:

  • Node.js native test runner
  • Undici HTTP client with pipelining support
  • Fastify server with configurable shutdown options
  • Dynamic port allocation for test isolation
  • Promise.all and Promise.allSettled for concurrent request handling

Best Practices Demonstrated

The test suite exemplifies several testing best practices:

  • Isolated test cases with proper cleanup
  • Comprehensive assertion coverage for both success and failure scenarios
  • Configuration-based behavior testing
  • Async/await pattern usage for clear test flow
  • Edge case handling with multiple concurrent requests

fastify/fastify

test/close-pipelining.test.js

            
'use strict'

const { test } = require('node:test')
const Fastify = require('..')
const { Client } = require('undici')

test('Should return 503 while closing - pipelining', async t => {
  const fastify = Fastify({
    return503OnClosing: true,
    forceCloseConnections: false
  })

  fastify.get('/', async (req, reply) => {
    fastify.close()
    return { hello: 'world' }
  })

  await fastify.listen({ port: 0 })

  const instance = new Client('http://localhost:' + fastify.server.address().port, {
    pipelining: 2
  })

  const codes = [200, 200, 503]
  const responses = await Promise.all([
    instance.request({ path: '/', method: 'GET' }),
    instance.request({ path: '/', method: 'GET' }),
    instance.request({ path: '/', method: 'GET' })
  ])
  const actual = responses.map(r => r.statusCode)
  t.assert.deepStrictEqual(actual, codes)

  await instance.close()
})

test('Should close the socket abruptly - pipelining - return503OnClosing: false', async t => {
  // Since Node v20, we will always invoke server.closeIdleConnections()
  // therefore our socket will be closed
  const fastify = Fastify({
    return503OnClosing: false,
    forceCloseConnections: false
  })

  fastify.get('/', async (req, reply) => {
    reply.send({ hello: 'world' })
    fastify.close()
  })

  await fastify.listen({ port: 0 })

  const instance = new Client('http://localhost:' + fastify.server.address().port, {
    pipelining: 2
  })

  const responses = await Promise.allSettled([
    instance.request({ path: '/', method: 'GET' }),
    instance.request({ path: '/', method: 'GET' }),
    instance.request({ path: '/', method: 'GET' }),
    instance.request({ path: '/', method: 'GET' })
  ])

  t.assert.strictEqual(responses[0].status, 'fulfilled')
  t.assert.strictEqual(responses[1].status, 'fulfilled')
  t.assert.strictEqual(responses[2].status, 'rejected')
  t.assert.strictEqual(responses[3].status, 'rejected')

  await instance.close()
})