fastify 5.8.5 → 5.10.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.
Files changed (126) hide show
  1. package/PROJECT_CHARTER.md +1 -1
  2. package/README.md +7 -10
  3. package/SECURITY.md +1 -1
  4. package/SPONSORS.md +6 -4
  5. package/build/build-validation.js +2 -2
  6. package/docs/Guides/Database.md +0 -28
  7. package/docs/Guides/Delay-Accepting-Requests.md +1 -1
  8. package/docs/Guides/Ecosystem.md +23 -9
  9. package/docs/Guides/Getting-Started.md +2 -2
  10. package/docs/Guides/Migration-Guide-V4.md +2 -2
  11. package/docs/Guides/Migration-Guide-V5.md +1 -1
  12. package/docs/Guides/Plugins-Guide.md +1 -1
  13. package/docs/Guides/Prototype-Poisoning.md +3 -3
  14. package/docs/Guides/Serverless.md +8 -15
  15. package/docs/Guides/Style-Guide.md +1 -1
  16. package/docs/Guides/Write-Plugin.md +1 -1
  17. package/docs/Reference/Encapsulation.md +27 -26
  18. package/docs/Reference/Errors.md +12 -4
  19. package/docs/Reference/HTTP2.md +10 -10
  20. package/docs/Reference/Hooks.md +5 -5
  21. package/docs/Reference/Index.md +14 -16
  22. package/docs/Reference/LTS.md +12 -13
  23. package/docs/Reference/Lifecycle.md +9 -8
  24. package/docs/Reference/Logging.md +47 -39
  25. package/docs/Reference/Middleware.md +21 -25
  26. package/docs/Reference/Principles.md +2 -2
  27. package/docs/Reference/Reply.md +6 -1
  28. package/docs/Reference/Request.md +29 -18
  29. package/docs/Reference/Routes.md +5 -2
  30. package/docs/Reference/Server.md +138 -11
  31. package/docs/Reference/Type-Providers.md +53 -9
  32. package/docs/Reference/TypeScript.md +3 -3
  33. package/docs/Reference/Validation-and-Serialization.md +15 -2
  34. package/docs/Reference/Warnings.md +11 -6
  35. package/eslint.config.js +7 -2
  36. package/fastify.d.ts +13 -3
  37. package/fastify.js +60 -31
  38. package/lib/content-type-parser.js +2 -0
  39. package/lib/content-type.js +34 -1
  40. package/lib/context.js +0 -3
  41. package/lib/decorate.js +11 -3
  42. package/lib/error-handler.js +10 -28
  43. package/lib/error-serializer.js +59 -59
  44. package/lib/errors.js +23 -1
  45. package/lib/four-oh-four.js +22 -19
  46. package/lib/handle-request.js +12 -5
  47. package/lib/log-controller.js +169 -0
  48. package/lib/logger-factory.js +25 -4
  49. package/lib/plugin-override.js +2 -1
  50. package/lib/plugin-utils.js +5 -5
  51. package/lib/reply.js +96 -50
  52. package/lib/req-id-gen-factory.js +4 -1
  53. package/lib/request.js +16 -6
  54. package/lib/route.js +47 -41
  55. package/lib/schema-controller.js +1 -1
  56. package/lib/schemas.js +37 -30
  57. package/lib/symbols.js +4 -2
  58. package/lib/validation.js +10 -13
  59. package/lib/warnings.js +22 -4
  60. package/package.json +15 -17
  61. package/scripts/validate-ecosystem-links.js +1 -0
  62. package/test/bundler/esbuild/package.json +1 -1
  63. package/test/close-pipelining.test.js +1 -2
  64. package/test/content-type.test.js +20 -0
  65. package/test/custom-http-server.test.js +38 -0
  66. package/test/decorator-instance-properties.test.js +63 -0
  67. package/test/diagnostics-channel/async-error-handler.test.js +74 -0
  68. package/test/genReqId.test.js +24 -0
  69. package/test/hooks.test.js +94 -0
  70. package/test/http-methods/get.test.js +1 -1
  71. package/test/http2/plain.test.js +135 -0
  72. package/test/http2/secure-with-fallback.test.js +1 -1
  73. package/test/https/https.test.js +1 -2
  74. package/test/internals/errors.test.js +31 -1
  75. package/test/internals/logger.test.js +322 -0
  76. package/test/internals/plugin.test.js +3 -1
  77. package/test/internals/reply.test.js +35 -4
  78. package/test/internals/request.test.js +37 -10
  79. package/test/internals/schema-controller-perf.test.js +33 -0
  80. package/test/logger/logging.test.js +57 -1
  81. package/test/logger/options.test.js +38 -1
  82. package/test/reply-error.test.js +1 -1
  83. package/test/reply-trailers.test.js +70 -0
  84. package/test/request-media-type.test.js +105 -0
  85. package/test/route-prefix.test.js +34 -0
  86. package/test/router-options.test.js +222 -11
  87. package/test/schema-serialization.test.js +108 -0
  88. package/test/schema-validation.test.js +24 -0
  89. package/test/scripts/validate-ecosystem-links.test.js +40 -57
  90. package/test/stream.4.test.js +2 -2
  91. package/test/throw.test.js +14 -0
  92. package/test/trust-proxy.test.js +70 -30
  93. package/test/types/content-type-parser.tst.ts +70 -0
  94. package/test/types/{decorate-request-reply.test-d.ts → decorate-request-reply.tst.ts} +4 -4
  95. package/test/types/{dummy-plugin.ts → dummy-plugin.mts} +1 -1
  96. package/test/types/errors.tst.ts +91 -0
  97. package/test/types/fastify.tst.ts +351 -0
  98. package/test/types/hooks.tst.ts +578 -0
  99. package/test/types/instance.tst.ts +597 -0
  100. package/test/types/{logger.test-d.ts → logger.tst.ts} +61 -62
  101. package/test/types/plugin.tst.ts +96 -0
  102. package/test/types/register.tst.ts +245 -0
  103. package/test/types/reply.tst.ts +297 -0
  104. package/test/types/request.tst.ts +199 -0
  105. package/test/types/route.tst.ts +576 -0
  106. package/test/types/{schema.test-d.ts → schema.tst.ts} +22 -22
  107. package/test/types/{serverFactory.test-d.ts → serverFactory.tst.ts} +4 -4
  108. package/test/types/tsconfig.json +9 -0
  109. package/test/types/{type-provider.test-d.ts → type-provider.tst.ts} +256 -250
  110. package/test/types/using.tst.ts +14 -0
  111. package/types/errors.d.ts +4 -0
  112. package/types/instance.d.ts +2 -0
  113. package/types/logger.d.ts +22 -0
  114. package/types/request.d.ts +23 -2
  115. package/test/types/content-type-parser.test-d.ts +0 -72
  116. package/test/types/errors.test-d.ts +0 -90
  117. package/test/types/fastify.test-d.ts +0 -352
  118. package/test/types/hooks.test-d.ts +0 -550
  119. package/test/types/import.ts +0 -2
  120. package/test/types/instance.test-d.ts +0 -588
  121. package/test/types/plugin.test-d.ts +0 -97
  122. package/test/types/register.test-d.ts +0 -237
  123. package/test/types/reply.test-d.ts +0 -254
  124. package/test/types/request.test-d.ts +0 -188
  125. package/test/types/route.test-d.ts +0 -553
  126. package/test/types/using.test-d.ts +0 -17
