fastify 5.8.4 → 5.9.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 (105) hide show
  1. package/SECURITY.md +1 -1
  2. package/SPONSORS.md +6 -4
  3. package/docs/Guides/Database.md +0 -28
  4. package/docs/Guides/Ecosystem.md +17 -2
  5. package/docs/Guides/Serverless.md +2 -2
  6. package/docs/Guides/Write-Plugin.md +1 -1
  7. package/docs/Reference/Encapsulation.md +27 -26
  8. package/docs/Reference/Errors.md +10 -4
  9. package/docs/Reference/HTTP2.md +10 -10
  10. package/docs/Reference/Hooks.md +4 -4
  11. package/docs/Reference/Index.md +14 -16
  12. package/docs/Reference/LTS.md +12 -13
  13. package/docs/Reference/Lifecycle.md +9 -8
  14. package/docs/Reference/Logging.md +44 -39
  15. package/docs/Reference/Middleware.md +21 -25
  16. package/docs/Reference/Principles.md +2 -2
  17. package/docs/Reference/Reply.md +6 -1
  18. package/docs/Reference/Request.md +27 -16
  19. package/docs/Reference/Routes.md +5 -2
  20. package/docs/Reference/Server.md +31 -3
  21. package/docs/Reference/Type-Providers.md +29 -5
  22. package/docs/Reference/Validation-and-Serialization.md +15 -2
  23. package/docs/Reference/Warnings.md +7 -6
  24. package/eslint.config.js +7 -2
  25. package/fastify.d.ts +8 -3
  26. package/fastify.js +43 -14
  27. package/lib/content-type-parser.js +13 -1
  28. package/lib/decorate.js +11 -3
  29. package/lib/error-handler.js +4 -3
  30. package/lib/error-serializer.js +59 -59
  31. package/lib/errors.js +16 -1
  32. package/lib/four-oh-four.js +14 -9
  33. package/lib/handle-request.js +11 -5
  34. package/lib/plugin-override.js +2 -1
  35. package/lib/plugin-utils.js +5 -5
  36. package/lib/reply.js +63 -8
  37. package/lib/request.js +23 -19
  38. package/lib/route.js +20 -6
  39. package/lib/schema-controller.js +1 -1
  40. package/lib/schemas.js +37 -30
  41. package/lib/symbols.js +3 -1
  42. package/lib/validation.js +1 -13
  43. package/lib/warnings.js +3 -3
  44. package/package.json +16 -18
  45. package/scripts/validate-ecosystem-links.js +1 -0
  46. package/test/bundler/esbuild/package.json +1 -1
  47. package/test/close-pipelining.test.js +1 -2
  48. package/test/custom-http-server.test.js +38 -0
  49. package/test/decorator-instance-properties.test.js +63 -0
  50. package/test/diagnostics-channel/async-error-handler.test.js +74 -0
  51. package/test/hooks.test.js +23 -0
  52. package/test/http-methods/get.test.js +1 -1
  53. package/test/http2/plain.test.js +135 -0
  54. package/test/http2/secure-with-fallback.test.js +1 -1
  55. package/test/https/https.test.js +1 -2
  56. package/test/internals/errors.test.js +31 -1
  57. package/test/internals/plugin.test.js +3 -1
  58. package/test/internals/request.test.js +27 -3
  59. package/test/internals/schema-controller-perf.test.js +33 -0
  60. package/test/logger/logging.test.js +18 -1
  61. package/test/logger/options.test.js +38 -1
  62. package/test/reply-error.test.js +1 -1
  63. package/test/reply-trailers.test.js +70 -0
  64. package/test/request-media-type.test.js +105 -0
  65. package/test/request-port.test.js +72 -0
  66. package/test/route-prefix.test.js +34 -0
  67. package/test/router-options.test.js +222 -11
  68. package/test/schema-serialization.test.js +108 -0
  69. package/test/schema-validation.test.js +45 -0
  70. package/test/scripts/validate-ecosystem-links.test.js +40 -57
  71. package/test/throw.test.js +14 -0
  72. package/test/trust-proxy.test.js +74 -0
  73. package/test/types/content-type-parser.tst.ts +70 -0
  74. package/test/types/{decorate-request-reply.test-d.ts → decorate-request-reply.tst.ts} +4 -4
  75. package/test/types/{dummy-plugin.ts → dummy-plugin.mts} +1 -1
  76. package/test/types/errors.tst.ts +91 -0
  77. package/test/types/fastify.tst.ts +351 -0
  78. package/test/types/hooks.tst.ts +578 -0
  79. package/test/types/instance.tst.ts +597 -0
  80. package/test/types/{logger.test-d.ts → logger.tst.ts} +61 -62
  81. package/test/types/plugin.tst.ts +96 -0
  82. package/test/types/register.tst.ts +245 -0
  83. package/test/types/reply.tst.ts +297 -0
  84. package/test/types/request.tst.ts +199 -0
  85. package/test/types/route.tst.ts +576 -0
  86. package/test/types/{schema.test-d.ts → schema.tst.ts} +22 -22
  87. package/test/types/{serverFactory.test-d.ts → serverFactory.tst.ts} +4 -4
  88. package/test/types/tsconfig.json +9 -0
  89. package/test/types/{type-provider.test-d.ts → type-provider.tst.ts} +256 -250
  90. package/test/types/using.tst.ts +14 -0
  91. package/types/errors.d.ts +3 -0
  92. package/types/request.d.ts +23 -2
  93. package/AGENTS.md +0 -290
  94. package/test/types/content-type-parser.test-d.ts +0 -72
  95. package/test/types/errors.test-d.ts +0 -90
  96. package/test/types/fastify.test-d.ts +0 -352
  97. package/test/types/hooks.test-d.ts +0 -550
  98. package/test/types/import.ts +0 -2
  99. package/test/types/instance.test-d.ts +0 -588
  100. package/test/types/plugin.test-d.ts +0 -97
  101. package/test/types/register.test-d.ts +0 -237
  102. package/test/types/reply.test-d.ts +0 -254
  103. package/test/types/request.test-d.ts +0 -188
  104. package/test/types/route.test-d.ts +0 -553
  105. package/test/types/using.test-d.ts +0 -17
