fastify 5.2.2 → 5.3.1

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 (38) hide show
  1. package/SPONSORS.md +0 -1
  2. package/docs/Guides/Ecosystem.md +2 -0
  3. package/docs/Reference/Decorators.md +199 -0
  4. package/docs/Reference/Errors.md +2 -0
  5. package/fastify.js +3 -2
  6. package/lib/decorate.js +18 -3
  7. package/lib/errors.js +4 -0
  8. package/lib/reply.js +17 -2
  9. package/lib/request.js +28 -2
  10. package/lib/validation.js +11 -1
  11. package/package.json +3 -3
  12. package/test/build/error-serializer.test.js +1 -2
  13. package/test/custom-parser.0.test.js +160 -129
  14. package/test/custom-parser.1.test.js +77 -63
  15. package/test/custom-parser.4.test.js +55 -38
  16. package/test/custom-querystring-parser.test.js +46 -28
  17. package/test/decorator.test.js +174 -4
  18. package/test/fastify-instance.test.js +12 -2
  19. package/test/hooks.on-listen.test.js +17 -14
  20. package/test/internals/errors.test.js +14 -1
  21. package/test/listen.2.test.js +4 -1
  22. package/test/logger/instantiation.test.js +89 -96
  23. package/test/logger/logging.test.js +116 -120
  24. package/test/logger/options.test.js +97 -99
  25. package/test/logger/request.test.js +66 -66
  26. package/test/schema-validation.test.js +120 -0
  27. package/test/server.test.js +175 -0
  28. package/test/stream.4.test.js +38 -33
  29. package/test/toolkit.js +31 -0
  30. package/test/types/instance.test-d.ts +3 -0
  31. package/test/types/reply.test-d.ts +1 -0
  32. package/test/types/request.test-d.ts +4 -0
  33. package/test/types/type-provider.test-d.ts +40 -0
  34. package/test/upgrade.test.js +3 -6
  35. package/types/instance.d.ts +2 -0
  36. package/types/reply.d.ts +1 -0
  37. package/types/request.d.ts +2 -0
  38. package/types/type-provider.d.ts +12 -3
