fastify 6.0.0-alpha.1 → 6.0.0-alpha.3
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.
- package/docs/Guides/Ecosystem.md +2 -5
- package/docs/Guides/Style-Guide.md +6 -5
- package/docs/Reference/ContentTypeParser.md +23 -5
- package/docs/Reference/Reply.md +0 -3
- package/docs/Reference/Request.md +0 -4
- package/docs/Reference/Server.md +8 -3
- package/docs/Reference/Validation-and-Serialization.md +11 -0
- package/docs/Reference/Warnings.md +10 -0
- package/fastify.d.ts +1 -1
- package/fastify.js +2 -2
- package/lib/four-oh-four.js +10 -22
- package/lib/request.js +3 -2
- package/lib/route.js +9 -5
- package/lib/schemas.js +5 -2
- package/lib/validation.js +206 -24
- package/lib/warnings.js +10 -1
- package/package.json +1 -1
- package/test/404s.test.js +105 -7
- package/test/close.test.js +34 -0
- package/test/fix-root-primitive-coercion.test.js +35 -0
- package/test/internals/reply.test.js +1 -1
- package/test/route-prefix.test.js +32 -0
- package/test/router-options.test.js +24 -0
- package/test/schema-feature.test.js +158 -0
- package/test/schema-special-usage.test.js +81 -0
- package/test/schema-validation.test.js +378 -0
- package/test/trust-proxy.test.js +27 -21
- package/test/types/fastify.tst.ts +2 -0
- package/test/types/logger.tst.ts +6 -0
package/docs/Guides/Ecosystem.md
CHANGED
|
@@ -226,6 +226,8 @@ section.
|
|
|
226
226
|
- [`@pompelmi/fastify-plugin`](https://github.com/pompelmi/pompelmi/tree/main/packages/fastify-plugin)
|
|
227
227
|
In-process file upload scanning for Fastify with MIME/magic-byte
|
|
228
228
|
validation, ZIP bomb protection, size limits, and optional YARA.
|
|
229
|
+
- [`@punkish/fastify-better-sqlite3`](https://github.com/punkish/fastify-better-sqlite3)
|
|
230
|
+
Plugin for better-sqlite3.
|
|
229
231
|
- [`@pybot/fastify-autoload`](https://github.com/kunal097/fastify-autoload)
|
|
230
232
|
Plugin to generate routes automatically with valid json content
|
|
231
233
|
- [`@scalar/fastify-api-reference`](https://github.com/scalar/scalar/tree/main/integrations/fastify)
|
|
@@ -309,8 +311,6 @@ section.
|
|
|
309
311
|
development servers that require Babel transformations of JavaScript sources.
|
|
310
312
|
- [`fastify-bcrypt`](https://github.com/beliven-it/fastify-bcrypt) A Bcrypt hash
|
|
311
313
|
generator & checker.
|
|
312
|
-
- [`fastify-better-sqlite3`](https://github.com/punkish/fastify-better-sqlite3)
|
|
313
|
-
Plugin for better-sqlite3.
|
|
314
314
|
- [`fastify-blipp`](https://github.com/PavelPolyakov/fastify-blipp) Prints your
|
|
315
315
|
routes to the console, so you definitely know which endpoints are available.
|
|
316
316
|
- [`fastify-bookshelf`](https://github.com/butlerx/fastify-bookshelfjs) Fastify
|
|
@@ -698,9 +698,6 @@ middlewares into Fastify plugins
|
|
|
698
698
|
[uws](https://github.com/uNetworking/uWebSockets).
|
|
699
699
|
- [`fastify-xml-body-parser`](https://github.com/NaturalIntelligence/fastify-xml-body-parser)
|
|
700
700
|
Parse XML payload / request body into JS / JSON object.
|
|
701
|
-
- [`http-wizard`](https://github.com/flodlc/http-wizard)
|
|
702
|
-
Exports a typescript API client for your Fastify API and ensures fullstack type
|
|
703
|
-
safety for your project.
|
|
704
701
|
- [`i18next-http-middleware`](https://github.com/i18next/i18next-http-middleware#fastify-usage)
|
|
705
702
|
An [i18next](https://www.i18next.com) based i18n (internationalization)
|
|
706
703
|
middleware to be used with Node.js web frameworks like Express or Fastify and
|
|
@@ -13,7 +13,8 @@ This guide is for anyone who loves to build with Fastify or wants to contribute
|
|
|
13
13
|
to our documentation. You do not need to be an expert in writing technical
|
|
14
14
|
documentation. This guide is here to help you.
|
|
15
15
|
|
|
16
|
-
Visit the [contribute](https://fastify.dev/
|
|
16
|
+
Visit the [contribute](https://fastify.dev/docs/latest/Guides/Contributing/)
|
|
17
|
+
page on our website or
|
|
17
18
|
read the
|
|
18
19
|
[CONTRIBUTING.md](https://github.com/fastify/fastify/blob/main/CONTRIBUTING.md)
|
|
19
20
|
file on GitHub to join our Open Source folks.
|
|
@@ -224,18 +225,18 @@ hyperlink should look:
|
|
|
224
225
|
<!-- More like this -->
|
|
225
226
|
|
|
226
227
|
// Add clear & brief description
|
|
227
|
-
[Fastify Plugins] (https://fastify.dev/docs/latest/Plugins/)
|
|
228
|
+
[Fastify Plugins] (https://fastify.dev/docs/latest/Reference/Plugins/)
|
|
228
229
|
|
|
229
230
|
<!--Less like this -->
|
|
230
231
|
|
|
231
232
|
// incomplete description
|
|
232
|
-
[Fastify] (https://fastify.dev/docs/latest/Plugins/)
|
|
233
|
+
[Fastify] (https://fastify.dev/docs/latest/Reference/Plugins/)
|
|
233
234
|
|
|
234
235
|
// Adding title in link brackets
|
|
235
|
-
[](https://fastify.dev/docs/latest/Plugins/ "fastify plugin")
|
|
236
|
+
[](https://fastify.dev/docs/latest/Reference/Plugins/ "fastify plugin")
|
|
236
237
|
|
|
237
238
|
// Empty title
|
|
238
|
-
[](https://fastify.dev/docs/latest/Plugins/)
|
|
239
|
+
[](https://fastify.dev/docs/latest/Reference/Plugins/)
|
|
239
240
|
|
|
240
241
|
// Adding links localhost URLs instead of using code strings (``)
|
|
241
242
|
[http://localhost:3000/](http://localhost:3000/)
|
|
@@ -5,7 +5,10 @@ Fastify natively supports `'application/json'` and `'text/plain'` content types
|
|
|
5
5
|
with a default charset of `utf-8`. These default parsers can be changed or
|
|
6
6
|
removed.
|
|
7
7
|
|
|
8
|
-
Unsupported
|
|
8
|
+
Unsupported or [syntactically invalid](https://httpwg.org/specs/rfc9110.html#media.type)
|
|
9
|
+
content types result in a `415 Unsupported Media Type` response with an
|
|
10
|
+
[`FST_ERR_CTP_INVALID_MEDIA_TYPE`](./Errors.md#fst_err_ctp_invalid_media_type)
|
|
11
|
+
error.
|
|
9
12
|
|
|
10
13
|
To support other content types, use the `addContentTypeParser` API or an
|
|
11
14
|
existing [plugin](https://fastify.dev/ecosystem/).
|
|
@@ -45,6 +48,25 @@ parsed.
|
|
|
45
48
|
> by the regex has a corresponding entry in the schema's `content` map. See
|
|
46
49
|
> [Validation and Serialization](./Validation-and-Serialization.md) for details.
|
|
47
50
|
|
|
51
|
+
> ℹ️ Note:
|
|
52
|
+
> Fastify validates the request's `Content-Type` header before selecting a body
|
|
53
|
+
> parser. String, `RegExp`, and [catch-all](#catch-all) parsers therefore will
|
|
54
|
+
> never handle an invalid header. An `onRequest` hook can rewrite a malformed
|
|
55
|
+
> header prior to validation:
|
|
56
|
+
>
|
|
57
|
+
> ```js
|
|
58
|
+
> fastify.addHook('onRequest', async function rewriteContentType (request) {
|
|
59
|
+
> if (request.headers['content-type'] === 'application/json,application/json') {
|
|
60
|
+
> request.headers['content-type'] = 'application/json'
|
|
61
|
+
> }
|
|
62
|
+
> })
|
|
63
|
+
> ```
|
|
64
|
+
>
|
|
65
|
+
> Apply only unambiguous rewrites and reject other malformed values. An invalid
|
|
66
|
+
> media type can select a different body parser or `schema.body.content`
|
|
67
|
+
> validation schema. Rewrite the header before accessing `request.mediaType`,
|
|
68
|
+
> which parses and caches the current value.
|
|
69
|
+
|
|
48
70
|
### Usage
|
|
49
71
|
```js
|
|
50
72
|
fastify.addContentTypeParser('application/jsoff', function (request, payload, done) {
|
|
@@ -167,10 +189,6 @@ fastify.addContentTypeParser('text/xml', function (request, payload, done) {
|
|
|
167
189
|
})
|
|
168
190
|
```
|
|
169
191
|
|
|
170
|
-
> ℹ️ Note:
|
|
171
|
-
> `function(req, done)` and `async function(req)` are
|
|
172
|
-
> still supported but deprecated.
|
|
173
|
-
|
|
174
192
|
#### Body Parser
|
|
175
193
|
The request body can be parsed in two ways. First, add a custom content type
|
|
176
194
|
parser and handle the request stream. Or second, use the `parseAs` option in the
|
package/docs/Reference/Reply.md
CHANGED
|
@@ -638,9 +638,6 @@ invocation of `reply.send()` once the handler promise resolve should be skipped.
|
|
|
638
638
|
By calling `reply.hijack()`, an application claims full responsibility for the
|
|
639
639
|
low-level request and response. Moreover, hooks will not be invoked.
|
|
640
640
|
|
|
641
|
-
*Modifying the `.sent` property directly is deprecated. Please use the
|
|
642
|
-
aforementioned `.hijack()` method to achieve the same effect.*
|
|
643
|
-
|
|
644
641
|
### .hijack()
|
|
645
642
|
<a name="hijack"></a>
|
|
646
643
|
|
|
@@ -72,10 +72,6 @@ Request is a core Fastify object containing the following fields:
|
|
|
72
72
|
for cooperative cancellation. On timeout, `signal.reason` is the
|
|
73
73
|
`FST_ERR_HANDLER_TIMEOUT` error; on client disconnect it is a generic
|
|
74
74
|
`AbortError`. Check `signal.reason.code` to distinguish the two cases.
|
|
75
|
-
- `context` - Deprecated, use `request.routeOptions.config` instead. A Fastify
|
|
76
|
-
internal object. Do not use or modify it directly. It is useful to access one
|
|
77
|
-
special key:
|
|
78
|
-
- `context.config` - The route [`config`](./Routes.md#routes-config) object.
|
|
79
75
|
- `routeOptions` - The route [`option`](./Routes.md#routes-options) object.
|
|
80
76
|
- `bodyLimit` - Either server limit or route limit.
|
|
81
77
|
- `handlerTimeout` - The handler timeout configured for this route.
|
package/docs/Reference/Server.md
CHANGED
|
@@ -560,11 +560,14 @@ const fastify = require('fastify')({
|
|
|
560
560
|
+ `string`: Trust only given IP/CIDR (e.g. `'127.0.0.1'`). May be a list of
|
|
561
561
|
comma separated values (e.g. `'127.0.0.1,192.168.1.1/24'`).
|
|
562
562
|
+ `Array<string>`: Trust only given IP/CIDR list (e.g. `['127.0.0.1']`).
|
|
563
|
-
+ `number`:
|
|
563
|
+
+ `number`: Hop-count-only trust is disabled because it cannot validate the
|
|
564
|
+
immediate peer and lets direct clients spoof `X-Forwarded-*` values. Use an
|
|
565
|
+
IP/CIDR string, array, or custom function that validates the proxy address
|
|
566
|
+
instead.
|
|
564
567
|
+ `Function`: Custom trust function that takes `address` as first argument
|
|
565
568
|
```js
|
|
566
569
|
function myTrustFn(address, hop) {
|
|
567
|
-
return address === '1.2.3.4'
|
|
570
|
+
return address === '1.2.3.4' && hop === 0
|
|
568
571
|
}
|
|
569
572
|
```
|
|
570
573
|
|
|
@@ -586,7 +589,9 @@ You may access the `ip`, `ips`, `host` and `protocol` values on the
|
|
|
586
589
|
> These values are derived from socket/forwarding metadata and must be treated
|
|
587
590
|
> as untrusted input unless your proxy chain is explicitly trusted and
|
|
588
591
|
> validated. Do not use them directly for authorization or other
|
|
589
|
-
> security-sensitive decisions without explicit validation.
|
|
592
|
+
> security-sensitive decisions without explicit validation. Custom trust
|
|
593
|
+
> functions must validate the `address` argument; hop-count-only checks such as
|
|
594
|
+
> `hop < 1` are unsafe when the Fastify origin can be reached directly.
|
|
590
595
|
|
|
591
596
|
```js
|
|
592
597
|
fastify.get('/', (request, reply) => {
|
|
@@ -628,6 +628,17 @@ This pattern ensures validators work correctly with both sync and async
|
|
|
628
628
|
`preValidation` hooks, preventing unhandled promise rejections that can crash
|
|
629
629
|
an application.
|
|
630
630
|
|
|
631
|
+
> [!IMPORTANT]
|
|
632
|
+
> The `{ value, error }` result convention applies to **synchronous** validator
|
|
633
|
+
> compilers only. An **asynchronous** validator (a compiler that returns a
|
|
634
|
+
> promise, including an [`$async` Ajv schema](https://ajv.js.org/guide/async-validation.html))
|
|
635
|
+
> is treated as pass/fail only: its resolved value is not unwrapped as
|
|
636
|
+
> `{ value, error }`. An async schema resolves with the validated data itself, so
|
|
637
|
+
> unwrapping it would let a payload that carries `value` or `error` keys replace
|
|
638
|
+
> the request part or inject a validation error. An async validator must signal
|
|
639
|
+
> failure by rejecting (throwing), and it cannot coerce the request part through
|
|
640
|
+
> a resolved `value`.
|
|
641
|
+
|
|
631
642
|
##### .statusCode property
|
|
632
643
|
|
|
633
644
|
All validation errors have a `.statusCode` property set to `400`, ensuring the
|
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
- [Fastify Warning Codes](#fastify-warning-codes)
|
|
8
8
|
- [FSTWRN001](#FSTWRN001)
|
|
9
9
|
- [FSTWRN003](#FSTWRN003)
|
|
10
|
+
- [Fastify Security Codes](#fastify-security-codes)
|
|
11
|
+
- [FSTSEC001](#FSTSEC001)
|
|
12
|
+
- [FSTSEC002](#FSTSEC002)
|
|
10
13
|
- [Fastify Deprecation Codes](#fastify-deprecation-codes)
|
|
11
14
|
|
|
12
15
|
## Warnings
|
|
@@ -42,6 +45,13 @@ Disabling warnings is not recommended and may cause unexpected behavior.
|
|
|
42
45
|
| <a id="FSTWRN001">FSTWRN001</a> | The specified schema for a route is missing. This may indicate the schema is not well specified. | Check the schema for the route. | [#4647](https://github.com/fastify/fastify/pull/4647) |
|
|
43
46
|
| <a id="FSTWRN003">FSTWRN003</a> | The `%s` plugin mixes async and callback styles, which may lead to unhandled rejections. | Do not mix async and callback style. | [#6011](https://github.com/fastify/fastify/pull/6011) |
|
|
44
47
|
|
|
48
|
+
### Fastify Security Codes
|
|
49
|
+
|
|
50
|
+
| Code | Description | How to solve | Discussion |
|
|
51
|
+
| ---- | ----------- | ------------ | ---------- |
|
|
52
|
+
| <a id="FSTSEC001">FSTSEC001</a> | A `RegExp` Content-Type is used that may be vulnerable to a CORS bypass, because it does not anchor the essence MIME type. | Start the `RegExp` with `^` or include `;?` so the essence MIME type is detected correctly. | [#4450](https://github.com/fastify/fastify/pull/4450) |
|
|
53
|
+
| <a id="FSTSEC002">FSTSEC002</a> | A headers schema references an external `$ref` (a schema registered with `addSchema`) that is not reached by header case normalization. Header names in the referenced schema keep their original case and do not match the lowercased request headers, so case-insensitive assertions such as `required` and `dependencies` may not apply. | Inline the header schema instead of referencing it with an external `$ref`. | - |
|
|
54
|
+
|
|
45
55
|
### Fastify Deprecation Codes
|
|
46
56
|
|
|
47
57
|
Deprecation codes are supported by the Node.js CLI options:
|
package/fastify.d.ts
CHANGED
|
@@ -144,7 +144,7 @@ declare namespace fastify {
|
|
|
144
144
|
serverFactory?: FastifyServerFactory<RawServer>,
|
|
145
145
|
requestIdHeader?: string | false,
|
|
146
146
|
genReqId?: (req: RawRequestDefaultExpression<RawServer>) => string,
|
|
147
|
-
trustProxy?: boolean | string | string[] |
|
|
147
|
+
trustProxy?: boolean | string | string[] | TrustProxyFunction,
|
|
148
148
|
schemaController?: {
|
|
149
149
|
bucket?: (parentSchemas?: unknown) => {
|
|
150
150
|
add(schema: unknown): FastifyInstance;
|
package/fastify.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const VERSION = '6.0.0-alpha.
|
|
3
|
+
const VERSION = '6.0.0-alpha.3'
|
|
4
4
|
|
|
5
5
|
const Avvio = require('avvio')
|
|
6
6
|
const http = require('node:http')
|
|
@@ -600,7 +600,7 @@ function fastify (serverOptions) {
|
|
|
600
600
|
|
|
601
601
|
if (name === 'onClose') {
|
|
602
602
|
this.onClose(fn.bind(this))
|
|
603
|
-
} else if (name === 'onReady' || name === 'onListen' || name === 'onRoute') {
|
|
603
|
+
} else if (name === 'onReady' || name === 'onListen' || name === 'onRoute' || name === 'preClose') {
|
|
604
604
|
this[kHooks].add(name, fn)
|
|
605
605
|
} else {
|
|
606
606
|
this.after((err, done) => {
|
package/lib/four-oh-four.js
CHANGED
|
@@ -34,11 +34,10 @@ function fourOhFour (options) {
|
|
|
34
34
|
|
|
35
35
|
// 404 router, used for handling encapsulated 404 handlers
|
|
36
36
|
const router = FindMyWay({
|
|
37
|
-
onBadUrl:
|
|
38
|
-
onMaxParamLength:
|
|
37
|
+
onBadUrl: options.routerOptions.onBadUrl,
|
|
38
|
+
onMaxParamLength: options.routerOptions.onMaxParamLength,
|
|
39
39
|
defaultRoute: fourOhFourFallBack
|
|
40
40
|
})
|
|
41
|
-
let _routeEventHandler = null
|
|
42
41
|
|
|
43
42
|
return { router, setNotFoundHandler, setContext, arrange404 }
|
|
44
43
|
|
|
@@ -47,8 +46,6 @@ function fourOhFour (options) {
|
|
|
47
46
|
instance[kFourOhFourLevelInstance] = instance
|
|
48
47
|
instance[kCanSetNotFoundHandler] = true
|
|
49
48
|
// we need to bind instance for the context
|
|
50
|
-
router.onBadUrl = router.onBadUrl.bind(instance)
|
|
51
|
-
router.onMaxParamLength = router.onMaxParamLength.bind(instance)
|
|
52
49
|
router.defaultRoute = router.defaultRoute.bind(instance)
|
|
53
50
|
}
|
|
54
51
|
|
|
@@ -62,20 +59,15 @@ function fourOhFour (options) {
|
|
|
62
59
|
})
|
|
63
60
|
}
|
|
64
61
|
|
|
65
|
-
function createRouteEventHandler () {
|
|
66
|
-
return function onRouteEvent (path, req, res) {
|
|
67
|
-
const fourOhFourContext = this[kFourOhFourLevelInstance][kFourOhFourContext]
|
|
68
|
-
const id = getGenReqId(fourOhFourContext.server, req)
|
|
69
|
-
const childLogger = createChildLogger(fourOhFourContext, logger, req, id)
|
|
70
|
-
const request = new Request(id, null, req, null, childLogger, fourOhFourContext)
|
|
71
|
-
const reply = new Reply(res, request, childLogger)
|
|
72
|
-
|
|
73
|
-
_routeEventHandler(request, reply)
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
62
|
function setContext (instance, context) {
|
|
78
|
-
|
|
63
|
+
// Use the selected not-found context as the prototype rather than taking a
|
|
64
|
+
// detached shallow snapshot. The selected context's lifecycle hooks are
|
|
65
|
+
// populated during preReady (potentially after this snapshot is taken when
|
|
66
|
+
// the route is registered first), and prototype lookup keeps the
|
|
67
|
+
// route-specific 404 context linked to the live one so hooks like
|
|
68
|
+
// preHandler are not lost. The route's own onSend is preserved as an own
|
|
69
|
+
// property.
|
|
70
|
+
const _404Context = Object.create(instance[kFourOhFourContext])
|
|
79
71
|
_404Context.onSend = context.onSend
|
|
80
72
|
context[kFourOhFourContext] = _404Context
|
|
81
73
|
}
|
|
@@ -123,12 +115,8 @@ function fourOhFour (options) {
|
|
|
123
115
|
if (handler) {
|
|
124
116
|
this[kFourOhFourLevelInstance][kCanSetNotFoundHandler] = false
|
|
125
117
|
handler = handler.bind(this)
|
|
126
|
-
// update route event handler
|
|
127
|
-
_routeEventHandler = handler
|
|
128
118
|
} else {
|
|
129
119
|
handler = basic404
|
|
130
|
-
// update route event handler
|
|
131
|
-
_routeEventHandler = basic404
|
|
132
120
|
}
|
|
133
121
|
|
|
134
122
|
this.after((notHandledErr, done) => {
|
package/lib/request.js
CHANGED
|
@@ -49,8 +49,9 @@ function getTrustProxyFn (tp) {
|
|
|
49
49
|
return function () { return true }
|
|
50
50
|
}
|
|
51
51
|
if (typeof tp === 'number') {
|
|
52
|
-
//
|
|
53
|
-
|
|
52
|
+
// Hop-count-only trust cannot validate the immediate peer. Fail closed so
|
|
53
|
+
// direct clients cannot spoof X-Forwarded-* values by supplying enough hops.
|
|
54
|
+
return function () { return false }
|
|
54
55
|
}
|
|
55
56
|
if (typeof tp === 'string') {
|
|
56
57
|
// Support comma-separated tps
|
package/lib/route.js
CHANGED
|
@@ -289,6 +289,7 @@ function buildRouting (options) {
|
|
|
289
289
|
opts.attachValidation = false
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
+
const routeConfigUrl = prefixing ? prefix : url
|
|
292
293
|
if (prefixing === false) {
|
|
293
294
|
// run 'onRoute' hooks
|
|
294
295
|
for (const hook of this[kHooks].onRoute) {
|
|
@@ -327,7 +328,7 @@ function buildRouting (options) {
|
|
|
327
328
|
const constraints = opts.constraints || {}
|
|
328
329
|
const config = {
|
|
329
330
|
...opts.config,
|
|
330
|
-
url,
|
|
331
|
+
url: routeConfigUrl,
|
|
331
332
|
method: opts.method
|
|
332
333
|
}
|
|
333
334
|
|
|
@@ -409,10 +410,13 @@ function buildRouting (options) {
|
|
|
409
410
|
context.schema = normalizeSchema(context.schema, this.initialConfig)
|
|
410
411
|
|
|
411
412
|
const schemaController = this[kSchemaController]
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
413
|
+
// A JSON Schema boolean (`false` or `true`) is a valid schema and
|
|
414
|
+
// must still trigger the validator compiler setup, so presence is
|
|
415
|
+
// determined from an explicit `undefined` check rather than truthiness.
|
|
416
|
+
const hasValidationSchema = opts.schema.body !== undefined ||
|
|
417
|
+
opts.schema.headers !== undefined ||
|
|
418
|
+
opts.schema.querystring !== undefined ||
|
|
419
|
+
opts.schema.params !== undefined
|
|
416
420
|
if (!opts.validatorCompiler && hasValidationSchema) {
|
|
417
421
|
schemaController.setupValidator(this[kOptions])
|
|
418
422
|
}
|
package/lib/schemas.js
CHANGED
|
@@ -61,9 +61,12 @@ function normalizeSchema (routeSchemas, serverOptions) {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
// alias query to querystring schema
|
|
64
|
-
|
|
64
|
+
// A JSON Schema boolean (`false` or `true`) is a valid schema and must still
|
|
65
|
+
// be aliased, so presence is determined from an explicit `undefined` check
|
|
66
|
+
// rather than truthiness.
|
|
67
|
+
if (routeSchemas.query !== undefined) {
|
|
65
68
|
// check if our schema has both querystring and query
|
|
66
|
-
if (routeSchemas.querystring) {
|
|
69
|
+
if (routeSchemas.querystring !== undefined) {
|
|
67
70
|
throw new FST_ERR_SCH_DUPLICATE('querystring')
|
|
68
71
|
}
|
|
69
72
|
routeSchemas.querystring = routeSchemas.query
|
package/lib/validation.js
CHANGED
|
@@ -13,7 +13,7 @@ const {
|
|
|
13
13
|
FST_ERR_SCH_RESPONSE_SCHEMA_NOT_NESTED_2XX
|
|
14
14
|
} = require('./errors')
|
|
15
15
|
|
|
16
|
-
const { FSTWRN001 } = require('./warnings')
|
|
16
|
+
const { FSTWRN001, FSTSEC002 } = require('./warnings')
|
|
17
17
|
|
|
18
18
|
function compileSchemasForSerialization (context, compile) {
|
|
19
19
|
if (!context.schema || !context.schema.response) {
|
|
@@ -54,6 +54,169 @@ function compileSchemasForSerialization (context, compile) {
|
|
|
54
54
|
}, {})
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
// Header names are case insensitive (RFC 9110 §5.1), and Node.js stores every
|
|
58
|
+
// received header name in lowercase in `request.headers` (see the Node.js
|
|
59
|
+
// `message.headers` docs). The header schema must be normalized the same way
|
|
60
|
+
// before compilation, otherwise schema keywords that reference header names by
|
|
61
|
+
// their canonical or mixed case — `properties`, `required`, and the Draft 7
|
|
62
|
+
// `dependencies` keyword (triggers and array-form dependent names) — never
|
|
63
|
+
// match the lowercased request object, silently skipping assertions.
|
|
64
|
+
//
|
|
65
|
+
// JSON Schema is recursive, so the transformation walks the whole schema and
|
|
66
|
+
// lowercases every position where a property name (i.e. a header name) appears,
|
|
67
|
+
// while recursing into subschemas. This covers `properties` and `required` in
|
|
68
|
+
// every nested subschema, `dependencies` trigger keys, array-form dependent
|
|
69
|
+
// property names and subschemas used as dependency values, and subschemas
|
|
70
|
+
// reachable through `allOf`, `anyOf`, `oneOf`, `not`, `if`/`then`/`else`, local
|
|
71
|
+
// `$ref` definitions (`definitions`/`$defs`), and the other standard subschema
|
|
72
|
+
// containers. The input schema is never mutated: a new schema tree is returned.
|
|
73
|
+
function lowerCaseHeadersSchema (schema) {
|
|
74
|
+
if (Array.isArray(schema)) {
|
|
75
|
+
return schema.map(lowerCaseHeadersSchema)
|
|
76
|
+
}
|
|
77
|
+
if (schema === null || typeof schema !== 'object') {
|
|
78
|
+
return schema
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const result = {}
|
|
82
|
+
|
|
83
|
+
for (const key of Object.keys(schema)) {
|
|
84
|
+
const value = schema[key]
|
|
85
|
+
|
|
86
|
+
switch (key) {
|
|
87
|
+
case 'properties': {
|
|
88
|
+
if (value === null || typeof value !== 'object') {
|
|
89
|
+
result.properties = value
|
|
90
|
+
break
|
|
91
|
+
}
|
|
92
|
+
const normalized = {}
|
|
93
|
+
for (const prop of Object.keys(value)) {
|
|
94
|
+
normalized[prop.toLowerCase()] = lowerCaseHeadersSchema(value[prop])
|
|
95
|
+
}
|
|
96
|
+
result.properties = normalized
|
|
97
|
+
break
|
|
98
|
+
}
|
|
99
|
+
case 'required':
|
|
100
|
+
result.required = Array.isArray(value)
|
|
101
|
+
? value.map(name => name.toLowerCase())
|
|
102
|
+
: value
|
|
103
|
+
break
|
|
104
|
+
case 'dependencies': {
|
|
105
|
+
if (value === null || typeof value !== 'object') {
|
|
106
|
+
result.dependencies = value
|
|
107
|
+
break
|
|
108
|
+
}
|
|
109
|
+
const normalized = {}
|
|
110
|
+
for (const dep of Object.keys(value)) {
|
|
111
|
+
const depValue = value[dep]
|
|
112
|
+
if (Array.isArray(depValue)) {
|
|
113
|
+
normalized[dep.toLowerCase()] = depValue.map(name => name.toLowerCase())
|
|
114
|
+
} else {
|
|
115
|
+
normalized[dep.toLowerCase()] = lowerCaseHeadersSchema(depValue)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
result.dependencies = normalized
|
|
119
|
+
break
|
|
120
|
+
}
|
|
121
|
+
case 'dependentSchemas': {
|
|
122
|
+
if (value === null || typeof value !== 'object') {
|
|
123
|
+
result.dependentSchemas = value
|
|
124
|
+
break
|
|
125
|
+
}
|
|
126
|
+
const normalized = {}
|
|
127
|
+
for (const dep of Object.keys(value)) {
|
|
128
|
+
normalized[dep.toLowerCase()] = lowerCaseHeadersSchema(value[dep])
|
|
129
|
+
}
|
|
130
|
+
result.dependentSchemas = normalized
|
|
131
|
+
break
|
|
132
|
+
}
|
|
133
|
+
case 'dependentRequired': {
|
|
134
|
+
if (value === null || typeof value !== 'object') {
|
|
135
|
+
result.dependentRequired = value
|
|
136
|
+
break
|
|
137
|
+
}
|
|
138
|
+
const normalized = {}
|
|
139
|
+
for (const dep of Object.keys(value)) {
|
|
140
|
+
normalized[dep.toLowerCase()] = value[dep].map(name => name.toLowerCase())
|
|
141
|
+
}
|
|
142
|
+
result.dependentRequired = normalized
|
|
143
|
+
break
|
|
144
|
+
}
|
|
145
|
+
// Arrays of subschemas (`allOf`/`anyOf`/`oneOf`) or a single subschema
|
|
146
|
+
// (`not`/`if`/`then`/`else`/`items`/`additionalItems`/...). `lowerCaseHeadersSchema`
|
|
147
|
+
// handles arrays, objects and booleans, so recursing here covers them all.
|
|
148
|
+
case 'allOf':
|
|
149
|
+
case 'anyOf':
|
|
150
|
+
case 'oneOf':
|
|
151
|
+
case 'not':
|
|
152
|
+
case 'if':
|
|
153
|
+
case 'then':
|
|
154
|
+
case 'else':
|
|
155
|
+
case 'items':
|
|
156
|
+
case 'additionalItems':
|
|
157
|
+
case 'additionalProperties':
|
|
158
|
+
case 'unevaluatedItems':
|
|
159
|
+
case 'unevaluatedProperties':
|
|
160
|
+
case 'contains':
|
|
161
|
+
case 'propertyNames':
|
|
162
|
+
case 'contentSchema':
|
|
163
|
+
result[key] = lowerCaseHeadersSchema(value)
|
|
164
|
+
break
|
|
165
|
+
// Maps of subschemas whose keys are not header names (definition names,
|
|
166
|
+
// regex patterns): keep the keys, normalize the subschema values.
|
|
167
|
+
case 'definitions':
|
|
168
|
+
case '$defs':
|
|
169
|
+
case 'patternProperties': {
|
|
170
|
+
if (value === null || typeof value !== 'object') {
|
|
171
|
+
result[key] = value
|
|
172
|
+
break
|
|
173
|
+
}
|
|
174
|
+
const normalized = {}
|
|
175
|
+
for (const k of Object.keys(value)) {
|
|
176
|
+
normalized[k] = lowerCaseHeadersSchema(value[k])
|
|
177
|
+
}
|
|
178
|
+
result[key] = normalized
|
|
179
|
+
break
|
|
180
|
+
}
|
|
181
|
+
default:
|
|
182
|
+
result[key] = value
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return result
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// `lowerCaseHeadersSchema` only normalizes the inline schema tree. A header
|
|
190
|
+
// schema can also pull in a schema registered with `addSchema()` through an
|
|
191
|
+
// external `$ref` (any ref that does not start with `#`, i.e. not a same
|
|
192
|
+
// document fragment). Those referenced schemas are resolved by the validator
|
|
193
|
+
// after normalization and keep their original header-name case, so
|
|
194
|
+
// case-insensitive assertions in them never match the lowercased request
|
|
195
|
+
// headers. We cannot safely normalize them here because the same registered
|
|
196
|
+
// schema is shared with body, querystring and params validation, so we detect
|
|
197
|
+
// the external reference and warn the developer instead. Returns the first
|
|
198
|
+
// external `$ref` found, or undefined.
|
|
199
|
+
function findExternalHeaderRef (schema) {
|
|
200
|
+
if (Array.isArray(schema)) {
|
|
201
|
+
for (const item of schema) {
|
|
202
|
+
const ref = findExternalHeaderRef(item)
|
|
203
|
+
if (ref !== undefined) return ref
|
|
204
|
+
}
|
|
205
|
+
return undefined
|
|
206
|
+
}
|
|
207
|
+
if (schema === null || typeof schema !== 'object') {
|
|
208
|
+
return undefined
|
|
209
|
+
}
|
|
210
|
+
if (typeof schema.$ref === 'string' && schema.$ref[0] !== '#') {
|
|
211
|
+
return schema.$ref
|
|
212
|
+
}
|
|
213
|
+
for (const key of Object.keys(schema)) {
|
|
214
|
+
const ref = findExternalHeaderRef(schema[key])
|
|
215
|
+
if (ref !== undefined) return ref
|
|
216
|
+
}
|
|
217
|
+
return undefined
|
|
218
|
+
}
|
|
219
|
+
|
|
57
220
|
function compileSchemasForValidation (context, compile, isCustom) {
|
|
58
221
|
const { schema } = context
|
|
59
222
|
if (!schema) {
|
|
@@ -62,32 +225,37 @@ function compileSchemasForValidation (context, compile, isCustom) {
|
|
|
62
225
|
|
|
63
226
|
const { method, url } = context.config || {}
|
|
64
227
|
|
|
228
|
+
// JSON Schema Draft 7 defines the boolean `false` (and `true`) as a valid
|
|
229
|
+
// schema. A present boolean must be passed to the compiler unchanged, so
|
|
230
|
+
// every request-part schema is selected by an explicit `undefined` check
|
|
231
|
+
// rather than truthiness. FSTWRN001 is preserved only for schemas that are
|
|
232
|
+
// explicitly set to `undefined`.
|
|
233
|
+
|
|
65
234
|
const headers = schema.headers
|
|
66
235
|
// the or part is used for backward compatibility
|
|
67
|
-
if (headers
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
headersSchemaLowerCase.properties[k.toLowerCase()] = headers.properties[k]
|
|
82
|
-
})
|
|
236
|
+
if (headers !== undefined) {
|
|
237
|
+
if (isCustom || typeof headers !== 'object' || headers === null || Object.getPrototypeOf(headers) !== Object.prototype) {
|
|
238
|
+
// do not mess with schema when custom validator applied, e.g. Joi, Typebox
|
|
239
|
+
// boolean schemas (true/false) and invalid values are compiled directly
|
|
240
|
+
context[headersSchema] = compile({ schema: headers, method, url, httpPart: 'headers' })
|
|
241
|
+
} else {
|
|
242
|
+
// The header keys are case insensitive
|
|
243
|
+
// https://datatracker.ietf.org/doc/html/rfc2616#section-4.2
|
|
244
|
+
const headersSchemaLowerCase = lowerCaseHeadersSchema(headers)
|
|
245
|
+
const externalRef = findExternalHeaderRef(headers)
|
|
246
|
+
if (externalRef !== undefined) {
|
|
247
|
+
FSTSEC002(method, url, externalRef)
|
|
248
|
+
}
|
|
249
|
+
context[headersSchema] = compile({ schema: headersSchemaLowerCase, method, url, httpPart: 'headers' })
|
|
83
250
|
}
|
|
84
|
-
context[headersSchema] = compile({ schema: headersSchemaLowerCase, method, url, httpPart: 'headers' })
|
|
85
251
|
} else if (Object.hasOwn(schema, 'headers')) {
|
|
86
252
|
FSTWRN001('headers', method, url)
|
|
87
253
|
}
|
|
88
254
|
|
|
89
|
-
if (schema.body) {
|
|
90
|
-
const contentProperty = schema.body.
|
|
255
|
+
if (schema.body !== undefined) {
|
|
256
|
+
const contentProperty = schema.body !== null && typeof schema.body === 'object'
|
|
257
|
+
? schema.body.content
|
|
258
|
+
: undefined
|
|
91
259
|
if (contentProperty) {
|
|
92
260
|
const contentTypeSchemas = {}
|
|
93
261
|
for (const contentType of Object.keys(contentProperty)) {
|
|
@@ -102,13 +270,13 @@ function compileSchemasForValidation (context, compile, isCustom) {
|
|
|
102
270
|
FSTWRN001('body', method, url)
|
|
103
271
|
}
|
|
104
272
|
|
|
105
|
-
if (schema.querystring) {
|
|
273
|
+
if (schema.querystring !== undefined) {
|
|
106
274
|
context[querystringSchema] = compile({ schema: schema.querystring, method, url, httpPart: 'querystring' })
|
|
107
275
|
} else if (Object.hasOwn(schema, 'querystring')) {
|
|
108
276
|
FSTWRN001('querystring', method, url)
|
|
109
277
|
}
|
|
110
278
|
|
|
111
|
-
if (schema.params) {
|
|
279
|
+
if (schema.params !== undefined) {
|
|
112
280
|
context[paramsSchema] = compile({ schema: schema.params, method, url, httpPart: 'params' })
|
|
113
281
|
} else if (Object.hasOwn(schema, 'params')) {
|
|
114
282
|
FSTWRN001('params', method, url)
|
|
@@ -120,7 +288,16 @@ function validateParam (validatorFunction, request, paramName) {
|
|
|
120
288
|
let ret
|
|
121
289
|
|
|
122
290
|
try {
|
|
123
|
-
|
|
291
|
+
const data = isUndefined ? null : request[paramName]
|
|
292
|
+
// Ajv can only mutate a root value when its parent is provided.
|
|
293
|
+
if (validatorFunction?.schemaEnv) {
|
|
294
|
+
ret = validatorFunction(data, {
|
|
295
|
+
parentData: request,
|
|
296
|
+
parentDataProperty: paramName
|
|
297
|
+
})
|
|
298
|
+
} else {
|
|
299
|
+
ret = validatorFunction?.(data)
|
|
300
|
+
}
|
|
124
301
|
} catch (err) {
|
|
125
302
|
// If validator throws synchronously, ensure it propagates as an internal error
|
|
126
303
|
err.statusCode = 500
|
|
@@ -128,8 +305,13 @@ function validateParam (validatorFunction, request, paramName) {
|
|
|
128
305
|
}
|
|
129
306
|
|
|
130
307
|
if (ret && typeof ret.then === 'function') {
|
|
308
|
+
// An async validator (for example an `$async` Ajv schema) resolves with the
|
|
309
|
+
// validated data itself, not a `{ value, error }` result. Its properties must
|
|
310
|
+
// not be unwrapped the way a synchronous custom compiler result is, otherwise
|
|
311
|
+
// a payload carrying an `error` or `value` key could inject a validation error
|
|
312
|
+
// or replace the whole request part. Treat the async result as pass/fail only.
|
|
131
313
|
return ret
|
|
132
|
-
.then((res) => { return
|
|
314
|
+
.then((res) => { return res === false ? validatorFunction.errors : false })
|
|
133
315
|
.catch(err => { return err }) // return as simple error (not throw)
|
|
134
316
|
}
|
|
135
317
|
|