package/lib/reply.js CHANGED
@@ -60,6 +60,7 @@ const {
60
60
  const decorators = require('./decorate')
61
61
 
62
62
  const toString = Object.prototype.toString
63
+ const HTTP2_WRITE_CHUNK_SIZE = 64 * 1024
63
64
 
64
65
  function Reply (res, request, log) {
65
66
  this.raw = res
@@ -75,6 +76,7 @@ function Reply (res, request, log) {
75
76
  this.log = log
76
77
  }
77
78
  Reply.props = []
79
+ Reply.instanceProperties = new Set(['raw', 'request', 'log'])
78
80
 
79
81
  Object.defineProperties(Reply.prototype, {
80
82
  [kRouteContext]: {
@@ -167,7 +169,7 @@ Reply.prototype.send = function (payload) {
167
169
  // node:stream/web
168
170
  typeof payload.getReader === 'function' ||
169
171
  // Response
170
- toString.call(payload) === '[object Response]'
172
+ (typeof payload === 'object' && toString.call(payload) === '[object Response]')
171
173
  ) {
172
174
  onSendHook(this, payload)
173
175
  return this
@@ -593,7 +595,7 @@ function onSendEnd (reply, payload) {
593
595
  // since Response contain status code, headers and body,
594
596
  // we need to update the status, add the headers and use it's body as payload
595
597
  // before continuing
596
- if (toString.call(payload) === '[object Response]') {
598
+ if (payload != null && typeof payload === 'object' && toString.call(payload) === '[object Response]') {
597
599
  // https://developer.mozilla.org/en-US/docs/Web/API/Response/status
598
600
  if (typeof payload.status === 'number') {
599
601
  reply.code(payload.status)
@@ -677,9 +679,52 @@ function onSendEnd (reply, payload) {
677
679
 
678
680
  safeWriteHead(reply, statusCode)
679
681
  // write payload first
680
- res.write(payload)
681
- // then send trailers
682
- sendTrailer(payload, res, reply)
682
+ writePayload(payload, res, reply)
683
+ }
684
+
685
+ function isHttp2Reply (reply) {
686
+ return reply.request.raw?.httpVersionMajor === 2
687
+ }
688
+
689
+ function writePayload (payload, res, reply) {
690
+ if (!isHttp2Reply(reply) || Buffer.byteLength(payload) <= HTTP2_WRITE_CHUNK_SIZE) {
691
+ res.write(payload)
692
+ // then send trailers
693
+ sendTrailer(payload, res, reply)
694
+ return
695
+ }
696
+
697
+ writeHttp2Payload(payload, res, () => {
698
+ // then send trailers
699
+ sendTrailer(payload, res, reply)
700
+ })
701
+ }
702
+
703
+ function writeHttp2Payload (payload, res, done) {
704
+ const buffer = Buffer.isBuffer(payload) ? payload : Buffer.from(payload)
705
+ let offset = 0
706
+
707
+ function writeChunk () {
708
+ while (offset < buffer.length) {
709
+ /* c8 ignore next 3 - defensive guard for aborted responses */
710
+ if (res.destroyed || res.writableEnded) {
711
+ return
712
+ }
713
+
714
+ const end = Math.min(offset + HTTP2_WRITE_CHUNK_SIZE, buffer.length)
715
+ const shouldContinue = res.write(buffer.subarray(offset, end))
716
+ offset = end
717
+
718
+ if (shouldContinue === false) {
719
+ res.once('drain', writeChunk)
720
+ return
721
+ }
722
+ }
723
+
724
+ done()
725
+ }
726
+
727
+ writeChunk()
683
728
  }
684
729
 
685
730
  function logStreamError (logger, err, res) {
@@ -830,10 +875,12 @@ function sendTrailer (payload, res, reply) {
830
875
  const trailers = {}
831
876
  let handled = 0
832
877
  let skipped = true
878
+ let sent = false
833
879
  function send () {
834
880
  // add trailers when all handler handled
835
881
  /* istanbul ignore else */
836
- if (handled === 0) {
882
+ if (handled === 0 && !sent) {
883
+ sent = true
837
884
  res.addTrailers(trailers)
838
885
  // we need to properly close the stream
839
886
  // after trailers sent
@@ -846,9 +893,10 @@ function sendTrailer (payload, res, reply) {
846
893
  skipped = false
847
894
  handled--
848
895
 
896
+ let cbAlreadyCalled = false
849
897
  function cb (err, value) {
850
- // TODO: we may protect multiple callback calls
851
- // or mixing async-await with callback
898
+ if (cbAlreadyCalled) return
899
+ cbAlreadyCalled = true
852
900
  handled++
853
901
 
854
902
  // we can safely ignore error for trailer
@@ -914,6 +962,13 @@ function setupResponseListeners (reply) {
914
962
  }
915
963
  }
916
964
 
965
+ // Fix: release socket._meta so request/reply objects are not retained
966
+ // past the response on keep-alive connections.
967
+ const socket = reply.request.raw.socket
968
+ if (socket && socket._meta && socket._meta.request === reply.request) {
969
+ socket._meta = null
970
+ }
971
+
917
972
  if (ctx && ctx.onResponse !== null) {
918
973
  onResponseHookRunner(
919
974
  ctx.onResponse,
package/lib/request.js CHANGED
@@ -10,6 +10,7 @@ const {
10
10
  kSchemaController,
11
11
  kOptions,
12
12
  kRequestCacheValidateFns,
13
+ kRequestContentType,
13
14
  kRouteContext,
14
15
  kRequestOriginalUrl,
15
16
  kRequestSignal,
@@ -36,6 +37,7 @@ function Request (id, params, req, query, log, context) {
36
37
  this.body = undefined
37
38
  }
38
39
  Request.props = []
40
+ Request.instanceProperties = new Set(['id', 'params', 'raw', 'query', 'log', 'body'])
39
41
 
40
42
  function getTrustProxyFn (tp) {
41
43
  if (typeof tp === 'function') {
@@ -47,16 +49,14 @@ function getTrustProxyFn (tp) {
47
49
  }
48
50
  if (typeof tp === 'number') {
49
51
  // Support trusting hop count
50
- return function (a, i) { return a != null && i < tp }
52
+ return function (a, i) { return i < tp }
51
53
  }
52
54
  if (typeof tp === 'string') {
53
55
  // Support comma-separated tps
54
56
  const values = tp.split(',').map(it => it.trim())
55
- const trust = proxyAddr.compile(values)
56
- return function (a, i) { return a != null && trust(a, i) }
57
+ return proxyAddr.compile(values)
57
58
  }
58
- const trust = proxyAddr.compile(tp)
59
- return function (a, i) { return a != null && trust(a, i) }
59
+ return proxyAddr.compile(tp)
60
60
  }
61
61
 
62
62
  function buildRequest (R, trustProxy) {
@@ -119,7 +119,8 @@ function buildRequestWithTrustProxy (R, trustProxy) {
119
119
  },
120
120
  host: {
121
121
  get () {
122
- if (this.headers['x-forwarded-host'] && proxyFn(this.raw.socket?.remoteAddress, 0)) {
122
+ const socket = this.raw.socket
123
+ if (this.headers['x-forwarded-host'] && socket != null && proxyFn(socket.remoteAddress, 0)) {
123
124
  return getLastEntryInMultiHeaderValue(this.headers['x-forwarded-host'])
124
125
  }
125
126
  /**
@@ -133,7 +134,8 @@ function buildRequestWithTrustProxy (R, trustProxy) {
133
134
  },
134
135
  protocol: {
135
136
  get () {
136
- if (this.headers['x-forwarded-proto'] && proxyFn(this.raw.socket?.remoteAddress, 0)) {
137
+ const socket = this.raw.socket
138
+ if (this.headers['x-forwarded-proto'] && socket != null && proxyFn(socket.remoteAddress, 0)) {
137
139
  return getLastEntryInMultiHeaderValue(this.headers['x-forwarded-proto'])
138
140
  }
139
141
  if (this.socket) {
@@ -255,19 +257,13 @@ Object.defineProperties(Request.prototype, {
255
257
  },
256
258
  port: {
257
259
  get () {
258
- // first try taking port from host
259
- const portFromHost = parseInt(this.host.split(':').slice(-1)[0])
260
- if (!isNaN(portFromHost)) {
261
- return portFromHost
260
+ const portReg = /(?<port>:\d+)$/
261
+ const host = this.headers.host ?? this.headers[':authority'] ?? ''
262
+ const matches = portReg.exec(host)
263
+ if (matches === null || matches[1] === undefined) {
264
+ return null
262
265
  }
263
- // now fall back to port from host/:authority header
264
- const host = (this.headers.host ?? this.headers[':authority'] ?? '')
265
- const portFromHeader = parseInt(host.split(':').slice(-1)[0])
266
- if (!isNaN(portFromHeader)) {
267
- return portFromHeader
268
- }
269
- // fall back to null
270
- return null
266
+ return parseInt(matches.groups.port.slice(1), 10)
271
267
  }
272
268
  },
273
269
  protocol: {
@@ -288,6 +284,14 @@ Object.defineProperties(Request.prototype, {
288
284
  this.additionalHeaders = headers
289
285
  }
290
286
  },
287
+ mediaType: {
288
+ get () {
289
+ if (!this[kRequestContentType] && this.headers['content-type'] !== undefined) {
290
+ this[kRequestContentType] = this[kRouteContext].contentTypeParser.getContentType(this.headers['content-type'])
291
+ }
292
+ return this[kRequestContentType]?.mediaType
293
+ }
294
+ },
291
295
  getValidationFunction: {
292
296
  value: function (httpPartOrSchema) {
293
297
  if (typeof httpPartOrSchema === 'string') {
package/lib/route.js CHANGED
@@ -24,6 +24,7 @@ const {
24
24
  FST_ERR_ROUTE_MISSING_HANDLER,
25
25
  FST_ERR_ROUTE_METHOD_NOT_SUPPORTED,
26
26
  FST_ERR_ROUTE_METHOD_INVALID,
27
+ FST_ERR_ROUTE_LOG_LEVEL_INVALID,
27
28
  FST_ERR_ROUTE_BODY_VALIDATION_SCHEMA_NOT_SUPPORTED,
28
29
  FST_ERR_ROUTE_BODY_LIMIT_OPTION_NOT_INT,
29
30
  FST_ERR_ROUTE_HANDLER_TIMEOUT_OPTION_NOT_INT,
@@ -68,6 +69,7 @@ const routerKeys = [
68
69
  'ignoreTrailingSlash',
69
70
  'maxParamLength',
70
71
  'onBadUrl',
72
+ 'onMaxParamLength',
71
73
  'querystringParser',
72
74
  'useSemicolonDelimiter'
73
75
  ]
@@ -284,6 +286,7 @@ function buildRouting (options) {
284
286
  opts.routePath = path
285
287
  opts.prefix = prefix
286
288
  opts.logLevel = opts.logLevel || this[kLogLevel]
289
+ validateLogLevelOption(opts.logLevel, opts.method, opts.url, logger)
287
290
 
288
291
  if (this[kLogSerializers] || opts.logSerializers) {
289
292
  opts.logSerializers = Object.assign(Object.create(this[kLogSerializers]), opts.logSerializers)
@@ -365,7 +368,8 @@ function buildRouting (options) {
365
368
  // any route insertion error created by fastify can be safely ignore
366
369
  // because it only duplicate route for head
367
370
  if (!context[kRouteByFastify]) {
368
- const isDuplicatedRoute = error.message.includes(`Method '${opts.method}' already declared for route`)
371
+ const methods = Array.isArray(opts.method) ? opts.method : [opts.method]
372
+ const isDuplicatedRoute = methods.some(method => error.message.includes(`Method '${method}' already declared for route`))
369
373
  if (isDuplicatedRoute) {
370
374
  throw new FST_ERR_DUPLICATED_ROUTE(opts.method, opts.url)
371
375
  }
@@ -476,12 +480,13 @@ function buildRouting (options) {
476
480
  res.setHeader('Connection', 'close')
477
481
  }
478
482
 
479
- // TODO remove return503OnClosing after Node v18 goes EOL
480
- /* istanbul ignore else */
483
+ // Load-shedding fast path during drain. server.close() and
484
+ // closeIdleConnections() only reap idle sockets; requests already
485
+ // pipelined or arriving on an active keep-alive connection still reach
486
+ // this point with closing === true. Short-circuiting with 503 avoids
487
+ // running the full handler chain (and any downstream calls) for work
488
+ // the load balancer should redirect elsewhere.
481
489
  if (return503OnClosing) {
482
- // On Node v19 we cannot test this behavior as it won't be necessary
483
- // anymore. It will close all the idle connections before they reach this
484
- // stage.
485
490
  const headers = {
486
491
  'Content-Type': 'application/json',
487
492
  'Content-Length': '80'
@@ -635,6 +640,15 @@ function validateHandlerTimeoutOption (handlerTimeout) {
635
640
  }
636
641
  }
637
642
 
643
+ function validateLogLevelOption (logLevel, method, path, logger) {
644
+ if (logLevel == null || logLevel === '') return
645
+ if (logger?.levels?.values == null) return
646
+
647
+ if (typeof logLevel !== 'string' || logger.levels.values[logLevel] === undefined) {
648
+ throw new FST_ERR_ROUTE_LOG_LEVEL_INVALID(method, path, logLevel)
649
+ }
650
+ }
651
+
638
652
  function runPreParsing (err, request, reply) {
639
653
  if (reply.sent === true) return
640
654
  if (err != null) {
@@ -31,7 +31,7 @@ function buildSchemaController (parentSchemaCtrl, opts) {
31
31
  bucket: (opts && opts.bucket) || buildSchemas,
32
32
  compilersFactory,
33
33
  isCustomValidatorCompiler: typeof opts?.compilersFactory?.buildValidator === 'function',
34
- isCustomSerializerCompiler: typeof opts?.compilersFactory?.buildValidator === 'function'
34
+ isCustomSerializerCompiler: typeof opts?.compilersFactory?.buildSerializer === 'function'
35
35
  }
36
36
 
37
37
  return new SchemaController(undefined, option)
package/lib/schemas.js CHANGED
@@ -10,6 +10,7 @@ const {
10
10
  FST_ERR_SCH_DUPLICATE,
11
11
  FST_ERR_SCH_CONTENT_MISSING_SCHEMA
12
12
  } = require('./errors')
13
+ const ContentType = require('./content-type')
13
14
 
14
15
  const SCHEMAS_SOURCE = ['params', 'body', 'querystring', 'query', 'headers']
15
16
 
@@ -147,52 +148,58 @@ function getSchemaSerializer (context, statusCode, contentType) {
147
148
  return false
148
149
  }
149
150
  if (responseSchemaDef[statusCode]) {
150
- if (responseSchemaDef[statusCode].constructor === Object && contentType) {
151
- const mediaName = contentType.split(';', 1)[0]
152
- if (responseSchemaDef[statusCode][mediaName]) {
153
- return responseSchemaDef[statusCode][mediaName]
154
- }
151
+ if (responseSchemaDef[statusCode].constructor === Object) {
152
+ const ct = new ContentType(contentType)
153
+ if (ct.isValid) {
154
+ if (responseSchemaDef[statusCode][ct.mediaType]) {
155
+ return responseSchemaDef[statusCode][ct.mediaType]
156
+ }
155
157
 
156
- // fallback to match all media-type
157
- if (responseSchemaDef[statusCode]['*/*']) {
158
- return responseSchemaDef[statusCode]['*/*']
159
- }
158
+ // fallback to match all media-type
159
+ if (responseSchemaDef[statusCode]['*/*']) {
160
+ return responseSchemaDef[statusCode]['*/*']
161
+ }
160
162
 
161
- return false
163
+ return false
164
+ }
162
165
  }
163
166
  return responseSchemaDef[statusCode]
164
167
  }
165
168
  const fallbackStatusCode = (statusCode + '')[0] + 'xx'
166
169
  if (responseSchemaDef[fallbackStatusCode]) {
167
- if (responseSchemaDef[fallbackStatusCode].constructor === Object && contentType) {
168
- const mediaName = contentType.split(';', 1)[0]
169
- if (responseSchemaDef[fallbackStatusCode][mediaName]) {
170
- return responseSchemaDef[fallbackStatusCode][mediaName]
171
- }
170
+ if (responseSchemaDef[fallbackStatusCode].constructor === Object) {
171
+ const ct = new ContentType(contentType)
172
+ if (ct.isValid) {
173
+ if (responseSchemaDef[fallbackStatusCode][ct.mediaType]) {
174
+ return responseSchemaDef[fallbackStatusCode][ct.mediaType]
175
+ }
172
176
 
173
- // fallback to match all media-type
174
- if (responseSchemaDef[fallbackStatusCode]['*/*']) {
175
- return responseSchemaDef[fallbackStatusCode]['*/*']
176
- }
177
+ // fallback to match all media-type
178
+ if (responseSchemaDef[fallbackStatusCode]['*/*']) {
179
+ return responseSchemaDef[fallbackStatusCode]['*/*']
180
+ }
177
181
 
178
- return false
182
+ return false
183
+ }
179
184
  }
180
185
 
181
186
  return responseSchemaDef[fallbackStatusCode]
182
187
  }
183
188
  if (responseSchemaDef.default) {
184
- if (responseSchemaDef.default.constructor === Object && contentType) {
185
- const mediaName = contentType.split(';', 1)[0]
186
- if (responseSchemaDef.default[mediaName]) {
187
- return responseSchemaDef.default[mediaName]
188
- }
189
+ if (responseSchemaDef.default.constructor === Object) {
190
+ const ct = new ContentType(contentType)
191
+ if (ct.isValid) {
192
+ if (responseSchemaDef.default[ct.mediaType]) {
193
+ return responseSchemaDef.default[ct.mediaType]
194
+ }
189
195
 
190
- // fallback to match all media-type
191
- if (responseSchemaDef.default['*/*']) {
192
- return responseSchemaDef.default['*/*']
193
- }
196
+ // fallback to match all media-type
197
+ if (responseSchemaDef.default['*/*']) {
198
+ return responseSchemaDef.default['*/*']
199
+ }
194
200
 
195
- return false
201
+ return false
202
+ }
196
203
  }
197
204
 
198
205
  return responseSchemaDef.default
package/lib/symbols.js CHANGED
@@ -32,6 +32,7 @@ const keys = {
32
32
  kRequestPayloadStream: Symbol('fastify.RequestPayloadStream'),
33
33
  kRequestAcceptVersion: Symbol('fastify.RequestAcceptVersion'),
34
34
  kRequestCacheValidateFns: Symbol('fastify.request.cache.validateFns'),
35
+ kRequestContentType: Symbol('fastify.request.contentType'),
35
36
  kRequestOriginalUrl: Symbol('fastify.request.originalUrl'),
36
37
  kRequestSignal: Symbol('fastify.request.signal'),
37
38
  kHandlerTimeout: Symbol('fastify.handlerTimeout'),
@@ -65,7 +66,8 @@ const keys = {
65
66
  kChildLoggerFactory: Symbol('fastify.childLoggerFactory'),
66
67
  kHasBeenDecorated: Symbol('fastify.hasBeenDecorated'),
67
68
  kKeepAliveConnections: Symbol('fastify.keepAliveConnections'),
68
- kRouteByFastify: Symbol('fastify.routeByFastify')
69
+ kRouteByFastify: Symbol('fastify.routeByFastify'),
70
+ kDiagnosticsStore: Symbol('fastify.diagnosticsStore')
69
71
  }
70
72
 
71
73
  module.exports = keys
package/lib/validation.js CHANGED
@@ -162,9 +162,7 @@ function validate (context, request, execution) {
162
162
  if (typeof context[bodySchema] === 'function') {
163
163
  validatorFunction = context[bodySchema]
164
164
  } 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]
165
+ const contentSchema = context[bodySchema][request.mediaType]
168
166
  if (contentSchema) {
169
167
  validatorFunction = contentSchema
170
168
  }
@@ -262,16 +260,6 @@ function wrapValidationError (result, dataVar, schemaErrorFormatter) {
262
260
  return error
263
261
  }
264
262
 
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.split(/[ ;]/, 1)[0].trim().toLowerCase()
273
- }
274
-
275
263
  module.exports = {
276
264
  symbols: { bodySchema, querystringSchema, responseSchema, paramsSchema, headersSchema },
277
265
  compileSchemasForValidation,
package/lib/warnings.js CHANGED
@@ -8,9 +8,9 @@ const { createWarning } = require('process-warning')
8
8
  * - FSTSEC001
9
9
  * - FSTDEP022
10
10
  *
11
- * Deprecation Codes FSTDEP001 - FSTDEP021 were used by v4 and MUST NOT not be reused.
11
+ * Deprecation Codes FSTDEP001 - FSTDEP021 were used by v4 and MUST NOT be reused.
12
12
  * - 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.
13
+ * Warning Codes FSTWRN001 - FSTWRN002 were used by v4 and MUST NOT be reused.
14
14
  */
15
15
 
16
16
  const FSTWRN001 = createWarning({
@@ -30,7 +30,7 @@ const FSTWRN003 = createWarning({
30
30
  const FSTWRN004 = createWarning({
31
31
  name: 'FastifyWarning',
32
32
  code: 'FSTWRN004',
33
- message: 'It seems that you are overriding an errorHandler in the same scope, which can lead to subtle bugs.',
33
+ 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
34
  unlimited: true
35
35
  })
36
36
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fastify",
3
- "version": "5.8.4",
3
+ "version": "5.9.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
  {
@@ -177,27 +177,28 @@
177
177
  "ajv-i18n": "^4.2.0",
178
178
  "ajv-merge-patch": "^5.0.1",
179
179
  "autocannon": "^8.0.0",
180
- "borp": "^0.21.0",
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",
190
191
  "joi": "^18.0.1",
191
192
  "json-schema-to-ts": "^3.0.1",
192
193
  "JSONStream": "^1.3.5",
193
- "markdownlint-cli2": "^0.21.0",
194
+ "markdownlint-cli2": "^0.22.0",
194
195
  "neostandard": "^0.12.0",
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
- "typescript": "~5.9.2",
201
+ "typescript": "~6.0.2",
201
202
  "undici": "^7.11.0",
202
203
  "vary": "^1.1.2",
203
204
  "yup": "^1.4.0"
@@ -210,7 +211,7 @@
210
211
  "abstract-logging": "^2.0.1",
211
212
  "avvio": "^9.0.0",
212
213
  "fast-json-stringify": "^6.0.0",
213
- "find-my-way": "^9.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
@@ -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)))