package/lib/symbols.js CHANGED
@@ -13,7 +13,6 @@ const keys = {
13
13
  kContentTypeParser: Symbol('fastify.contentTypeParser'),
14
14
  kState: Symbol('fastify.state'),
15
15
  kOptions: Symbol('fastify.options'),
16
- kDisableRequestLogging: Symbol('fastify.disableRequestLogging'),
17
16
  kPluginNameChain: Symbol('fastify.pluginNameChain'),
18
17
  kRouteContext: Symbol('fastify.context'),
19
18
  kGenReqId: Symbol('fastify.genReqId'),
@@ -32,6 +31,7 @@ const keys = {
32
31
  kRequestPayloadStream: Symbol('fastify.RequestPayloadStream'),
33
32
  kRequestAcceptVersion: Symbol('fastify.RequestAcceptVersion'),
34
33
  kRequestCacheValidateFns: Symbol('fastify.request.cache.validateFns'),
34
+ kRequestContentType: Symbol('fastify.request.contentType'),
35
35
  kRequestOriginalUrl: Symbol('fastify.request.originalUrl'),
36
36
  kRequestSignal: Symbol('fastify.request.signal'),
37
37
  kHandlerTimeout: Symbol('fastify.handlerTimeout'),
@@ -65,7 +65,9 @@ const keys = {
65
65
  kChildLoggerFactory: Symbol('fastify.childLoggerFactory'),
66
66
  kHasBeenDecorated: Symbol('fastify.hasBeenDecorated'),
67
67
  kKeepAliveConnections: Symbol('fastify.keepAliveConnections'),
68
- kRouteByFastify: Symbol('fastify.routeByFastify')
68
+ kRouteByFastify: Symbol('fastify.routeByFastify'),
69
+ kLogController: Symbol('fastify.logController'),
70
+ kDiagnosticsStore: Symbol('fastify.diagnosticsStore')
69
71
  }
70
72
 
71
73
  module.exports = keys
package/lib/validation.js CHANGED
@@ -146,6 +146,15 @@ function validateParam (validatorFunction, request, paramName) {
146
146
  function validate (context, request, execution) {
147
147
  const runExecution = execution === undefined
148
148
 
149
+ if (runExecution &&
150
+ context[paramsSchema] === undefined &&
151
+ context[bodySchema] === undefined &&
152
+ context[querystringSchema] === undefined &&
153
+ context[headersSchema] === undefined) {
154
+ // the route has no validation schemas
155
+ return false
156
+ }
157
+
149
158
  if (runExecution || !execution.skipParams) {
150
159
  const params = validateParam(context[paramsSchema], request, 'params')
151
160
  if (params) {
@@ -162,9 +171,7 @@ function validate (context, request, execution) {
162
171
  if (typeof context[bodySchema] === 'function') {
163
172
  validatorFunction = context[bodySchema]
164
173
  } else if (context[bodySchema]) {
165
- // TODO: add request.contentType and reuse it here
166
- const contentType = getEssenceMediaType(request.headers['content-type'])
167
- const contentSchema = context[bodySchema][contentType]
174
+ const contentSchema = context[bodySchema][request.mediaType]
168
175
  if (contentSchema) {
169
176
  validatorFunction = contentSchema
170
177
  }
@@ -262,16 +269,6 @@ function wrapValidationError (result, dataVar, schemaErrorFormatter) {
262
269
  return error
263
270
  }
264
271
 
265
- /**
266
- * simple function to retrieve the essence media type
267
- * @param {string} header
268
- * @returns {string} Mimetype string.
269
- */
270
- function getEssenceMediaType (header) {
271
- if (!header) return ''
272
- return header.trimStart().split(/[ ;]/, 1)[0].trim().toLowerCase()
273
- }
274
-
275
272
  module.exports = {
276
273
  symbols: { bodySchema, querystringSchema, responseSchema, paramsSchema, headersSchema },
277
274
  compileSchemasForValidation,
package/lib/warnings.js CHANGED
@@ -7,10 +7,12 @@ const { createWarning } = require('process-warning')
7
7
  * - FSTWRN001
8
8
  * - FSTSEC001
9
9
  * - FSTDEP022
10
+ * - FSTDEP023
11
+ * - FSTDEP024
10
12
  *
11
- * Deprecation Codes FSTDEP001 - FSTDEP021 were used by v4 and MUST NOT not be reused.
13
+ * Deprecation Codes FSTDEP001 - FSTDEP021 were used by v4 and MUST NOT be reused.
12
14
  * - FSTDEP022 is used by v5 and MUST NOT be reused.
13
- * Warning Codes FSTWRN001 - FSTWRN002 were used by v4 and MUST NOT not be reused.
15
+ * Warning Codes FSTWRN001 - FSTWRN002 were used by v4 and MUST NOT be reused.
14
16
  */
15
17
 
16
18
  const FSTWRN001 = createWarning({
@@ -30,7 +32,7 @@ const FSTWRN003 = createWarning({
30
32
  const FSTWRN004 = createWarning({
31
33
  name: 'FastifyWarning',
32
34
  code: 'FSTWRN004',
33
- message: 'It seems that you are overriding an errorHandler in the same scope, which can lead to subtle bugs.',
35
+ message: 'It seems that you are overriding an errorHandler in the same scope, which can lead to subtle bugs. To disable this behavior, set \'allowErrorHandlerOverride\' to false. For more information, visit: https://fastify.dev/docs/latest/Reference/Server/#allowerrorhandleroverride',
34
36
  unlimited: true
35
37
  })
36
38
 
@@ -48,10 +50,26 @@ const FSTDEP022 = createWarning({
48
50
  unlimited: true
49
51
  })
50
52
 
53
+ const FSTDEP023 = createWarning({
54
+ name: 'FastifyDeprecation',
55
+ code: 'FSTDEP023',
56
+ message: 'disableRequestLogging option is deprecated. Use the logController option with disableRequestLogging or isLogDisabled override instead. The disableRequestLogging top-level option will be removed in `fastify@6`.',
57
+ unlimited: true
58
+ })
59
+
60
+ const FSTDEP024 = createWarning({
61
+ name: 'FastifyDeprecation',
62
+ code: 'FSTDEP024',
63
+ message: 'requestIdLogLabel option is deprecated. Use the logController option with requestIdLogLabel instead. The requestIdLogLabel top-level option will be removed in `fastify@6`.',
64
+ unlimited: true
65
+ })
66
+
51
67
  module.exports = {
52
68
  FSTWRN001,
53
69
  FSTWRN003,
54
70
  FSTWRN004,
55
71
  FSTSEC001,
56
- FSTDEP022
72
+ FSTDEP022,
73
+ FSTDEP023,
74
+ FSTDEP024
57
75
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fastify",
3
- "version": "5.8.5",
3
+ "version": "5.10.0",
4
4
  "description": "Fast and low overhead web framework, for Node.js",
5
5
  "main": "fastify.js",
6
6
  "type": "commonjs",
@@ -19,11 +19,11 @@
19
19
  "lint:markdown": "markdownlint-cli2",
20
20
  "lint:eslint": "eslint",
21
21
  "prepublishOnly": "cross-env PREPUBLISH=true borp --reporter=@jsumners/line-reporter && npm run test:validator:integrity && npm run build:sync-version",
22
- "test": "npm run lint && npm run unit && npm run test:typescript",
23
- "test:ci": "npm run unit && npm run test:typescript",
24
- "test:report": "npm run lint && npm run unit:report && npm run test:typescript",
22
+ "test": "npm run lint && npm run unit && npm run test:types",
23
+ "test:ci": "npm run unit && npm run test:types",
24
+ "test:report": "npm run lint && npm run unit:report && npm run test:types",
25
25
  "test:validator:integrity": "npm run build:validation && git diff --quiet --ignore-all-space --ignore-blank-lines --ignore-cr-at-eol lib/error-serializer.js && git diff --quiet --ignore-all-space --ignore-blank-lines --ignore-cr-at-eol lib/config-validator.js",
26
- "test:typescript": "tsc test/types/import.ts --target es2022 --moduleResolution node16 --module node16 --noEmit && tsd",
26
+ "test:types": "tstyche",
27
27
  "test:watch": "npm run unit -- --watch --coverage-report=none --reporter=terse",
28
28
  "unit": "borp",
29
29
  "unit:report": "c8 --reporter html borp --reporter=@jsumners/line-reporter",
@@ -45,7 +45,7 @@
45
45
  "contributors": [
46
46
  {
47
47
  "name": "Tomas Della Vedova",
48
- "url": "http://delved.org",
48
+ "url": "https://delvedor.dev",
49
49
  "author": true
50
50
  },
51
51
  {
@@ -59,7 +59,7 @@
59
59
  },
60
60
  {
61
61
  "name": "Dustin Deus",
62
- "url": "http://starptech.de",
62
+ "url": "https://starptech.com",
63
63
  "email": "deusdustin@gmail.com"
64
64
  },
65
65
  {
@@ -74,7 +74,7 @@
74
74
  },
75
75
  {
76
76
  "name": "Trivikram Kamat",
77
- "url": "http://trivikr.github.io",
77
+ "url": "https://trivikr.github.io",
78
78
  "email": "trivikr.dev@gmail.com"
79
79
  },
80
80
  {
@@ -170,7 +170,7 @@
170
170
  "@sinonjs/fake-timers": "^11.2.2",
171
171
  "@stylistic/eslint-plugin": "^5.1.0",
172
172
  "@stylistic/eslint-plugin-js": "^4.1.0",
173
- "@types/node": "^25.0.3",
173
+ "@types/node": "^26.0.1",
174
174
  "ajv": "^8.12.0",
175
175
  "ajv-errors": "^3.0.0",
176
176
  "ajv-formats": "^3.0.1",
@@ -179,11 +179,12 @@
179
179
  "autocannon": "^8.0.0",
180
180
  "borp": "^1.0.0",
181
181
  "branch-comparer": "^1.1.0",
182
- "concurrently": "^9.1.2",
182
+ "concurrently": "^10.0.0",
183
183
  "cross-env": "^10.0.0",
184
184
  "eslint": "^9.0.0",
185
185
  "fast-json-body": "^1.1.0",
186
- "fastify-plugin": "^5.0.0",
186
+ "fastify-plugin": "^6.0.0",
187
+ "fastify-tsconfig": "^3.0.0",
187
188
  "fluent-json-schema": "^6.0.0",
188
189
  "h2url": "^0.2.0",
189
190
  "http-errors": "^2.0.0",
@@ -195,7 +196,7 @@
195
196
  "node-forge": "^1.3.1",
196
197
  "proxyquire": "^2.1.3",
197
198
  "split2": "^4.2.0",
198
- "tsd": "^0.33.0",
199
+ "tstyche": "^7.0.0",
199
200
  "typebox": "^1.0.81",
200
201
  "typescript": "~6.0.2",
201
202
  "undici": "^7.11.0",
@@ -209,8 +210,8 @@
209
210
  "@fastify/proxy-addr": "^5.0.0",
210
211
  "abstract-logging": "^2.0.1",
211
212
  "avvio": "^9.0.0",
212
- "fast-json-stringify": "^6.0.0",
213
- "find-my-way": "^9.0.0",
213
+ "fast-json-stringify": "^7.0.0",
214
+ "find-my-way": "^9.6.0",
214
215
  "light-my-request": "^6.0.0",
215
216
  "pino": "^9.14.0 || ^10.1.0",
216
217
  "process-warning": "^5.0.0",
@@ -218,8 +219,5 @@
218
219
  "secure-json-parse": "^4.0.0",
219
220
  "semver": "^7.6.0",
220
221
  "toad-cache": "^3.7.0"
221
- },
222
- "tsd": {
223
- "directory": "test/types"
224
222
  }
225
223
  }
@@ -14,6 +14,7 @@
14
14
 
15
15
  const fs = require('node:fs')
16
16
  const path = require('node:path')
17
+ const { fetch } = require('undici')
17
18
 
18
19
  const ECOSYSTEM_FILE = path.join(__dirname, '../docs/Guides/Ecosystem.md')
19
20
  const GITHUB_OWNER_REGEX = /^[a-z\d](?:[a-z\d-]{0,38})$/i
@@ -5,6 +5,6 @@
5
5
  "test": "npm run bundle && node bundler-test.js"
6
6
  },
7
7
  "devDependencies": {
8
- "esbuild": "^0.25.0"
8
+ "esbuild": "^0.28.1"
9
9
  }
10
10
  }
@@ -41,8 +41,7 @@ test('Should return 503 while closing - pipelining', async t => {
41
41
  })
42
42
 
43
43
  test('Should close the socket abruptly - pipelining - return503OnClosing: false', async t => {
44
- // Since Node v20, we will always invoke server.closeIdleConnections()
45
- // therefore our socket will be closed
44
+ // Node.js will always invoke server.closeIdleConnections() therefore our socket will be closed
46
45
  const fastify = Fastify({
47
46
  return503OnClosing: false,
48
47
  forceCloseConnections: false
@@ -179,3 +179,23 @@ describe('ContentType class', () => {
179
179
  )
180
180
  })
181
181
  })
182
+
183
+ describe('ContentType class cache', () => {
184
+ test('allow access cache', (t) => {
185
+ const contentType1 = ContentType.from('application/json')
186
+ const contentType2 = ContentType.cache.get('application/json')
187
+ t.assert.equal(contentType1, contentType2)
188
+ })
189
+
190
+ test('returns same instance for the same content type string', (t) => {
191
+ const contentType1 = ContentType.from('application/json')
192
+ const contentType2 = ContentType.from('application/json')
193
+ t.assert.equal(contentType1, contentType2)
194
+ })
195
+
196
+ test('returns different instances for different content type strings', (t) => {
197
+ const contentType1 = ContentType.from('application/json')
198
+ const contentType2 = ContentType.from('text/plain')
199
+ t.assert.notEqual(contentType1, contentType2)
200
+ })
201
+ })
@@ -63,6 +63,44 @@ async function setup () {
63
63
  )
64
64
  })
65
65
 
66
+ test('Should not make an extra closeIdleConnections call for native servers', async t => {
67
+ const fastify = Fastify({
68
+ forceCloseConnections: 'idle'
69
+ })
70
+
71
+ await fastify.listen({ port: 0 })
72
+
73
+ let called = 0
74
+ fastify.server.closeIdleConnections = function () {
75
+ called++
76
+ }
77
+
78
+ await fastify.close()
79
+
80
+ t.assert.strictEqual(called, 1)
81
+ })
82
+
83
+ test('Should preserve the extra closeIdleConnections call for custom servers', async t => {
84
+ let called = 0
85
+ const fastify = Fastify({
86
+ forceCloseConnections: 'idle',
87
+ serverFactory (handler) {
88
+ const server = http.createServer(handler)
89
+ const originalCloseIdleConnections = server.closeIdleConnections.bind(server)
90
+ server.closeIdleConnections = function () {
91
+ called++
92
+ return originalCloseIdleConnections()
93
+ }
94
+ return server
95
+ }
96
+ })
97
+
98
+ await fastify.listen({ port: 0 })
99
+ await fastify.close()
100
+
101
+ t.assert.strictEqual(called, 2)
102
+ })
103
+
66
104
  test('Should accept user defined serverFactory and ignore secondary server creation', async t => {
67
105
  const server = http.createServer(() => { })
68
106
  t.after(() => new Promise(resolve => server.close(resolve)))
@@ -0,0 +1,63 @@
1
+ 'use strict'
2
+
3
+ const { test } = require('node:test')
4
+ const Fastify = require('..')
5
+
6
+ test('hasRequestDecorator returns true for built-in constructor-assigned request properties', t => {
7
+ t.plan(6)
8
+ const fastify = Fastify()
9
+ t.assert.equal(fastify.hasRequestDecorator('id'), true)
10
+ t.assert.equal(fastify.hasRequestDecorator('params'), true)
11
+ t.assert.equal(fastify.hasRequestDecorator('raw'), true)
12
+ t.assert.equal(fastify.hasRequestDecorator('query'), true)
13
+ t.assert.equal(fastify.hasRequestDecorator('log'), true)
14
+ t.assert.equal(fastify.hasRequestDecorator('body'), true)
15
+ })
16
+
17
+ test('hasReplyDecorator returns true for built-in constructor-assigned reply properties', t => {
18
+ t.plan(3)
19
+ const fastify = Fastify()
20
+ t.assert.equal(fastify.hasReplyDecorator('raw'), true)
21
+ t.assert.equal(fastify.hasReplyDecorator('request'), true)
22
+ t.assert.equal(fastify.hasReplyDecorator('log'), true)
23
+ })
24
+
25
+ test('decorateRequest throws FST_ERR_DEC_ALREADY_PRESENT for built-in request properties', t => {
26
+ t.plan(4)
27
+ const fastify = Fastify()
28
+ for (const name of ['id', 'params', 'raw', 'body']) {
29
+ t.assert.throws(
30
+ () => fastify.decorateRequest(name, null),
31
+ (err) => err.code === 'FST_ERR_DEC_ALREADY_PRESENT'
32
+ )
33
+ }
34
+ })
35
+
36
+ test('decorateReply throws FST_ERR_DEC_ALREADY_PRESENT for built-in reply properties', t => {
37
+ t.plan(2)
38
+ const fastify = Fastify()
39
+ for (const name of ['raw', 'request']) {
40
+ t.assert.throws(
41
+ () => fastify.decorateReply(name, null),
42
+ (err) => err.code === 'FST_ERR_DEC_ALREADY_PRESENT'
43
+ )
44
+ }
45
+ })
46
+
47
+ test('hasRequestDecorator returns false for unknown properties', t => {
48
+ t.plan(1)
49
+ const fastify = Fastify()
50
+ t.assert.equal(fastify.hasRequestDecorator('nonExistent'), false)
51
+ })
52
+
53
+ test('decorateRequest still works for non-built-in names', (t, done) => {
54
+ t.plan(2)
55
+ const fastify = Fastify()
56
+ fastify.decorateRequest('myExtra', null)
57
+ t.assert.equal(fastify.hasRequestDecorator('myExtra'), true)
58
+ fastify.get('/', async (req) => req.myExtra)
59
+ fastify.ready((err) => {
60
+ t.assert.ifError(err)
61
+ done()
62
+ })
63
+ })
@@ -0,0 +1,74 @@
1
+ 'use strict'
2
+
3
+ const { test } = require('node:test')
4
+ const diagnostics = require('node:diagnostics_channel')
5
+ const Fastify = require('../..')
6
+ const Request = require('../../lib/request')
7
+ const Reply = require('../../lib/reply')
8
+
9
+ test('diagnostics channel tracks async operations in async error handlers', async t => {
10
+ t.plan(17)
11
+ let callOrder = 0
12
+ let firstEncounteredMessage
13
+ let asyncStartFired = false
14
+ let asyncEndFired = false
15
+
16
+ const fastify = Fastify()
17
+
18
+ function subscribe (name, handler) {
19
+ const wrapped = (msg) => {
20
+ if (msg.request.server === fastify) handler(msg)
21
+ }
22
+ diagnostics.subscribe(name, wrapped)
23
+ t.after(() => diagnostics.unsubscribe(name, wrapped))
24
+ }
25
+
26
+ subscribe('tracing:fastify.request.handler:start', (msg) => {
27
+ t.assert.strictEqual(callOrder++, 0)
28
+ firstEncounteredMessage = msg
29
+ t.assert.ok(msg.request instanceof Request)
30
+ t.assert.ok(msg.reply instanceof Reply)
31
+ })
32
+
33
+ subscribe('tracing:fastify.request.handler:error', (msg) => {
34
+ t.assert.strictEqual(callOrder++, 1)
35
+ t.assert.ok(msg.error instanceof Error)
36
+ t.assert.strictEqual(msg.error.message, 'handler error')
37
+ })
38
+
39
+ subscribe('tracing:fastify.request.handler:end', (msg) => {
40
+ t.assert.strictEqual(callOrder++, 2)
41
+ t.assert.strictEqual(msg, firstEncounteredMessage)
42
+ t.assert.strictEqual(msg.async, true)
43
+ })
44
+
45
+ subscribe('tracing:fastify.request.handler:asyncStart', (msg) => {
46
+ t.assert.strictEqual(callOrder++, 3)
47
+ t.assert.strictEqual(msg, firstEncounteredMessage)
48
+ asyncStartFired = true
49
+ })
50
+
51
+ subscribe('tracing:fastify.request.handler:asyncEnd', (msg) => {
52
+ t.assert.strictEqual(callOrder++, 4)
53
+ t.assert.strictEqual(msg, firstEncounteredMessage)
54
+ asyncEndFired = true
55
+ })
56
+
57
+ fastify.setErrorHandler(async (error, request, reply) => {
58
+ await new Promise(resolve => setImmediate(resolve))
59
+ reply.status(503).send({ error: error.message })
60
+ })
61
+
62
+ fastify.get('/', () => {
63
+ throw new Error('handler error')
64
+ })
65
+
66
+ const fastifyServer = await fastify.listen({ port: 0 })
67
+ t.after(() => fastify.close())
68
+
69
+ const response = await fetch(fastifyServer)
70
+ t.assert.ok(!response.ok)
71
+ t.assert.strictEqual(response.status, 503)
72
+ t.assert.ok(asyncStartFired, 'asyncStart should fire for async error handler')
73
+ t.assert.ok(asyncEndFired, 'asyncEnd should fire for async error handler')
74
+ })
@@ -85,6 +85,30 @@ test('Should handle properly requestIdHeader option', t => {
85
85
  t.assert.strictEqual(Fastify({ requestIdHeader: 'x-request-id' }).initialConfig.requestIdHeader, 'x-request-id')
86
86
  })
87
87
 
88
+ test('Should expose the genReqId function via the getter', (t, done) => {
89
+ t.plan(5)
90
+
91
+ const fastify = Fastify()
92
+ t.assert.strictEqual(typeof fastify.genReqId, 'function')
93
+ t.assert.strictEqual(typeof fastify.genReqId({ headers: {} }), 'string')
94
+
95
+ const custom = function (req) {
96
+ return 'custom'
97
+ }
98
+
99
+ fastify.register(function (instance, opts, next) {
100
+ instance.setGenReqId(custom)
101
+ t.assert.strictEqual(instance.genReqId(), 'custom')
102
+ next()
103
+ })
104
+
105
+ fastify.ready(err => {
106
+ t.assert.ifError(err)
107
+ t.assert.notStrictEqual(fastify.genReqId, custom)
108
+ done()
109
+ })
110
+ })
111
+
88
112
  test('Should accept option to set genReqId with setGenReqId option', (t, done) => {
89
113
  t.plan(9)
90
114
 
@@ -12,6 +12,7 @@ const proxyquire = require('proxyquire')
12
12
  const { connect } = require('node:net')
13
13
  const { sleep } = require('./helper')
14
14
  const { waitForCb } = require('./toolkit.js')
15
+ const { fetch } = require('undici')
15
16
 
16
17
  process.removeAllListeners('warning')
17
18
 
@@ -3277,6 +3278,28 @@ test('onTimeout should be triggered and socket _meta is set', async t => {
3277
3278
  }
3278
3279
  })
3279
3280
 
3281
+ test('socket._meta is cleared after response to prevent keep-alive leaks', async t => {
3282
+ t.plan(3)
3283
+ const fastify = Fastify({ connectionTimeout: 500 })
3284
+ t.after(() => { fastify.close() })
3285
+
3286
+ fastify.addHook('onTimeout', function (req, res, done) { done() })
3287
+
3288
+ fastify.addHook('onResponse', function (req, reply, done) {
3289
+ t.assert.strictEqual(req.raw.socket._meta, null, 'socket._meta must be null after response')
3290
+ done()
3291
+ })
3292
+
3293
+ fastify.get('/', async (req, reply) => {
3294
+ return { hello: 'world' }
3295
+ })
3296
+
3297
+ const address = await fastify.listen({ port: 0 })
3298
+ const result = await fetch(address)
3299
+ t.assert.ok(result.ok)
3300
+ t.assert.strictEqual(result.status, 200)
3301
+ })
3302
+
3280
3303
  test('registering invalid hooks should throw an error', async t => {
3281
3304
  t.plan(3)
3282
3305
 
@@ -3576,3 +3599,74 @@ test('onRequestAbort should handle async errors / 2', (t, testDone) => {
3576
3599
  sleep(500).then(() => socket.destroy())
3577
3600
  })
3578
3601
  })
3602
+
3603
+ test('socket timeout listener is removed when socket._meta is cleared after response', async t => {
3604
+ t.plan(4)
3605
+
3606
+ const fastify = Fastify({ connectionTimeout: 200 })
3607
+ t.after(() => fastify.close())
3608
+
3609
+ fastify.addHook('onTimeout', function (req, reply, done) { done() })
3610
+
3611
+ fastify.addHook('onResponse', function (req, reply, done) {
3612
+ const socket = req.raw.socket
3613
+ t.assert.strictEqual(socket._meta, null, 'socket._meta must be null after response')
3614
+ t.assert.strictEqual(socket.listeners('timeout').some(listener => listener.name === 'handleTimeout'), false, 'Fastify timeout listener must be removed after response')
3615
+ done()
3616
+ })
3617
+
3618
+ fastify.get('/', async () => ({ ok: true }))
3619
+
3620
+ const address = await fastify.listen({ port: 0 })
3621
+ const result = await fetch(address)
3622
+ t.assert.ok(result.ok)
3623
+ t.assert.strictEqual(result.status, 200)
3624
+ })
3625
+
3626
+ test('socket._meta and timeout listener are cleared on reply.hijack() when onTimeout is registered', async t => {
3627
+ // reply.hijack() opts out of Fastify's response lifecycle so onResFinished
3628
+ // never fires. Before this fix, socket._meta was left pointing at the
3629
+ // request/reply objects for the full keep-alive socket lifetime when
3630
+ // an onTimeout hook was registered.
3631
+ t.plan(3)
3632
+
3633
+ const net = require('node:net')
3634
+ const fastify = Fastify({ connectionTimeout: 300 })
3635
+ t.after(() => fastify.close())
3636
+
3637
+ fastify.addHook('onTimeout', function (req, reply, done) { done() })
3638
+
3639
+ let metaAfterHijack = 'not-set'
3640
+ let hasFastifyTimeoutListenerAfterHijack = true
3641
+ fastify.get('/', async (req, reply) => {
3642
+ reply.hijack()
3643
+ // _meta must be cleared synchronously inside hijack()
3644
+ metaAfterHijack = req.raw.socket._meta
3645
+ hasFastifyTimeoutListenerAfterHijack = req.raw.socket.listeners('timeout').some(listener => listener.name === 'handleTimeout')
3646
+ // Write a minimal valid HTTP/1.1 response and close
3647
+ reply.raw.write('HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok')
3648
+ reply.raw.end()
3649
+ return reply
3650
+ })
3651
+
3652
+ await fastify.listen({ port: 0 })
3653
+
3654
+ // Use raw TCP so we are not affected by fetch() rejecting a closed socket.
3655
+ // Extract host/port from the server address object to handle IPv6 (::1) on Windows.
3656
+ const { port, address: host } = fastify.server.address()
3657
+ await new Promise((resolve, reject) => {
3658
+ const socket = net.connect(port, host, () => {
3659
+ socket.write('GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n')
3660
+ })
3661
+ socket.on('data', () => {})
3662
+ socket.on('close', resolve)
3663
+ socket.on('error', reject)
3664
+ })
3665
+
3666
+ t.assert.ok(true, 'no crash during hijacked request with onTimeout registered')
3667
+ t.assert.ok(
3668
+ metaAfterHijack == null,
3669
+ `socket._meta must be null or undefined after reply.hijack() — got: ${JSON.stringify(metaAfterHijack)}`
3670
+ )
3671
+ t.assert.strictEqual(hasFastifyTimeoutListenerAfterHijack, false, 'Fastify timeout listener must be removed after reply.hijack()')
3672
+ })
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const { test } = require('node:test')
4
- const { Client } = require('undici')
4
+ const { Client, fetch } = require('undici')
5
5
  const fastify = require('../../fastify')()
6
6
 
7
7
  const schema = {