fastify 3.26.0 → 3.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,6 +15,7 @@ const ajv = new Ajv({
15
15
  const defaultInitOptions = {
16
16
  connectionTimeout: 0, // 0 sec
17
17
  keepAliveTimeout: 5000, // 5 sec
18
+ forceCloseConnections: false, // keep-alive connections
18
19
  maxRequestsPerSocket: 0, // no limit
19
20
  requestTimeout: 0, // no limit
20
21
  bodyLimit: 1024 * 1024, // 1 MiB
@@ -49,6 +50,7 @@ const schema = {
49
50
  properties: {
50
51
  connectionTimeout: { type: 'integer', default: defaultInitOptions.connectionTimeout },
51
52
  keepAliveTimeout: { type: 'integer', default: defaultInitOptions.keepAliveTimeout },
53
+ forceCloseConnections: { type: 'boolean', default: defaultInitOptions.forceCloseConnections },
52
54
  maxRequestsPerSocket: { type: 'integer', default: defaultInitOptions.maxRequestsPerSocket, nullable: true },
53
55
  requestTimeout: { type: 'integer', default: defaultInitOptions.requestTimeout },
54
56
  bodyLimit: { type: 'integer', default: defaultInitOptions.bodyLimit },
@@ -13,6 +13,7 @@ describes the properties available in that options object.
13
13
  - [`https`](#https)
14
14
  - [`connectionTimeout`](#connectiontimeout)
15
15
  - [`keepAliveTimeout`](#keepalivetimeout)
16
+ - [`forceCloseConnections](#forcecloseconnections)
16
17
  - [`maxRequestsPerSocket`](#maxrequestspersocket)
17
18
  - [`requestTimeout`](#requesttimeout)
18
19
  - [`ignoreTrailingSlash`](#ignoretrailingslash)
@@ -124,6 +125,19 @@ use. Also, when `serverFactory` option is specified, this option is ignored.
124
125
 
125
126
  + Default: `5000` (5 seconds)
126
127
 
128
+ ### `forceCloseConnections`
129
+ <a id="forcecloseconnections"></a>
130
+
131
+ When set to `true` requests with the header `connection: keep-alive` will be
132
+ tracked by the server. Upon [`close`](#close), the server will iterate the
133
+ current persistent connections and [destroy their
134
+ sockets](https://nodejs.org/dist/latest-v16.x/docs/api/net.html#socketdestroyerror).
135
+ This means the server will shutdown immediately instead of waiting for existing
136
+ persistent connections to timeout first. Important: connections are not
137
+ inspected to determine if requests have been completed.
138
+
139
+ + Default: `false`
140
+
127
141
  ### `maxRequestsPerSocket`
128
142
  <a id="factory-max-requests-per-socket"></a>
129
143
 
package/fastify.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const VERSION = '3.26.0'
3
+ const VERSION = '3.27.0'
4
4
 
5
5
  const Avvio = require('avvio')
6
6
  const http = require('http')
@@ -26,7 +26,8 @@ const {
26
26
  kOptions,
27
27
  kPluginNameChain,
28
28
  kSchemaErrorFormatter,
29
- kErrorHandler
29
+ kErrorHandler,
30
+ kKeepAliveConnections
30
31
  } = require('./lib/symbols.js')
31
32
 
32
33
  const { createServer } = require('./lib/server')
@@ -45,6 +46,7 @@ const build404 = require('./lib/fourOhFour')
45
46
  const getSecuredInitialConfig = require('./lib/initialConfigValidation')
46
47
  const override = require('./lib/pluginOverride')
47
48
  const warning = require('./lib/warnings')
49
+ const noopSet = require('./lib/noop-set')
48
50
  const { defaultInitOptions } = getSecuredInitialConfig
49
51
 
50
52
  const {
@@ -133,6 +135,7 @@ function fastify (options) {
133
135
  // Update the options with the fixed values
134
136
  options.connectionTimeout = options.connectionTimeout || defaultInitOptions.connectionTimeout
135
137
  options.keepAliveTimeout = options.keepAliveTimeout || defaultInitOptions.keepAliveTimeout
138
+ options.forceCloseConnections = typeof options.forceCloseConnections === 'boolean' ? options.forceCloseConnections : defaultInitOptions.forceCloseConnections
136
139
  options.maxRequestsPerSocket = options.maxRequestsPerSocket || defaultInitOptions.maxRequestsPerSocket
137
140
  options.requestTimeout = options.requestTimeout || defaultInitOptions.requestTimeout
138
141
  options.logger = logger
@@ -146,6 +149,7 @@ function fastify (options) {
146
149
  options.exposeHeadRoutes = exposeHeadRoutes
147
150
 
148
151
  const initialConfig = getSecuredInitialConfig(options)
152
+ const keepAliveConnections = options.forceCloseConnections === true ? new Set() : noopSet()
149
153
 
150
154
  let constraints = options.constraints
151
155
  if (options.versioning) {
@@ -176,7 +180,8 @@ function fastify (options) {
176
180
  maxParamLength: options.maxParamLength || defaultInitOptions.maxParamLength,
177
181
  caseSensitive: options.caseSensitive,
178
182
  buildPrettyMeta: defaultBuildPrettyMeta
179
- }
183
+ },
184
+ keepAliveConnections
180
185
  })
181
186
 
182
187
  // 404 router, used for handling encapsulated 404 handlers
@@ -200,6 +205,7 @@ function fastify (options) {
200
205
  closing: false,
201
206
  started: false
202
207
  },
208
+ [kKeepAliveConnections]: keepAliveConnections,
203
209
  [kOptions]: options,
204
210
  [kChildren]: [],
205
211
  [kBodyLimit]: bodyLimit,
@@ -375,6 +381,15 @@ function fastify (options) {
375
381
  if (fastify[kState].listening) {
376
382
  // No new TCP connections are accepted
377
383
  instance.server.close(done)
384
+
385
+ for (const conn of fastify[kKeepAliveConnections]) {
386
+ // We must invoke the destroy method instead of merely unreffing
387
+ // the sockets. If we only unref, then the callback passed to
388
+ // `fastify.close` will never be invoked; nor will any of the
389
+ // registered `onClose` hooks.
390
+ conn.destroy()
391
+ fastify[kKeepAliveConnections].delete(conn)
392
+ }
378
393
  } else {
379
394
  done(null)
380
395
  }
@@ -0,0 +1,10 @@
1
+ 'use strict'
2
+
3
+ module.exports = function noopSet () {
4
+ return {
5
+ [Symbol.iterator]: function * () {},
6
+ add () {},
7
+ delete () {},
8
+ has () { return true }
9
+ }
10
+ }
package/lib/route.js CHANGED
@@ -41,6 +41,7 @@ const {
41
41
  } = require('./symbols.js')
42
42
 
43
43
  function buildRouting (options) {
44
+ const { keepAliveConnections } = options
44
45
  const router = FindMyWay(options.config)
45
46
 
46
47
  let avvio
@@ -345,6 +346,17 @@ function buildRouting (options) {
345
346
  }
346
347
  }
347
348
 
349
+ // When server.forceCloseConnections is true, we will collect any requests
350
+ // that have indicated they want persistence so that they can be reaped
351
+ // on server close. Otherwise, the container is a noop container.
352
+ const connHeader = String.prototype.toLowerCase.call(req.headers.connection || '')
353
+ if (connHeader === 'keep-alive') {
354
+ if (keepAliveConnections.has(req.socket) === false) {
355
+ keepAliveConnections.add(req.socket)
356
+ req.socket.on('close', removeTrackedSocket.bind({ keepAliveConnections, socket: req.socket }))
357
+ }
358
+ }
359
+
348
360
  // we revert the changes in defaultRoute
349
361
  if (req.headers[kRequestAcceptVersion] !== undefined) {
350
362
  req.headers['accept-version'] = req.headers[kRequestAcceptVersion]
@@ -485,6 +497,15 @@ function preParsingHookRunner (functions, request, reply, cb) {
485
497
  next(null, request[kRequestPayloadStream])
486
498
  }
487
499
 
500
+ /**
501
+ * Used within the route handler as a `net.Socket.close` event handler.
502
+ * The purpose is to remove a socket from the tracked sockets collection when
503
+ * the socket has naturally timed out.
504
+ */
505
+ function removeTrackedSocket () {
506
+ this.keepAliveConnections.delete(this.socket)
507
+ }
508
+
488
509
  function noop () { }
489
510
 
490
511
  module.exports = { buildRouting, validateBodyLimitOption }
package/lib/symbols.js CHANGED
@@ -44,7 +44,8 @@ const keys = {
44
44
  kPluginNameChain: Symbol('fastify.pluginNameChain'),
45
45
  // This symbol is only meant to be used for fastify tests and should not be used for any other purpose
46
46
  kTestInternals: Symbol('fastify.testInternals'),
47
- kErrorHandler: Symbol('fastify.errorHandler')
47
+ kErrorHandler: Symbol('fastify.errorHandler'),
48
+ kKeepAliveConnections: Symbol('fastify.keepAliveConnections')
48
49
  }
49
50
 
50
51
  module.exports = keys
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fastify",
3
- "version": "3.26.0",
3
+ "version": "3.27.0",
4
4
  "description": "Fast and low overhead web framework, for Node.js",
5
5
  "main": "fastify.js",
6
6
  "type": "commonjs",
@@ -16,7 +16,7 @@
16
16
  "lint:typescript": "eslint -c types/.eslintrc.json types/**/*.d.ts test/types/**/*.test-d.ts",
17
17
  "prepublishOnly": "tap --no-check-coverage test/internals/version.test.js",
18
18
  "test": "npm run lint && npm run unit && npm run test:typescript",
19
- "test:ci": "npm run unit -- --cov --coverage-report=lcovonly && npm run test:typescript",
19
+ "test:ci": "npm run unit -- -R terse --cov --coverage-report=lcovonly && npm run test:typescript",
20
20
  "test:report": "npm run lint && npm run unit:report && npm run test:typescript",
21
21
  "test:typescript": "tsd",
22
22
  "unit": "tap -J test/*.test.js test/*/*.test.js",
@@ -129,6 +129,7 @@
129
129
  "@types/pino": "^6.0.1",
130
130
  "@typescript-eslint/eslint-plugin": "^4.5.0",
131
131
  "@typescript-eslint/parser": "^4.5.0",
132
+ "JSONStream": "^1.3.5",
132
133
  "ajv": "^6.0.0",
133
134
  "ajv-errors": "^1.0.1",
134
135
  "ajv-formats": "^2.1.1",
@@ -157,7 +158,6 @@
157
158
  "hsts": "^2.2.0",
158
159
  "http-errors": "^2.0.0",
159
160
  "ienoopen": "^1.1.0",
160
- "JSONStream": "^1.3.5",
161
161
  "license-checker": "^25.0.1",
162
162
  "pem": "^1.14.4",
163
163
  "proxyquire": "^2.1.3",
@@ -173,7 +173,7 @@
173
173
  "then-sleep": "^1.0.1",
174
174
  "tsd": "^0.19.0",
175
175
  "typescript": "^4.0.2",
176
- "undici": "^3.3.5",
176
+ "undici": "^3.3.6",
177
177
  "x-xss-protection": "^2.0.0",
178
178
  "yup": "^0.32.0"
179
179
  },
@@ -291,3 +291,41 @@ test('Cannot be reopened the closed server has listen callback', async t => {
291
291
  t.ok(err)
292
292
  })
293
293
  })
294
+
295
+ test('shutsdown while keep-alive connections are active (non-async)', t => {
296
+ t.plan(5)
297
+
298
+ const timeoutTime = 2 * 60 * 1000
299
+ const fastify = Fastify({ forceCloseConnections: true })
300
+
301
+ fastify.server.setTimeout(timeoutTime)
302
+ fastify.server.keepAliveTimeout = timeoutTime
303
+
304
+ fastify.get('/', (req, reply) => {
305
+ reply.send({ hello: 'world' })
306
+ })
307
+
308
+ fastify.listen(0, (err, address) => {
309
+ t.error(err)
310
+
311
+ const client = new Client(
312
+ 'http://localhost:' + fastify.server.address().port,
313
+ { keepAliveTimeout: 1 * 60 * 1000 }
314
+ )
315
+ client.request({ path: '/', method: 'GET' }, (err, response) => {
316
+ t.error(err)
317
+ t.equal(client.closed, false)
318
+
319
+ fastify.close((err) => {
320
+ t.error(err)
321
+
322
+ // Due to the nature of the way we reap these keep-alive connections,
323
+ // there hasn't been enough time before the server fully closed in order
324
+ // for the client to have seen the socket get destroyed. The mere fact
325
+ // that we have reached this callback is enough indication that the
326
+ // feature being tested works as designed.
327
+ t.equal(client.closed, false)
328
+ })
329
+ })
330
+ })
331
+ })
@@ -0,0 +1,19 @@
1
+ 'use strict'
2
+
3
+ const tap = require('tap')
4
+ const noopSet = require('../lib/noop-set')
5
+
6
+ tap.test('does a lot of nothing', async t => {
7
+ const aSet = noopSet()
8
+ t.type(aSet, 'object')
9
+
10
+ const item = {}
11
+ aSet.add(item)
12
+ aSet.add({ another: 'item' })
13
+ aSet.delete(item)
14
+ t.equal(aSet.has(item), true)
15
+
16
+ for (const i of aSet) {
17
+ t.fail('should not have any items', i)
18
+ }
19
+ })