package/SPONSORS.md CHANGED
@@ -16,7 +16,6 @@ _Be the first!_
16
16
  - [Mercedes-Benz Group](https://github.com/mercedes-benz)
17
17
  - [Val Town, Inc.](https://opencollective.com/valtown)
18
18
  - [Handsontable - JavaScript Data Grid](https://handsontable.com/docs/react-data-grid/?utm_source=Fastify_GH&utm_medium=sponsorship&utm_campaign=library_sponsorship_2024)
19
- - [Jspreadsheet](https://jspreadsheet.com/)
20
19
  - [Lokalise - A Localization and Translation Software Tool](https://lokalise.com/?utm_source=Fastify_GH&utm_medium=sponsorship)
21
20
 
22
21
  ## Tier 2
@@ -460,6 +460,8 @@ middlewares into Fastify plugins
460
460
  Lightweight cache plugin
461
461
  - [`fastify-list-routes`](https://github.com/chuongtrh/fastify-list-routes)
462
462
  A simple plugin for Fastify to list all available routes.
463
+ - [`fastify-lm`](https://github.com/galiprandi/fastify-lm#readme)
464
+ Use OpenAI, Claude, Google, Deepseek, and others LMs with one Fastify plugin.
463
465
  - [`fastify-loader`](https://github.com/TheNoim/fastify-loader) Load routes from
464
466
  a directory and inject the Fastify instance in each file.
465
467
  - [`fastify-log-controller`](https://github.com/Eomm/fastify-log-controller/)
@@ -365,3 +365,202 @@ Will define the `foo` property on the Fastify instance:
365
365
  ```js
366
366
  console.log(fastify.foo) // 'a getter'
367
367
  ```
368
+
369
+ ### `getDecorator<T>` API
370
+
371
+ Fastify's `getDecorator<T>` API retrieves an existing decorator from the
372
+ Fastify instance, `Request`, or `Reply`. If the decorator is not defined, an
373
+ `FST_ERR_DEC_UNDECLARED` error is thrown.
374
+
375
+ #### Use cases
376
+
377
+ **Early Plugin Dependency Validation**
378
+
379
+ `getDecorator<T>` on Fastify instance verifies that required decorators are
380
+ available at registration time.
381
+
382
+ For example:
383
+
384
+ ```js
385
+ fastify.register(async function (fastify) {
386
+ const usersRepository = fastify.getDecorator('usersRepository')
387
+
388
+ fastify.get('/users', async function (request, reply) {
389
+ // We are sure `usersRepository` exists at runtime
390
+ return usersRepository.findAll()
391
+ })
392
+ })
393
+ ```
394
+
395
+ **Handling Missing Decorators**
396
+
397
+ Directly accessing a decorator may lead to unexpected behavior if it is not declared:
398
+
399
+ ```ts
400
+ const user = request.user;
401
+ if (user && user.isAdmin) {
402
+ // Execute admin tasks.
403
+ }
404
+ ```
405
+
406
+ If `request.user` doesn't exist, then `user` will be set to `undefined`.
407
+ This makes it unclear whether the user is unauthenticated or the decorator is missing.
408
+
409
+ Using `getDecorator` enforces runtime safety:
410
+
411
+ ```ts
412
+ // If the decorator is missing, an explicit `FST_ERR_DEC_UNDECLARED`
413
+ // error is thrown immediately.
414
+ const user = request.getDecorator('user');
415
+ if (user && user.isAdmin) {
416
+ // Execute admin tasks.
417
+ }
418
+ ```
419
+
420
+ **Alternative to Module Augmentation**
421
+
422
+ Decorators are typically typed via module augmentation:
423
+
424
+ ```ts
425
+ declare module 'fastify' {
426
+ interface FastifyInstance {
427
+ usersRepository: IUsersRepository
428
+ }
429
+ interface FastifyRequest {
430
+ session: ISession
431
+ }
432
+ interface FastifyReply {
433
+ sendSuccess: SendSuccessFn
434
+ }
435
+ }
436
+ ```
437
+
438
+ This approach modifies the Fastify instance globally, which may lead to
439
+ conflicts and inconsistent behavior in multi-server setups or with plugin
440
+ encapsulation.
441
+
442
+ Using `getDecorator<T>` allows to limit types scope:
443
+
444
+ ```ts
445
+ serverOne.register(async function (fastify) {
446
+ const usersRepository = fastify.getDecorator<PostgreUsersRepository>(
447
+ 'usersRepository'
448
+ )
449
+
450
+ fastify.decorateRequest('session', null)
451
+ fastify.addHook('onRequest', async (req, reply) => {
452
+ // Yes, the request object has a setDecorator method.
453
+ // More information will be provided soon.
454
+ req.setDecorator('session', { user: 'Jean' })
455
+ })
456
+
457
+ fastify.get('/me', (request, reply) => {
458
+ const session = request.getDecorator<ISession>('session')
459
+ reply.send(session)
460
+ })
461
+ })
462
+
463
+ serverTwo.register(async function (fastify) {
464
+ const usersRepository = fastify.getDecorator<SqlLiteUsersRepository>(
465
+ 'usersRepository'
466
+ )
467
+
468
+ fastify.decorateReply('sendSuccess', function (data) {
469
+ return this.send({ success: true })
470
+ })
471
+
472
+ fastify.get('/success', async (request, reply) => {
473
+ const sendSuccess = reply.getDecorator<SendSuccessFn>('sendSuccess')
474
+ await sendSuccess()
475
+ })
476
+ })
477
+ ```
478
+
479
+ #### Bound functions inference
480
+
481
+ To save time, it's common to infer function types instead of
482
+ writing them manually:
483
+
484
+ ```ts
485
+ function sendSuccess (this: FastifyReply) {
486
+ return this.send({ success: true })
487
+ }
488
+
489
+ export type SendSuccess = typeof sendSuccess
490
+ ```
491
+
492
+ However, `getDecorator` returns functions with the `this`
493
+ context already **bound**, meaning the `this` parameter disappears
494
+ from the function signature.
495
+
496
+ To correctly type it, you should use `OmitThisParameter` utility:
497
+
498
+ ```ts
499
+ function sendSuccess (this: FastifyReply) {
500
+ return this.send({ success: true })
501
+ }
502
+
503
+ type BoundSendSuccess = OmitThisParameter<typeof sendSuccess>
504
+
505
+ fastify.decorateReply('sendSuccess', sendSuccess)
506
+ fastify.get('/success', async (request, reply) => {
507
+ const sendSuccess = reply.getDecorator<BoundSendSuccess>('sendSuccess')
508
+ await sendSuccess()
509
+ })
510
+ ```
511
+
512
+ ### `Request.setDecorator<T>` Method
513
+
514
+ The `setDecorator<T>` method provides a safe and convenient way to
515
+ update the value of a `Request` decorator.
516
+ If the decorator does not exist, a `FST_ERR_DEC_UNDECLARED` error
517
+ is thrown.
518
+
519
+ #### Use Cases
520
+
521
+ **Runtime Safety**
522
+
523
+ A typical way to set a `Request` decorator looks like this:
524
+
525
+ ```ts
526
+ fastify.decorateRequest('user', '')
527
+ fastify.addHook('preHandler', async (req, reply) => {
528
+ req.user = 'Bob Dylan'
529
+ })
530
+ ```
531
+
532
+ However, there is no guarantee that the decorator actually exists
533
+ unless you manually check beforehand.
534
+ Additionally, typos are common, e.g. `account`, `acount`, or `accout`.
535
+
536
+ By using `setDecorator`, you are always sure that the decorator exists:
537
+
538
+ ```ts
539
+ fastify.decorateRequest('user', '')
540
+ fastify.addHook('preHandler', async (req, reply) => {
541
+ // Throws FST_ERR_DEC_UNDECLARED if the decorator does not exist
542
+ req.setDecorator('user-with-typo', 'Bob Dylan')
543
+ })
544
+ ```
545
+
546
+ ---
547
+
548
+ **Type Safety**
549
+
550
+ If the `FastifyRequest` interface does not declare the decorator, you
551
+ would typically need to use type assertions:
552
+
553
+ ```ts
554
+ fastify.addHook('preHandler', async (req, reply) => {
555
+ (req as typeof req & { user: string }).user = 'Bob Dylan'
556
+ })
557
+ ```
558
+
559
+ The `setDecorator<T>` method eliminates the need for explicit type
560
+ assertions while allowing type safety:
561
+
562
+ ```ts
563
+ fastify.addHook('preHandler', async (req, reply) => {
564
+ req.setDecorator<string>('user', 'Bob Dylan')
565
+ })
566
+ ```
@@ -36,6 +36,7 @@
36
36
  - [FST_ERR_DEC_MISSING_DEPENDENCY](#fst_err_dec_missing_dependency)
37
37
  - [FST_ERR_DEC_AFTER_START](#fst_err_dec_after_start)
38
38
  - [FST_ERR_DEC_REFERENCE_TYPE](#fst_err_dec_reference_type)
39
+ - [FST_ERR_DEC_UNDECLARED](#fst_err_dec_undeclared)
39
40
  - [FST_ERR_HOOK_INVALID_TYPE](#fst_err_hook_invalid_type)
40
41
  - [FST_ERR_HOOK_INVALID_HANDLER](#fst_err_hook_invalid_handler)
41
42
  - [FST_ERR_HOOK_INVALID_ASYNC_HANDLER](#fst_err_hook_invalid_async_handler)
@@ -306,6 +307,7 @@ Below is a table with all the error codes used by Fastify.
306
307
  | <a id="fst_err_dec_missing_dependency">FST_ERR_DEC_MISSING_DEPENDENCY</a> | The decorator cannot be registered due to a missing dependency. | Register the missing dependency. | [#1168](https://github.com/fastify/fastify/pull/1168) |
307
308
  | <a id="fst_err_dec_after_start">FST_ERR_DEC_AFTER_START</a> | The decorator cannot be added after start. | Add the decorator before starting the server. | [#2128](https://github.com/fastify/fastify/pull/2128) |
308
309
  | <a id="fst_err_dec_reference_type">FST_ERR_DEC_REFERENCE_TYPE</a> | The decorator cannot be a reference type. | Define the decorator with a getter/setter interface or an empty decorator with a hook. | [#5462](https://github.com/fastify/fastify/pull/5462) |
310
+ | <a id="fst_err_dec_undeclared">FST_ERR_DEC_UNDECLARED</a> | An attempt was made to access a decorator that has not been declared. | Declare the decorator before using it. | [#](https://github.com/fastify/fastify/pull/)
309
311
  | <a id="fst_err_hook_invalid_type">FST_ERR_HOOK_INVALID_TYPE</a> | The hook name must be a string. | Use a string for the hook name. | [#1168](https://github.com/fastify/fastify/pull/1168) |
310
312
  | <a id="fst_err_hook_invalid_handler">FST_ERR_HOOK_INVALID_HANDLER</a> | The hook callback must be a function. | Use a function for the hook callback. | [#1168](https://github.com/fastify/fastify/pull/1168) |
311
313
  | <a id="fst_err_hook_invalid_async_handler">FST_ERR_HOOK_INVALID_ASYNC_HANDLER</a> | Async function has too many arguments. Async hooks should not use the `done` argument. | Remove the `done` argument from the async hook. | [#4367](https://github.com/fastify/fastify/pull/4367) |
package/fastify.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const VERSION = '5.2.2'
3
+ const VERSION = '5.3.1'
4
4
 
5
5
  const Avvio = require('avvio')
6
6
  const http = require('node:http')
@@ -343,6 +343,7 @@ function fastify (options) {
343
343
  decorateRequest: decorator.decorateRequest,
344
344
  hasRequestDecorator: decorator.existRequest,
345
345
  hasReplyDecorator: decorator.existReply,
346
+ getDecorator: decorator.getInstanceDecorator,
346
347
  addHttpMethod,
347
348
  // fake http injection
348
349
  inject,
@@ -639,7 +640,7 @@ function fastify (options) {
639
640
  resolveReady(fastify)
640
641
  fastify[kState].booting = false
641
642
  fastify[kState].ready = true
642
- fastify[kState].promise = null
643
+ fastify[kState].readyPromise = null
643
644
  }
644
645
  }
645
646
 
package/lib/decorate.js CHANGED
@@ -4,7 +4,7 @@ const {
4
4
  kReply,
5
5
  kRequest,
6
6
  kState,
7
- kHasBeenDecorated
7
+ kHasBeenDecorated,
8
8
  } = require('./symbols.js')
9
9
 
10
10
  const {
@@ -12,7 +12,8 @@ const {
12
12
  FST_ERR_DEC_MISSING_DEPENDENCY,
13
13
  FST_ERR_DEC_AFTER_START,
14
14
  FST_ERR_DEC_REFERENCE_TYPE,
15
- FST_ERR_DEC_DEPENDENCY_INVALID_TYPE
15
+ FST_ERR_DEC_DEPENDENCY_INVALID_TYPE,
16
+ FST_ERR_DEC_UNDECLARED,
16
17
  } = require('./errors')
17
18
 
18
19
  function decorate (instance, name, fn, dependencies) {
@@ -32,6 +33,18 @@ function decorate (instance, name, fn, dependencies) {
32
33
  }
33
34
  }
34
35
 
36
+ function getInstanceDecorator (name) {
37
+ if (!checkExistence(this, name)) {
38
+ throw new FST_ERR_DEC_UNDECLARED(name, 'instance')
39
+ }
40
+
41
+ if (typeof this[name] === 'function') {
42
+ return this[name].bind(this)
43
+ }
44
+
45
+ return this[name]
46
+ }
47
+
35
48
  function decorateConstructor (konstructor, name, fn, dependencies) {
36
49
  const instance = konstructor.prototype
37
50
  if (Object.hasOwn(instance, name) || hasKey(konstructor, name)) {
@@ -133,5 +146,7 @@ module.exports = {
133
146
  existReply: checkReplyExistence,
134
147
  dependencies: checkDependencies,
135
148
  decorateReply,
136
- decorateRequest
149
+ decorateRequest,
150
+ getInstanceDecorator,
151
+ hasKey
137
152
  }
package/lib/errors.js CHANGED
@@ -149,6 +149,10 @@ const codes = {
149
149
  'FST_ERR_DEC_REFERENCE_TYPE',
150
150
  "The decorator '%s' of type '%s' is a reference type. Use the { getter, setter } interface instead."
151
151
  ),
152
+ FST_ERR_DEC_UNDECLARED: createError(
153
+ 'FST_ERR_DEC_UNDECLARED',
154
+ "No decorator '%s' has been declared on %s."
155
+ ),
152
156
 
153
157
  /**
154
158
  * hooks
package/lib/reply.js CHANGED
@@ -22,7 +22,7 @@ const {
22
22
  kReplyCacheSerializeFns,
23
23
  kSchemaController,
24
24
  kOptions,
25
- kRouteContext
25
+ kRouteContext,
26
26
  } = require('./symbols.js')
27
27
  const {
28
28
  onSendHookRunner,
@@ -52,8 +52,10 @@ const {
52
52
  FST_ERR_BAD_TRAILER_NAME,
53
53
  FST_ERR_BAD_TRAILER_VALUE,
54
54
  FST_ERR_MISSING_SERIALIZATION_FN,
55
- FST_ERR_MISSING_CONTENTTYPE_SERIALIZATION_FN
55
+ FST_ERR_MISSING_CONTENTTYPE_SERIALIZATION_FN,
56
+ FST_ERR_DEC_UNDECLARED,
56
57
  } = require('./errors')
58
+ const decorators = require('./decorate')
57
59
 
58
60
  const toString = Object.prototype.toString
59
61
 
@@ -475,6 +477,19 @@ Reply.prototype.then = function (fulfilled, rejected) {
475
477
  })
476
478
  }
477
479
 
480
+ Reply.prototype.getDecorator = function (name) {
481
+ if (!decorators.hasKey(this, name) && !decorators.exist(this, name)) {
482
+ throw new FST_ERR_DEC_UNDECLARED(name, 'reply')
483
+ }
484
+
485
+ const decorator = this[name]
486
+ if (typeof decorator === 'function') {
487
+ return decorator.bind(this)
488
+ }
489
+
490
+ return decorator
491
+ }
492
+
478
493
  function preSerializationHook (reply, payload) {
479
494
  if (reply[kRouteContext].preSerialization !== null) {
480
495
  preSerializationHookRunner(
package/lib/request.js CHANGED
@@ -11,9 +11,10 @@ const {
11
11
  kOptions,
12
12
  kRequestCacheValidateFns,
13
13
  kRouteContext,
14
- kRequestOriginalUrl
14
+ kRequestOriginalUrl,
15
15
  } = require('./symbols')
16
- const { FST_ERR_REQ_INVALID_VALIDATION_INVOCATION } = require('./errors')
16
+ const { FST_ERR_REQ_INVALID_VALIDATION_INVOCATION, FST_ERR_DEC_UNDECLARED } = require('./errors')
17
+ const decorators = require('./decorate')
17
18
 
18
19
  const HTTP_PART_SYMBOL_MAP = {
19
20
  body: kSchemaBody,
@@ -141,6 +142,12 @@ function buildRequestWithTrustProxy (R, trustProxy) {
141
142
  return _Request
142
143
  }
143
144
 
145
+ function assertsRequestDecoration (request, name) {
146
+ if (!decorators.hasKey(request, name) && !decorators.exist(request, name)) {
147
+ throw new FST_ERR_DEC_UNDECLARED(name, 'request')
148
+ }
149
+ }
150
+
144
151
  Object.defineProperties(Request.prototype, {
145
152
  server: {
146
153
  get () {
@@ -343,6 +350,25 @@ Object.defineProperties(Request.prototype, {
343
350
 
344
351
  return validate(input)
345
352
  }
353
+ },
354
+ getDecorator: {
355
+ value: function (name) {
356
+ assertsRequestDecoration(this, name)
357
+
358
+ const decorator = this[name]
359
+ if (typeof decorator === 'function') {
360
+ return decorator.bind(this)
361
+ }
362
+
363
+ return decorator
364
+ }
365
+ },
366
+ setDecorator: {
367
+ value: function (name, value) {
368
+ assertsRequestDecoration(this, name)
369
+
370
+ this[name] = value
371
+ }
346
372
  }
347
373
  })
348
374
 
package/lib/validation.js CHANGED
@@ -155,7 +155,7 @@ function validate (context, request, execution) {
155
155
  validatorFunction = context[bodySchema]
156
156
  } else if (context[bodySchema]) {
157
157
  // TODO: add request.contentType and reuse it here
158
- const contentType = request.headers['content-type']?.split(';', 1)[0]
158
+ const contentType = getEssenceMediaType(request.headers['content-type'])
159
159
  const contentSchema = context[bodySchema][contentType]
160
160
  if (contentSchema) {
161
161
  validatorFunction = contentSchema
@@ -254,6 +254,16 @@ function wrapValidationError (result, dataVar, schemaErrorFormatter) {
254
254
  return error
255
255
  }
256
256
 
257
+ /**
258
+ * simple function to retrieve the essence media type
259
+ * @param {string} header
260
+ * @returns {string} Mimetype string.
261
+ */
262
+ function getEssenceMediaType (header) {
263
+ if (!header) return ''
264
+ return header.split(';', 1)[0].trim().toLowerCase()
265
+ }
266
+
257
267
  module.exports = {
258
268
  symbols: { bodySchema, querystringSchema, responseSchema, paramsSchema, headersSchema },
259
269
  compileSchemasForValidation,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fastify",
3
- "version": "5.2.2",
3
+ "version": "5.3.1",
4
4
  "description": "Fast and low overhead web framework, for Node.js",
5
5
  "main": "fastify.js",
6
6
  "type": "commonjs",
@@ -209,9 +209,9 @@
209
209
  "find-my-way": "^9.0.0",
210
210
  "light-my-request": "^6.0.0",
211
211
  "pino": "^9.0.0",
212
- "process-warning": "^4.0.0",
212
+ "process-warning": "^5.0.0",
213
213
  "rfdc": "^1.3.1",
214
- "secure-json-parse": "^3.0.1",
214
+ "secure-json-parse": "^4.0.0",
215
215
  "semver": "^7.6.0",
216
216
  "toad-cache": "^3.7.0"
217
217
  },
@@ -32,6 +32,5 @@ test('ensure the current error serializer is latest', { skip: !isPrepublish }, a
32
32
  const current = await fs.promises.readFile(path.resolve('lib/error-serializer.js'))
33
33
 
34
34
  // line break should not be a problem depends on system
35
- // t.assert.strictEqual(unifyLineBreak(current), unifyLineBreak(code))
36
- t.assert.ok(current)
35
+ t.assert.strictEqual(unifyLineBreak(current), unifyLineBreak(code))
37
36
  })