fastify 4.1.0 → 4.2.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.
@@ -48,7 +48,7 @@ server (fewer resources allocated to a bound-to-fail task) and for the client
48
48
  That will be achieved by wrapping into a custom plugin two main features:
49
49
 
50
50
  1. the mechanism for authenticating with the provider
51
- [decorating](../Referece/Decorators.md) the `fastify` object with the
51
+ [decorating](../Reference/Decorators.md) the `fastify` object with the
52
52
  authentication key (`magicKey` from here onwards)
53
53
  1. the mechanism for denying requests that would, otherwise, fail
54
54
 
@@ -132,9 +132,15 @@ section.
132
132
  sampling process metrics.
133
133
  - [`@dnlup/fastify-traps`](https://github.com/dnlup/fastify-traps) A plugin to
134
134
  close the server gracefully on `SIGINT` and `SIGTERM` signals.
135
+ - [`@eropple/fastify-openapi3`](https://github.com/eropple/fastify-openapi3) Provides
136
+ easy, developer-friendly OpenAPI 3.1 specs + doc explorer based on your routes.
135
137
  - [`@gquittet/graceful-server`](https://github.com/gquittet/graceful-server)
136
138
  Tiny (~5k), Fast, KISS, and dependency-free Node.JS library to make your
137
139
  Fastify API graceful.
140
+ - [`@h4ad/serverless-adapter`](https://github.com/H4ad/serverless-adapter)
141
+ Run REST APIs and other web applications using your existing Node.js
142
+ application framework (Express, Koa, Hapi and Fastify), on top of AWS Lambda,
143
+ Huawei and many other clouds.
138
144
  - [`@immobiliarelabs/fastify-metrics`](https://github.com/immobiliare/fastify-metrics)
139
145
  Minimalistic and opinionated plugin that collects usage/process metrics and
140
146
  dispatches to [statsd](https://github.com/statsd/statsd).
@@ -327,6 +333,8 @@ section.
327
333
  Kubernetes client plugin.
328
334
  - [`fastify-language-parser`](https://github.com/lependu/fastify-language-parser)
329
335
  Fastify plugin to parse request language.
336
+ - [`fastify-lcache`](https://github.com/denbon05/fastify-lcache)
337
+ Lightweight cache plugin
330
338
  - [`fastify-loader`](https://github.com/TheNoim/fastify-loader) Load routes from
331
339
  a directory and inject the Fastify instance in each file.
332
340
  - [`fastify-lured`](https://github.com/lependu/fastify-lured) Plugin to load lua
@@ -478,6 +486,8 @@ section.
478
486
  Events with `reply.sse( … )` to Fastify.
479
487
  - [`fastify-sse-v2`](https://github.com/nodefactoryio/fastify-sse-v2) to provide
480
488
  Server-Sent Events using Async Iterators (supports newer versions of Fastify).
489
+ - [`fastify-ssr-vite`](https://github.com/nineohnine/fastify-ssr-vite) A simple
490
+ plugin for setting up server side rendering with vite.
481
491
  - [`fastify-stripe`](https://github.com/coopflow/fastify-stripe) Plugin to
482
492
  initialize and encapsulate [Stripe
483
493
  Node.js](https://github.com/stripe/stripe-node) instances in Fastify.
@@ -8,7 +8,39 @@ work after upgrading.
8
8
 
9
9
  ## Breaking Changes
10
10
 
11
- ### Deprecation of `app.use()`
11
+ ### Error handling composition ([#3261](https://github.com/fastify/fastify/pull/3261))
12
+
13
+ When an error is thrown in a async error handler function,
14
+ the upper-level error handler is executed if set.
15
+ If there is not a upper-level error handler, the default will
16
+ be executed as it was previously.
17
+
18
+ ```
19
+ import Fastify from 'fastify'
20
+
21
+ const fastify = Fastify()
22
+
23
+ fastify.register(async fastify => {
24
+ fastify.setErrorHandler(async err => {
25
+ console.log(err.message) // 'kaboom'
26
+ throw new Error('caught')
27
+ })
28
+
29
+ fastify.get('/encapsulated', async () => {
30
+ throw new Error('kaboom')
31
+ })
32
+ })
33
+
34
+ fastify.setErrorHandler(async err => {
35
+ console.log(err.message) // 'caught'
36
+ throw new Error('wrapped')
37
+ })
38
+
39
+ const res = await fastify.inject('/encapsulated')
40
+ console.log(res.json().message) // 'wrapped'
41
+ ```
42
+
43
+ ### Deprecation of `app.use()` ([#3506](https://github.com/fastify/fastify/pull/3506))
12
44
 
13
45
  Starting this version of Fastify, we have deprecated the use of `app.use()`. We
14
46
  have decided not to support the use of middlewares. Both
@@ -70,3 +102,10 @@ properties: {
70
102
  }
71
103
  }
72
104
  ```
105
+
106
+ ### Add `reply.trailers` methods ([#3794](https://github.com/fastify/fastify/pull/3794))
107
+
108
+ Fastify now supports the [HTTP Trailer] response headers.
109
+
110
+
111
+ [HTTP Trailer]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Trailer
@@ -24,23 +24,31 @@ snippet of code.
24
24
 
25
25
  ### Contents
26
26
 
27
- - [AWS Lambda](#aws-lambda)
27
+ - [AWS](#aws)
28
28
  - [Google Cloud Functions](#google-cloud-functions)
29
29
  - [Google Cloud Run](#google-cloud-run)
30
30
  - [Netlify Lambda](#netlify-lambda)
31
31
  - [Vercel](#vercel)
32
32
 
33
- ## AWS Lambda
33
+ ## AWS
34
+
35
+ To integrate with AWS, you have two choices of library:
36
+
37
+ - Using [@fastify/aws-lambda](https://github.com/fastify/aws-lambda-fastify)
38
+ which only adds API Gateway support but has heavy optimizations for fastify.
39
+ - Using [@h4ad/serverless-adapter](https://github.com/H4ad/serverless-adapter)
40
+ which is a little slower as it creates an HTTP request for each AWS event but
41
+ has support for more AWS services such as: AWS SQS, AWS SNS and others.
42
+
43
+ So you can decide which option is best for you, but you can test both libraries.
44
+
45
+ ### Using @fastify/aws-lambda
34
46
 
35
47
  The sample provided allows you to easily build serverless web
36
48
  applications/services and RESTful APIs using Fastify on top of AWS Lambda and
37
49
  Amazon API Gateway.
38
50
 
39
- *Note: Using
40
- [@fastify/aws-lambda](https://github.com/fastify/aws-lambda-fastify) is just one
41
- possible way.*
42
-
43
- ### app.js
51
+ #### app.js
44
52
 
45
53
  ```js
46
54
  const fastify = require('fastify');
@@ -71,7 +79,7 @@ When you execute your Fastify application like always, i.e. `node app.js` *(the
71
79
  detection for this could be `require.main === module`)*, you can normally listen
72
80
  to your port, so you can still run your Fastify function locally.
73
81
 
74
- ### lambda.js
82
+ #### lambda.js
75
83
 
76
84
  ```js
77
85
  const awsLambdaFastify = require('@fastify/aws-lambda')
@@ -99,14 +107,13 @@ signature to be used as a lambda `handler` function. This way all the incoming
99
107
  events (API Gateway requests) are passed to the `proxy` function of
100
108
  [@fastify/aws-lambda](https://github.com/fastify/aws-lambda-fastify).
101
109
 
102
- ### Example
110
+ #### Example
103
111
 
104
112
  An example deployable with
105
113
  [claudia.js](https://claudiajs.com/tutorials/serverless-express.html) can be
106
114
  found
107
115
  [here](https://github.com/claudiajs/example-projects/tree/master/fastify-app-lambda).
108
116
 
109
-
110
117
  ### Considerations
111
118
 
112
119
  - API Gateway does not support streams yet, so you are not able to handle
@@ -114,6 +121,12 @@ found
114
121
  - API Gateway has a timeout of 29 seconds, so it is important to provide a reply
115
122
  during this time.
116
123
 
124
+ #### Beyond API Gateway
125
+
126
+ If you need to integrate with more AWS services, take a look at
127
+ [@h4ad/serverless-adapter](https://viniciusl.com.br/serverless-adapter/docs/main/frameworks/fastify)
128
+ on Fastify to find out how to integrate.
129
+
117
130
  ## Google Cloud Functions
118
131
 
119
132
  ### Creation of Fastify instance
@@ -28,6 +28,7 @@ are Request/Reply hooks and application hooks:
28
28
  - [onRegister](#onregister)
29
29
  - [Scope](#scope)
30
30
  - [Route level hooks](#route-level-hooks)
31
+ - [Using Hooks to Inject Custom Properties](#using-hooks-to-inject-custom-properties)
31
32
  - [Diagnostics Channel Hooks](#diagnostics-channel-hooks)
32
33
 
33
34
  **Notice:** the `done` callback is not available when using `async`/`await` or
@@ -649,6 +650,57 @@ fastify.route({
649
650
 
650
651
  **Note**: both options also accept an array of functions.
651
652
 
653
+ ## Using Hooks to Inject Custom Properties
654
+ <a id="using-hooks-to-inject-custom-properties"></a>
655
+
656
+ You can use a hook to inject custom properties into incoming requests.
657
+ This is useful for reusing processed data from hooks in controllers.
658
+
659
+ A very common use case is, for example, checking user authentication based
660
+ on their token and then storing their recovered data into
661
+ the [Request](./Request.md) instance. This way, your controllers can read it
662
+ easily with `request.authenticatedUser` or whatever you want to call it.
663
+ That's how it might look like:
664
+
665
+ ```js
666
+ fastify.addHook('preParsing', async (request) => {
667
+ request.authenticatedUser = {
668
+ id: 42,
669
+ name: 'Jane Doe',
670
+ role: 'admin'
671
+ }
672
+ })
673
+
674
+ fastify.get('/me/is-admin', async function (req, reply) {
675
+ return { isAdmin: req.authenticatedUser?.role === 'admin' || false }
676
+ })
677
+ ```
678
+
679
+ Note that `.authenticatedUser` could actually be any property name
680
+ choosen by yourself. Using your own custom property prevents you
681
+ from mutating existing properties, which
682
+ would be a dangerous and destructive operation. So be careful and
683
+ make sure your property is entirely new, also using this approach
684
+ only for very specific and small cases like this example.
685
+
686
+ Regarding TypeScript in this example, you'd need to update the
687
+ `FastifyRequest` core interface to include your new property typing
688
+ (for more about it, see [TypeScript](./TypeScript.md) page), like:
689
+
690
+ ```ts
691
+ interface AuthenticatedUser { /* ... */ }
692
+
693
+ declare module 'fastify' {
694
+ export interface FastifyRequest {
695
+ authenticatedUser?: AuthenticatedUser;
696
+ }
697
+ }
698
+ ```
699
+
700
+ Although this is a very pragmatic approach, if you're trying to do
701
+ something more complex that changes these core objects, then
702
+ consider creating a custom [Plugin](./Plugins.md) instead.
703
+
652
704
  ## Diagnostics Channel Hooks
653
705
 
654
706
  > **Note:** The `diagnostics_channel` is currently experimental on Node.js, so
@@ -13,7 +13,7 @@ way we create a *directed acyclic graph* (DAG) and we will not have issues
13
13
  caused by cross dependencies.
14
14
 
15
15
  You may have already seen in the [Getting
16
- Started]((../Guides/Getting-Started.md#your-first-plugin)) guide how easy it is
16
+ Started](../Guides/Getting-Started.md#your-first-plugin) guide how easy it is
17
17
  to use this API:
18
18
  ```
19
19
  fastify.register(plugin, [options])
@@ -199,17 +199,18 @@ Below is how to setup schema validation using vanilla `typebox` and
199
199
  #### typebox
200
200
 
201
201
  A useful library for building types and a schema at once is
202
- [typebox](https://www.npmjs.com/package/@sinclair/typebox). With typebox you
203
- define your schema within your code and use them directly as types or schemas as
204
- you need them.
202
+ [typebox](https://www.npmjs.com/package/@sinclair/typebox) along with
203
+ [fastify-type-provider-typebox](https://github.com/fastify/fastify-type-provider-typebox).
204
+ With typebox you define your schema within your code and use them
205
+ directly as types or schemas as you need them.
205
206
 
206
207
  When you want to use it for validation of some payload in a fastify route you
207
208
  can do it as follows:
208
209
 
209
- 1. Install `typebox` in your project.
210
+ 1. Install `typebox` and `fastify-type-provider-typebox` in your project.
210
211
 
211
212
  ```bash
212
- npm i @sinclair/typebox
213
+ npm i @sinclair/typebox @fastify/type-provider-typebox
213
214
  ```
214
215
 
215
216
  2. Define the schema you need with `Type` and create the respective type with
@@ -218,40 +219,52 @@ can do it as follows:
218
219
  ```typescript
219
220
  import { Static, Type } from '@sinclair/typebox'
220
221
 
221
- const User = Type.Object({
222
+ export const User = Type.Object({
222
223
  name: Type.String(),
223
- mail: Type.Optional(Type.String({ format: "email" })),
224
- });
225
- type UserType = Static<typeof User>;
224
+ mail: Type.Optional(Type.String({ format: 'email' })),
225
+ })
226
+
227
+ export type UserType = Static<typeof User>
226
228
  ```
227
229
 
228
230
  3. Use the defined type and schema during the definition of your route
229
231
 
230
232
  ```typescript
231
- const app = fastify();
233
+ import Fastify from 'fastify'
234
+ import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox'
235
+ // ...
236
+
237
+ const fastify = Fastify().withTypeProvider<TypeBoxTypeProvider>()
232
238
 
233
- app.post<{ Body: UserType; Reply: UserType }>(
234
- "/",
239
+ app.post<{ Body: UserType, Reply: UserType }>(
240
+ '/',
235
241
  {
236
242
  schema: {
237
243
  body: User,
238
244
  response: {
239
- 200: User,
245
+ 200: User
240
246
  },
241
247
  },
242
248
  },
243
249
  (request, reply) => {
244
- const { body: user } = request;
245
- /* user has type
246
- * const user: StaticProperties<{
247
- * name: TString;
248
- * mail: TOptional<TString>;
249
- * }>
250
- */
251
- //...
252
- reply.status(200).send(user);
250
+ // The `name` and `mail` types are automatically inferred
251
+ const { name, mail } = request.body;
252
+ reply.status(200).send({ name, mail });
253
+ }
254
+ )
255
+ ```
256
+
257
+ **Note** For Ajv version 7 and above is required to use the `ajvTypeBoxPlugin`:
258
+
259
+ ```typescript
260
+ import Fastify from 'fastify'
261
+ import { ajvTypeBoxPlugin, TypeBoxTypeProvider } from '@fastify/type-provider-typebox'
262
+
263
+ const fastify = Fastify({
264
+ ajv: {
265
+ plugins: [ajvTypeBoxPlugin]
253
266
  }
254
- );
267
+ }).withTypeProvider<TypeBoxTypeProvider>()
255
268
  ```
256
269
 
257
270
  #### Schemas in JSON Files
@@ -1359,7 +1372,7 @@ A method for checking the existence of a type parser of a certain content type
1359
1372
 
1360
1373
  ##### fastify.FastifyError
1361
1374
 
1362
- [src](https://github.com/fastify/fastify/blob/main/types/error.d.ts#L17)
1375
+ [src](https://github.com/fastify/fastify/blob/main/fastify.d.ts#L179)
1363
1376
 
1364
1377
  FastifyError is a custom error object that includes status code and validation
1365
1378
  results.
@@ -1369,7 +1382,7 @@ properties: `statusCode: number` and `validation: ValiationResult[]`.
1369
1382
 
1370
1383
  ##### fastify.ValidationResult
1371
1384
 
1372
- [src](https://github.com/fastify/fastify/blob/main/types/error.d.ts#L4)
1385
+ [src](https://github.com/fastify/fastify/blob/main/fastify.d.ts#L184)
1373
1386
 
1374
1387
  The route validation internally relies upon Ajv, which is a high-performance
1375
1388
  JSON schema validator.
@@ -484,6 +484,17 @@ fastify.post('/the/url', {
484
484
  }, handler)
485
485
  ```
486
486
 
487
+ ##### .statusCode property
488
+
489
+ All validation errors will be added a `.statusCode` property set to `400`. This guarantees
490
+ that the default error handler will set the status code of the response to `400`.
491
+
492
+ ```js
493
+ fastify.setErrorHandler(function (error, request, reply) {
494
+ request.log.error(error, `This error has status code ${error.statusCode}`)
495
+ reply.status(error.statusCode).send(error)
496
+ })
497
+ ```
487
498
 
488
499
  ##### Validation messages with other validation libraries
489
500
 
package/docs/index.md CHANGED
@@ -16,7 +16,7 @@ Complete newcomers to Fastify should first read our [Getting
16
16
  Started](./Guides/Getting-Started.md) guide.
17
17
 
18
18
  Developers experienced with Fastify should consult the [reference
19
- documentation](./Reference/index.md) directly to find the topic they are seeking
19
+ documentation](./Reference/Index.md) directly to find the topic they are seeking
20
20
  more information about.
21
21
 
22
22
  ## Additional Documentation
package/fastify.d.ts CHANGED
@@ -183,10 +183,10 @@ declare module '@fastify/error' {
183
183
 
184
184
  export interface ValidationResult {
185
185
  keyword: string;
186
- dataPath: string;
186
+ instancePath: string;
187
187
  schemaPath: string;
188
188
  params: Record<string, string | string[]>;
189
- message: string;
189
+ message?: string;
190
190
  }
191
191
 
192
192
  /* Export all additional types */
package/fastify.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const VERSION = '4.1.0'
3
+ const VERSION = '4.2.0'
4
4
 
5
5
  const Avvio = require('avvio')
6
6
  const http = require('http')
@@ -0,0 +1,27 @@
1
+ const Fastify = require('../fastify')
2
+
3
+ const fastify = Fastify()
4
+
5
+ fastify.listen({
6
+ host: '::',
7
+ port: 3000
8
+ })
9
+
10
+ fastify.get('/', async function (request, reply) {
11
+ reply.code(200).send({ data: 'home page' })
12
+ })
13
+
14
+ fastify.post('/post/:id', async function (request, reply) {
15
+ const { id } = request.params
16
+ reply.code(201).send({ data: `${id}` })
17
+ })
18
+
19
+ fastify.put('/put/:id', async function (request, reply) {
20
+ const { id } = request.params
21
+ reply.code(200).send({ data: `${id}` })
22
+ })
23
+
24
+ fastify.delete('/delete/:id', async function (request, reply) {
25
+ const { id } = request.params
26
+ reply.code(204).send({ data: `${id}` })
27
+ })
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/bash
2
+
3
+ set -e
4
+
5
+ NUMBER=$RANDOM
6
+ curl -i -X GET -H 'Content-Type: application/json' localhost:3000/ > GET
7
+ if [[ ! $(cat GET | head -1| cut -f2 -d" ") == "200" || ! $(cat GET | tail -1| cut -f4 -d"\"") == "home page" ]] ; then
8
+ exit 1
9
+ fi;
10
+ curl -i -X POST -H 'Content-Type: application/json' localhost:3000/post/$NUMBER --data {} > POST
11
+ if [[ ! $(cat POST | head -1| cut -f2 -d" ") == "201" || ! $(cat POST | tail -1| cut -f4 -d"\"") == $(echo $NUMBER) ]]; then
12
+ exit 1
13
+ fi;
14
+ curl -i -X PUT -H 'Content-Type: application/json' localhost:3000/put/$NUMBER --data {} > PUT
15
+ if [[ ! $(cat PUT | head -1| cut -f2 -d" ") == "200" || ! $(cat PUT | tail -1| cut -f4 -d"\"") == $(echo $NUMBER) ]]; then
16
+ exit 1
17
+ fi;
18
+ curl -i -X DELETE -H 'Content-Type: application/json' localhost:3000/delete/$NUMBER --data {} > DELETE
19
+ if [[ ! $(cat DELETE | head -1| cut -f2 -d" ") == "204" ]]; then
20
+ exit 1
21
+ fi;
22
+
23
+ rm -f GET POST PUT DELETE
package/lib/context.js CHANGED
@@ -9,7 +9,8 @@ const {
9
9
  kRequest,
10
10
  kBodyLimit,
11
11
  kLogLevel,
12
- kContentTypeParser
12
+ kContentTypeParser,
13
+ kRouteByFastify
13
14
  } = require('./symbols.js')
14
15
 
15
16
  // Objects that holds the context of every request
@@ -25,7 +26,8 @@ function Context ({
25
26
  attachValidation,
26
27
  replySerializer,
27
28
  schemaErrorFormatter,
28
- server
29
+ server,
30
+ isFastify
29
31
  }) {
30
32
  this.schema = schema
31
33
  this.handler = handler
@@ -50,6 +52,7 @@ function Context ({
50
52
  this.attachValidation = attachValidation
51
53
  this[kReplySerializerDefault] = replySerializer
52
54
  this.schemaErrorFormatter = schemaErrorFormatter || server[kSchemaErrorFormatter] || defaultSchemaErrorFormatter
55
+ this[kRouteByFastify] = isFastify
53
56
 
54
57
  this.server = server
55
58
  }
@@ -39,7 +39,7 @@ class Serializer {
39
39
  } else {
40
40
  /* eslint no-undef: "off" */
41
41
  const integer = this.parseInteger(i)
42
- if (Number.isNaN(integer)) {
42
+ if (Number.isNaN(integer) || !Number.isFinite(integer)) {
43
43
  throw new Error(`The value "${i}" cannot be converted to an integer.`)
44
44
  } else {
45
45
  return '' + integer
@@ -55,6 +55,8 @@ class Serializer {
55
55
  const num = Number(i)
56
56
  if (Number.isNaN(num)) {
57
57
  throw new Error(`The value "${i}" cannot be converted to a number.`)
58
+ } else if (!Number.isFinite(num)) {
59
+ return null
58
60
  } else {
59
61
  return '' + num
60
62
  }
@@ -72,44 +74,44 @@ class Serializer {
72
74
  return bool === null ? 'null' : this.asBoolean(bool)
73
75
  }
74
76
 
75
- asDatetime (date, skipQuotes) {
76
- const quotes = skipQuotes === true ? '' : '"'
77
+ asDatetime (date) {
78
+ const quotes = '"'
77
79
  if (date instanceof Date) {
78
80
  return quotes + date.toISOString() + quotes
79
81
  }
80
- return this.asString(date, skipQuotes)
82
+ return this.asString(date)
81
83
  }
82
84
 
83
- asDatetimeNullable (date, skipQuotes) {
84
- return date === null ? 'null' : this.asDatetime(date, skipQuotes)
85
+ asDatetimeNullable (date) {
86
+ return date === null ? 'null' : this.asDatetime(date)
85
87
  }
86
88
 
87
- asDate (date, skipQuotes) {
88
- const quotes = skipQuotes === true ? '' : '"'
89
+ asDate (date) {
90
+ const quotes = '"'
89
91
  if (date instanceof Date) {
90
92
  return quotes + new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(0, 10) + quotes
91
93
  }
92
- return this.asString(date, skipQuotes)
94
+ return this.asString(date)
93
95
  }
94
96
 
95
- asDateNullable (date, skipQuotes) {
96
- return date === null ? 'null' : this.asDate(date, skipQuotes)
97
+ asDateNullable (date) {
98
+ return date === null ? 'null' : this.asDate(date)
97
99
  }
98
100
 
99
- asTime (date, skipQuotes) {
100
- const quotes = skipQuotes === true ? '' : '"'
101
+ asTime (date) {
102
+ const quotes = '"'
101
103
  if (date instanceof Date) {
102
104
  return quotes + new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(11, 19) + quotes
103
105
  }
104
- return this.asString(date, skipQuotes)
106
+ return this.asString(date)
105
107
  }
106
108
 
107
- asTimeNullable (date, skipQuotes) {
108
- return date === null ? 'null' : this.asTime(date, skipQuotes)
109
+ asTimeNullable (date) {
110
+ return date === null ? 'null' : this.asTime(date)
109
111
  }
110
112
 
111
- asString (str, skipQuotes) {
112
- const quotes = skipQuotes === true ? '' : '"'
113
+ asString (str) {
114
+ const quotes = '"'
113
115
  if (str instanceof Date) {
114
116
  return quotes + str.toISOString() + quotes
115
117
  } else if (str === null) {
@@ -119,11 +121,6 @@ class Serializer {
119
121
  } else if (typeof str !== 'string') {
120
122
  str = str.toString()
121
123
  }
122
- // If we skipQuotes it means that we are using it as test
123
- // no need to test the string length for the render
124
- if (skipQuotes) {
125
- return str
126
- }
127
124
 
128
125
  if (str.length < 42) {
129
126
  return this.asStringSmall(str)
@@ -185,7 +182,7 @@ class Serializer {
185
182
  }
186
183
 
187
184
  function anonymous0 (input) {
188
- // main
185
+ // #
189
186
 
190
187
  var obj = (input && typeof input.toJSON === 'function')
191
188
  ? input.toJSON()
@@ -90,7 +90,7 @@ function preValidationCallback (err, request, reply) {
90
90
  const result = validateSchema(reply.context, request)
91
91
  if (result) {
92
92
  if (reply.context.attachValidation === false) {
93
- reply.code(400).send(result)
93
+ reply.send(result)
94
94
  return
95
95
  }
96
96
 
package/lib/route.js CHANGED
@@ -8,7 +8,7 @@ const supportedMethods = ['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT', 'OPTI
8
8
  const { normalizeSchema } = require('./schemas')
9
9
  const { parseHeadOnSendHandlers } = require('./headRoute')
10
10
  const warning = require('./warnings')
11
- const { kRequestAcceptVersion } = require('./symbols')
11
+ const { kRequestAcceptVersion, kRouteByFastify } = require('./symbols')
12
12
 
13
13
  const {
14
14
  compileSchemasForValidation,
@@ -113,7 +113,7 @@ function buildRouting (options) {
113
113
  }
114
114
 
115
115
  // Convert shorthand to extended route declaration
116
- function prepareRoute ({ method, url, options, handler }) {
116
+ function prepareRoute ({ method, url, options, handler, isFastify }) {
117
117
  if (typeof url !== 'string') {
118
118
  throw new FST_ERR_INVALID_URL(typeof url)
119
119
  }
@@ -140,11 +140,11 @@ function buildRouting (options) {
140
140
  handler: handler || (options && options.handler)
141
141
  })
142
142
 
143
- return route.call(this, { options })
143
+ return route.call(this, { options, isFastify })
144
144
  }
145
145
 
146
146
  // Route management
147
- function route ({ options }) {
147
+ function route ({ options, isFastify }) {
148
148
  // Since we are mutating/assigning only top level props, it is fine to have a shallow copy using the spread operator
149
149
  const opts = { ...options }
150
150
 
@@ -176,30 +176,30 @@ function buildRouting (options) {
176
176
  if (path === '/' && prefix.length > 0 && opts.method !== 'HEAD') {
177
177
  switch (opts.prefixTrailingSlash) {
178
178
  case 'slash':
179
- addNewRoute.call(this, { path })
179
+ addNewRoute.call(this, { path, isFastify })
180
180
  break
181
181
  case 'no-slash':
182
- addNewRoute.call(this, { path: '' })
182
+ addNewRoute.call(this, { path: '', isFastify })
183
183
  break
184
184
  case 'both':
185
185
  default:
186
- addNewRoute.call(this, { path: '' })
186
+ addNewRoute.call(this, { path: '', isFastify })
187
187
  // If ignoreTrailingSlash is set to true we need to add only the '' route to prevent adding an incomplete one.
188
188
  if (ignoreTrailingSlash !== true && (ignoreDuplicateSlashes !== true || !prefix.endsWith('/'))) {
189
- addNewRoute.call(this, { path, prefixing: true })
189
+ addNewRoute.call(this, { path, prefixing: true, isFastify })
190
190
  }
191
191
  }
192
192
  } else if (path[0] === '/' && prefix.endsWith('/')) {
193
193
  // Ensure that '/prefix/' + '/route' gets registered as '/prefix/route'
194
- addNewRoute.call(this, { path: path.slice(1) })
194
+ addNewRoute.call(this, { path: path.slice(1), isFastify })
195
195
  } else {
196
- addNewRoute.call(this, { path })
196
+ addNewRoute.call(this, { path, isFastify })
197
197
  }
198
198
 
199
199
  // chainable api
200
200
  return this
201
201
 
202
- function addNewRoute ({ path, prefixing = false }) {
202
+ function addNewRoute ({ path, prefixing = false, isFastify = false }) {
203
203
  const url = prefix + path
204
204
 
205
205
  opts.url = url
@@ -241,7 +241,8 @@ function buildRouting (options) {
241
241
  attachValidation: opts.attachValidation,
242
242
  schemaErrorFormatter: opts.schemaErrorFormatter,
243
243
  replySerializer: this[kReplySerializerDefault],
244
- server: this
244
+ server: this,
245
+ isFastify
245
246
  })
246
247
 
247
248
  if (opts.version) {
@@ -249,13 +250,20 @@ function buildRouting (options) {
249
250
  constraints.version = opts.version
250
251
  }
251
252
 
252
- const headRouteExists = opts.method === 'HEAD' && router.find(opts.method, opts.url, constraints) != null
253
+ const headHandler = router.find('HEAD', opts.url, constraints)
254
+ const hasHEADHandler = headHandler != null
253
255
 
254
- // Check if the current route is not for a sibling HEAD one
255
- if (!headRouteExists) {
256
- try {
257
- router.on(opts.method, opts.url, { constraints }, routeHandler, context)
258
- } catch (error) {
256
+ // remove the head route created by fastify
257
+ if (hasHEADHandler && !context[kRouteByFastify] && headHandler.store[kRouteByFastify]) {
258
+ router.off(opts.method, opts.url, { constraints })
259
+ }
260
+
261
+ try {
262
+ router.on(opts.method, opts.url, { constraints }, routeHandler, context)
263
+ } catch (error) {
264
+ // any route insertion error created by fastify can be safely ignore
265
+ // because it only duplicate route for head
266
+ if (!context[kRouteByFastify]) {
259
267
  const isDuplicatedRoute = error.message.includes(`Method '${opts.method}' already declared for route '${opts.url}'`)
260
268
  if (isDuplicatedRoute) {
261
269
  throw new FST_ERR_DUPLICATED_ROUTE(opts.method, opts.url)
@@ -320,19 +328,21 @@ function buildRouting (options) {
320
328
  }
321
329
  })
322
330
 
323
- const { exposeHeadRoute } = opts
324
- const hasRouteExposeHeadRouteFlag = exposeHeadRoute != null
325
- const shouldExposeHead = hasRouteExposeHeadRouteFlag ? exposeHeadRoute : globalExposeHeadRoutes
326
-
327
- if (shouldExposeHead && options.method === 'GET' && !headRouteExists) {
328
- const onSendHandlers = parseHeadOnSendHandlers(opts.onSend)
329
- prepareRoute.call(this, { method: 'HEAD', url: path, options: { ...opts, onSend: onSendHandlers } })
330
- } else if (headRouteExists && exposeHeadRoute) {
331
- warning.emit('FSTDEP007')
332
- }
333
-
334
331
  done(notHandledErr)
335
332
  })
333
+
334
+ // register head route in sync
335
+ // we must place it after the `this.after`
336
+ const { exposeHeadRoute } = opts
337
+ const hasRouteExposeHeadRouteFlag = exposeHeadRoute != null
338
+ const shouldExposeHead = hasRouteExposeHeadRouteFlag ? exposeHeadRoute : globalExposeHeadRoutes
339
+
340
+ if (shouldExposeHead && options.method === 'GET' && !hasHEADHandler) {
341
+ const onSendHandlers = parseHeadOnSendHandlers(opts.onSend)
342
+ prepareRoute.call(this, { method: 'HEAD', url: path, options: { ...opts, onSend: onSendHandlers }, isFastify: true })
343
+ } else if (hasHEADHandler && exposeHeadRoute) {
344
+ warning.emit('FSTDEP007')
345
+ }
336
346
  }
337
347
  }
338
348
 
package/lib/symbols.js CHANGED
@@ -47,7 +47,8 @@ const keys = {
47
47
  kTestInternals: Symbol('fastify.testInternals'),
48
48
  kErrorHandler: Symbol('fastify.errorHandler'),
49
49
  kHasBeenDecorated: Symbol('fastify.hasBeenDecorated'),
50
- kKeepAliveConnections: Symbol('fastify.keepAliveConnections')
50
+ kKeepAliveConnections: Symbol('fastify.keepAliveConnections'),
51
+ kRouteByFastify: Symbol('fastify.routeByFastify')
51
52
  }
52
53
 
53
54
  module.exports = keys
package/lib/validation.js CHANGED
@@ -106,11 +106,13 @@ function validate (context, request) {
106
106
 
107
107
  function wrapValidationError (result, dataVar, schemaErrorFormatter) {
108
108
  if (result instanceof Error) {
109
+ result.statusCode = result.statusCode || 400
109
110
  result.validationContext = result.validationContext || dataVar
110
111
  return result
111
112
  }
112
113
 
113
114
  const error = schemaErrorFormatter(result, dataVar)
115
+ error.statusCode = error.statusCode || 400
114
116
  error.validation = result
115
117
  error.validationContext = dataVar
116
118
  return error
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fastify",
3
- "version": "4.1.0",
3
+ "version": "4.2.0",
4
4
  "description": "Fast and low overhead web framework, for Node.js",
5
5
  "main": "fastify.js",
6
6
  "type": "commonjs",
@@ -146,7 +146,7 @@
146
146
  "eslint-plugin-n": "^15.2.0",
147
147
  "eslint-plugin-promise": "^6.0.0",
148
148
  "fast-json-body": "^1.1.0",
149
- "fast-json-stringify": "^4.2.0",
149
+ "fast-json-stringify": "^5.0.0",
150
150
  "fastify-plugin": "^3.0.1",
151
151
  "fluent-json-schema": "^3.1.0",
152
152
  "form-data": "^4.0.0",
@@ -179,10 +179,10 @@
179
179
  "dependencies": {
180
180
  "@fastify/ajv-compiler": "^3.1.0",
181
181
  "@fastify/error": "^3.0.0",
182
- "@fastify/fast-json-stringify-compiler": "^3.0.1",
182
+ "@fastify/fast-json-stringify-compiler": "^4.0.0",
183
183
  "abstract-logging": "^2.0.1",
184
184
  "avvio": "^8.1.3",
185
- "find-my-way": "^6.3.0",
185
+ "find-my-way": "^7.0.0",
186
186
  "light-my-request": "^5.0.0",
187
187
  "pino": "^8.0.0",
188
188
  "process-warning": "^2.0.0",
@@ -799,6 +799,27 @@ test('onRoute hook with many prefix', t => {
799
799
  fastify.ready(err => { t.error(err) })
800
800
  })
801
801
 
802
+ test('onRoute hook should not be called when it registered after route', t => {
803
+ t.plan(3)
804
+ const fastify = Fastify()
805
+
806
+ fastify.addHook('onRoute', () => {
807
+ t.pass()
808
+ })
809
+
810
+ fastify.get('/', function (req, reply) {
811
+ reply.send()
812
+ })
813
+
814
+ fastify.addHook('onRoute', () => {
815
+ t.fail('should not be called')
816
+ })
817
+
818
+ fastify.ready(err => {
819
+ t.error(err)
820
+ })
821
+ })
822
+
802
823
  test('onResponse hook should log request error', t => {
803
824
  t.plan(4)
804
825
 
@@ -144,7 +144,7 @@ test('pretty print - commonPrefix', t => {
144
144
  `
145
145
  const flatExpected = `└── / (-)
146
146
  ├── helicopter (GET, HEAD)
147
- └── hello (GET, PUT, HEAD)
147
+ └── hello (GET, HEAD, PUT)
148
148
  `
149
149
  t.equal(typeof radixTree, 'string')
150
150
  t.equal(typeof flatTree, 'string')
@@ -200,7 +200,7 @@ test('pretty print - includeMeta, includeHooks', t => {
200
200
  │ • (onTimeout) ["onTimeout()"]
201
201
  │ • (onRequest) ["anonymous()"]
202
202
  │ • (errorHandler) "defaultErrorHandler()"
203
- └── hello (GET, PUT, HEAD)
203
+ └── hello (GET, HEAD, PUT)
204
204
  • (onTimeout) ["onTimeout()"]
205
205
  • (onRequest) ["anonymous()"]
206
206
  • (errorHandler) "defaultErrorHandler()"
@@ -210,7 +210,7 @@ test('pretty print - includeMeta, includeHooks', t => {
210
210
  ├── helicopter (GET, HEAD)
211
211
  │ • (onTimeout) ["onTimeout()"]
212
212
  │ • (onRequest) ["anonymous()"]
213
- └── hello (GET, PUT, HEAD)
213
+ └── hello (GET, HEAD, PUT)
214
214
  • (onTimeout) ["onTimeout()"]
215
215
  • (onRequest) ["anonymous()"]
216
216
  `
@@ -324,7 +324,7 @@ test('invalid schema - ajv', t => {
324
324
 
325
325
  fastify.setErrorHandler((err, request, reply) => {
326
326
  t.ok(Array.isArray(err.validation))
327
- reply.send('error')
327
+ reply.code(400).send('error')
328
328
  })
329
329
 
330
330
  fastify.inject({
@@ -206,7 +206,7 @@ test('Should throw of the schema does not exists in output', t => {
206
206
 
207
207
  fastify.ready(err => {
208
208
  t.equal(err.code, 'FST_ERR_SCH_SERIALIZATION_BUILD')
209
- t.match(err.message, /^Failed building the serialization schema for GET: \/:id, due to error Cannot read propert.*/) // error from fast-json-strinfigy
209
+ t.match(err.message, /^Failed building the serialization schema for GET: \/:id, due to error Cannot find reference.*/) // error from fast-json-strinfigy
210
210
  })
211
211
  })
212
212
 
@@ -826,7 +826,7 @@ test('Validation context in validation result', t => {
826
826
  t.equal(err instanceof Error, true)
827
827
  t.ok(err.validation, 'detailed errors')
828
828
  t.equal(err.validationContext, 'body')
829
- reply.send()
829
+ reply.code(400).send()
830
830
  })
831
831
  fastify.post('/', {
832
832
  handler: echoParams,
@@ -7,8 +7,10 @@ import fastify, {
7
7
  LightMyRequestChain,
8
8
  LightMyRequestResponse,
9
9
  LightMyRequestCallback,
10
- InjectOptions, FastifyBaseLogger
10
+ InjectOptions, FastifyBaseLogger,
11
+ ValidationResult
11
12
  } from '../../fastify'
13
+ import { ErrorObject as AjvErrorObject } from 'ajv'
12
14
  import * as http from 'http'
13
15
  import * as https from 'https'
14
16
  import * as http2 from 'http2'
@@ -208,3 +210,12 @@ fastify().then(fastifyInstance => expectAssignable<FastifyInstance>(fastifyInsta
208
210
  expectAssignable<FastifyPluginAsync>(async () => {})
209
211
  expectAssignable<FastifyPluginCallback>(() => {})
210
212
  expectAssignable<FastifyPlugin>(() => {})
213
+
214
+ const ajvErrorObject: AjvErrorObject = {
215
+ keyword: '',
216
+ instancePath: '',
217
+ schemaPath: '',
218
+ params: {},
219
+ message: ''
220
+ }
221
+ expectAssignable<ValidationResult>(ajvErrorObject)
@@ -49,7 +49,7 @@ interface RequestData extends RequestGenericInterface {
49
49
  type Handler = RouteHandler<RequestData>
50
50
 
51
51
  type CustomRequest = FastifyRequest<{
52
- Body: RequestBody;
52
+ Body: RequestBody | undefined;
53
53
  Querystring: RequestQuerystring;
54
54
  Params: RequestParams;
55
55
  Headers: RequestHeaders;
@@ -85,7 +85,7 @@ const getHandler: RouteHandler = function (request, _reply) {
85
85
  expectType<FastifyInstance>(request.server)
86
86
  }
87
87
 
88
- const getHandlerWithCustomLogger: RouteHandlerMethod<RawServerDefault, RawRequestDefaultExpression, RawReplyDefaultExpression, RouteGenericInterface, ContextConfigDefault, FastifySchema, FastifyTypeProviderDefault, ResolveFastifyReplyReturnType<FastifyTypeProviderDefault, FastifySchema, RouteGenericInterface>, ResolveFastifyRequestType<FastifyTypeProviderDefault, FastifySchema, RouteGenericInterface>, CustomLoggerInterface> = function (request, _reply) {
88
+ const getHandlerWithCustomLogger: RouteHandlerMethod<RawServerDefault, RawRequestDefaultExpression, RawReplyDefaultExpression, RouteGenericInterface, ContextConfigDefault, FastifySchema, FastifyTypeProviderDefault, ResolveFastifyRequestType<FastifyTypeProviderDefault, FastifySchema, RouteGenericInterface>, CustomLoggerInterface> = function (request, _reply) {
89
89
  expectType<CustomLoggerInterface>(request.log)
90
90
  }
91
91
 
@@ -104,11 +104,15 @@ const postHandler: Handler = function (request) {
104
104
  }
105
105
 
106
106
  function putHandler (request: CustomRequest, reply: FastifyReply) {
107
- expectType<RequestBody>(request.body)
107
+ expectType<RequestBody | undefined>(request.body)
108
108
  expectType<RequestParams>(request.params)
109
109
  expectType<RequestHeaders & RawRequestDefaultExpression['headers']>(request.headers)
110
110
  expectType<RequestQuerystring>(request.query)
111
- expectType<string>(request.body.content)
111
+ if (typeof request.body === 'undefined') {
112
+ expectType<undefined>(request.body)
113
+ } else {
114
+ expectType<string>(request.body.content)
115
+ }
112
116
  expectType<string>(request.query.from)
113
117
  expectType<number>(request.params.id)
114
118
  expectType<string>(request.headers['x-foobar'])
@@ -298,7 +298,7 @@ expectError(server.withTypeProvider<TypeBoxProvider>().get(
298
298
  }
299
299
  }
300
300
  },
301
- async (_, res): Promise<RouteHandlerMethod<RawServerDefault, RawRequestDefaultExpression, RawReplyDefaultExpression, RouteGenericInterface, ContextConfigDefault, FastifySchema, TypeBoxProvider>> => {
301
+ async (_, res) => {
302
302
  return false
303
303
  }
304
304
  ))
@@ -384,11 +384,20 @@ expectError(server.withTypeProvider<JsonSchemaToTsProvider>().get(
384
384
  } as const
385
385
  }
386
386
  },
387
- async (_, res): Promise<RouteHandlerMethod<RawServerDefault, RawRequestDefaultExpression, RawReplyDefaultExpression, RouteGenericInterface, ContextConfigDefault, FastifySchema, TypeBoxProvider>> => {
387
+ async (_, res) => {
388
388
  return false
389
389
  }
390
390
  ))
391
391
 
392
+ // https://github.com/fastify/fastify/issues/4088
393
+ expectError(server.withTypeProvider<JsonSchemaToTsProvider>().get('/', {
394
+ schema: {
395
+ response: { type: 'string' }
396
+ } as const
397
+ }, (_, res) => {
398
+ return { foo: 555 }
399
+ }))
400
+
392
401
  // -------------------------------------------------------------------
393
402
  // Reply Type Override
394
403
  // -------------------------------------------------------------------
@@ -97,6 +97,38 @@ test('should be able to use setErrorHandler specify custom validation error', t
97
97
  })
98
98
  })
99
99
 
100
+ test('validation error has 400 statusCode set', t => {
101
+ t.plan(3)
102
+
103
+ const fastify = Fastify()
104
+
105
+ fastify.setErrorHandler((error, request, reply) => {
106
+ const errorResponse = {
107
+ message: error.message,
108
+ statusCode: error.statusCode || 500
109
+ }
110
+
111
+ reply.code(errorResponse.statusCode).send(errorResponse)
112
+ })
113
+
114
+ fastify.post('/', { schema }, echoBody)
115
+
116
+ fastify.inject({
117
+ method: 'POST',
118
+ payload: {
119
+ hello: 'michelangelo'
120
+ },
121
+ url: '/'
122
+ }, (err, res) => {
123
+ t.error(err)
124
+ t.same(res.json(), {
125
+ statusCode: 400,
126
+ message: "body must have required property 'name'"
127
+ })
128
+ t.equal(res.statusCode, 400)
129
+ })
130
+ })
131
+
100
132
  test('error inside custom error handler should have validationContext', t => {
101
133
  t.plan(1)
102
134
 
package/types/route.d.ts CHANGED
@@ -69,14 +69,14 @@ export type RouteHandlerMethod<
69
69
  ContextConfig = ContextConfigDefault,
70
70
  SchemaCompiler extends FastifySchema = FastifySchema,
71
71
  TypeProvider extends FastifyTypeProvider = FastifyTypeProviderDefault,
72
- ReturnType = ResolveFastifyReplyReturnType<TypeProvider, SchemaCompiler, RouteGeneric>,
73
72
  RequestType extends FastifyRequestType = ResolveFastifyRequestType<TypeProvider, SchemaCompiler, RouteGeneric>,
74
73
  Logger extends FastifyLoggerInstance = FastifyLoggerInstance
75
74
  > = (
76
75
  this: FastifyInstance<RawServer, RawRequest, RawReply, Logger, TypeProvider>,
77
76
  request: FastifyRequest<RouteGeneric, RawServer, RawRequest, SchemaCompiler, TypeProvider, ContextConfig, RequestType, Logger>,
78
77
  reply: FastifyReply<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler, TypeProvider>
79
- ) => ReturnType
78
+ // This return type used to be a generic type argument. Due to TypeScript's inference of return types, this rendered returns unchecked.
79
+ ) => ResolveFastifyReplyReturnType<TypeProvider, SchemaCompiler, RouteGeneric>
80
80
 
81
81
  /**
82
82
  * Shorthand options including the handler function property
@@ -89,11 +89,10 @@ export interface RouteShorthandOptionsWithHandler<
89
89
  ContextConfig = ContextConfigDefault,
90
90
  SchemaCompiler = FastifySchema,
91
91
  TypeProvider extends FastifyTypeProvider = FastifyTypeProviderDefault,
92
- ReturnType = ResolveFastifyReplyReturnType<TypeProvider, SchemaCompiler, RouteGeneric>,
93
92
  RequestType extends FastifyRequestType = ResolveFastifyRequestType<TypeProvider, SchemaCompiler, RouteGeneric>,
94
93
  Logger extends FastifyLoggerInstance = FastifyLoggerInstance
95
94
  > extends RouteShorthandOptions<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler, TypeProvider, RequestType, Logger> {
96
- handler: RouteHandlerMethod<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler, TypeProvider, ReturnType, RequestType, Logger>;
95
+ handler: RouteHandlerMethod<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler, TypeProvider, RequestType, Logger>;
97
96
  }
98
97
 
99
98
  /**
@@ -105,18 +104,18 @@ export interface RouteShorthandMethod<
105
104
  RawReply extends RawReplyDefaultExpression<RawServer> = RawReplyDefaultExpression<RawServer>,
106
105
  TypeProvider extends FastifyTypeProvider = FastifyTypeProviderDefault,
107
106
  > {
108
- <RouteGeneric extends RouteGenericInterface = RouteGenericInterface, ContextConfig = ContextConfigDefault, SchemaCompiler = FastifySchema, ReturnType = ResolveFastifyReplyReturnType<TypeProvider, SchemaCompiler, RouteGeneric>, RequestType extends FastifyRequestType = ResolveFastifyRequestType<TypeProvider, SchemaCompiler, RouteGeneric>, Logger extends FastifyLoggerInstance = FastifyLoggerInstance>(
107
+ <RouteGeneric extends RouteGenericInterface = RouteGenericInterface, ContextConfig = ContextConfigDefault, SchemaCompiler = FastifySchema, RequestType extends FastifyRequestType = ResolveFastifyRequestType<TypeProvider, SchemaCompiler, RouteGeneric>, Logger extends FastifyLoggerInstance = FastifyLoggerInstance>(
109
108
  path: string,
110
109
  opts: RouteShorthandOptions<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler, TypeProvider, RequestType, Logger>,
111
- handler: RouteHandlerMethod<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler, TypeProvider, ReturnType, RequestType, Logger>
110
+ handler: RouteHandlerMethod<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler, TypeProvider, RequestType, Logger>
112
111
  ): FastifyInstance<RawServer, RawRequest, RawReply, Logger, TypeProvider>;
113
- <RouteGeneric extends RouteGenericInterface = RouteGenericInterface, ContextConfig = ContextConfigDefault, SchemaCompiler = FastifySchema, ReturnType = ResolveFastifyReplyReturnType<TypeProvider, SchemaCompiler, RouteGeneric>, RequestType extends FastifyRequestType = ResolveFastifyRequestType<TypeProvider, SchemaCompiler, RouteGeneric>, Logger extends FastifyLoggerInstance = FastifyLoggerInstance>(
112
+ <RouteGeneric extends RouteGenericInterface = RouteGenericInterface, ContextConfig = ContextConfigDefault, SchemaCompiler = FastifySchema, RequestType extends FastifyRequestType = ResolveFastifyRequestType<TypeProvider, SchemaCompiler, RouteGeneric>, Logger extends FastifyLoggerInstance = FastifyLoggerInstance>(
114
113
  path: string,
115
- handler: RouteHandlerMethod<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler, TypeProvider, ReturnType, RequestType, Logger>
114
+ handler: RouteHandlerMethod<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler, TypeProvider, RequestType, Logger>
116
115
  ): FastifyInstance<RawServer, RawRequest, RawReply, Logger, TypeProvider>;
117
- <RouteGeneric extends RouteGenericInterface = RouteGenericInterface, ContextConfig = ContextConfigDefault, SchemaCompiler = FastifySchema, ReturnType = ResolveFastifyReplyReturnType<TypeProvider, SchemaCompiler, RouteGeneric>, RequestType extends FastifyRequestType = ResolveFastifyRequestType<TypeProvider, SchemaCompiler, RouteGeneric>, Logger extends FastifyLoggerInstance = FastifyLoggerInstance>(
116
+ <RouteGeneric extends RouteGenericInterface = RouteGenericInterface, ContextConfig = ContextConfigDefault, SchemaCompiler = FastifySchema, RequestType extends FastifyRequestType = ResolveFastifyRequestType<TypeProvider, SchemaCompiler, RouteGeneric>, Logger extends FastifyLoggerInstance = FastifyLoggerInstance>(
118
117
  path: string,
119
- opts: RouteShorthandOptionsWithHandler<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler, TypeProvider, ReturnType, RequestType, Logger>
118
+ opts: RouteShorthandOptionsWithHandler<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler, TypeProvider, RequestType, Logger>
120
119
  ): FastifyInstance<RawServer, RawRequest, RawReply, Logger, TypeProvider>;
121
120
  }
122
121
 
@@ -131,13 +130,12 @@ export interface RouteOptions<
131
130
  ContextConfig = ContextConfigDefault,
132
131
  SchemaCompiler = FastifySchema,
133
132
  TypeProvider extends FastifyTypeProvider = FastifyTypeProviderDefault,
134
- ReturnType = ResolveFastifyReplyReturnType<TypeProvider, SchemaCompiler, RouteGeneric>,
135
133
  RequestType extends FastifyRequestType = ResolveFastifyRequestType<TypeProvider, SchemaCompiler, RouteGeneric>,
136
134
  Logger extends FastifyLoggerInstance = FastifyLoggerInstance
137
135
  > extends RouteShorthandOptions<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler, TypeProvider, RequestType, Logger> {
138
136
  method: HTTPMethods | HTTPMethods[];
139
137
  url: string;
140
- handler: RouteHandlerMethod<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler, TypeProvider, ReturnType, RequestType, Logger>;
138
+ handler: RouteHandlerMethod<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler, TypeProvider, RequestType, Logger>;
141
139
  }
142
140
 
143
141
  export type RouteHandler<
@@ -21,17 +21,24 @@ export type CallTypeProvider<F extends FastifyTypeProvider, I> = (F & { input: I
21
21
  // -----------------------------------------------------------------------------------------------
22
22
 
23
23
  // Used to map undefined SchemaCompiler properties to unknown
24
- type UndefinedToUnknown<T> = T extends undefined ? unknown : T
24
+ // Without brackets, UndefinedToUnknown<undefined | null> => unknown
25
+ type UndefinedToUnknown<T> = [T] extends [undefined] ? unknown : T
26
+
27
+ // union-aware keyof operator
28
+ // keyof ({ a: number} | { b: number}) => never
29
+ // KeysOf<{a: number} | {b: number}> => "a" | "b"
30
+ // this exists to allow users to override faulty type-provider logic.
31
+ type KeysOf<T> = T extends any ? keyof T : never
25
32
 
26
33
  // Resolves Request types either from generic argument or Type Provider.
27
34
  type ResolveRequestParams<TypeProvider extends FastifyTypeProvider, SchemaCompiler extends FastifySchema, RouteGeneric extends RouteGenericInterface> =
28
- UndefinedToUnknown<keyof RouteGeneric['Params'] extends never ? CallTypeProvider<TypeProvider, SchemaCompiler['params']> : RouteGeneric['Params']>
35
+ UndefinedToUnknown<KeysOf<RouteGeneric['Params']> extends never ? CallTypeProvider<TypeProvider, SchemaCompiler['params']> : RouteGeneric['Params']>
29
36
  type ResolveRequestQuerystring<TypeProvider extends FastifyTypeProvider, SchemaCompiler extends FastifySchema, RouteGeneric extends RouteGenericInterface> =
30
- UndefinedToUnknown<keyof RouteGeneric['Querystring'] extends never ? CallTypeProvider<TypeProvider, SchemaCompiler['querystring']> : RouteGeneric['Querystring']>
37
+ UndefinedToUnknown<KeysOf<RouteGeneric['Querystring']> extends never ? CallTypeProvider<TypeProvider, SchemaCompiler['querystring']> : RouteGeneric['Querystring']>
31
38
  type ResolveRequestHeaders<TypeProvider extends FastifyTypeProvider, SchemaCompiler extends FastifySchema, RouteGeneric extends RouteGenericInterface> =
32
- UndefinedToUnknown<keyof RouteGeneric['Headers'] extends never ? CallTypeProvider<TypeProvider, SchemaCompiler['headers']> : RouteGeneric['Headers']>
39
+ UndefinedToUnknown<KeysOf<RouteGeneric['Headers']> extends never ? CallTypeProvider<TypeProvider, SchemaCompiler['headers']> : RouteGeneric['Headers']>
33
40
  type ResolveRequestBody<TypeProvider extends FastifyTypeProvider, SchemaCompiler extends FastifySchema, RouteGeneric extends RouteGenericInterface> =
34
- UndefinedToUnknown<keyof RouteGeneric['Body'] extends never ? CallTypeProvider<TypeProvider, SchemaCompiler['body']> : RouteGeneric['Body']>
41
+ UndefinedToUnknown<KeysOf<RouteGeneric['Body']> extends never ? CallTypeProvider<TypeProvider, SchemaCompiler['body']> : RouteGeneric['Body']>
35
42
 
36
43
  // The target request type. This type is inferenced on fastify 'requests' via generic argument assignment
37
44
  export interface FastifyRequestType<Params = unknown, Querystring = unknown, Headers = unknown, Body = unknown> {