fastify 6.0.0-alpha.2 → 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.
@@ -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/contribute/) page on our website or
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 content types will throw an `FST_ERR_CTP_INVALID_MEDIA_TYPE` error.
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) {
@@ -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.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const VERSION = '6.0.0-alpha.2'
3
+ const VERSION = '6.0.0-alpha.3'
4
4
 
5
5
  const Avvio = require('avvio')
6
6
  const http = require('node:http')
@@ -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: createRouteEventHandler(),
38
- onMaxParamLength: createRouteEventHandler(),
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
- const _404Context = Object.assign({}, instance[kFourOhFourContext])
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/route.js CHANGED
@@ -410,10 +410,13 @@ function buildRouting (options) {
410
410
  context.schema = normalizeSchema(context.schema, this.initialConfig)
411
411
 
412
412
  const schemaController = this[kSchemaController]
413
- const hasValidationSchema = opts.schema.body ||
414
- opts.schema.headers ||
415
- opts.schema.querystring ||
416
- opts.schema.params
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
417
420
  if (!opts.validatorCompiler && hasValidationSchema) {
418
421
  schemaController.setupValidator(this[kOptions])
419
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
- if (routeSchemas.query) {
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 && (isCustom || Object.getPrototypeOf(headers) !== Object.prototype)) {
68
- // do not mess with schema when custom validator applied, e.g. Joi, Typebox
69
- context[headersSchema] = compile({ schema: headers, method, url, httpPart: 'headers' })
70
- } else if (headers) {
71
- // The header keys are case insensitive
72
- // https://datatracker.ietf.org/doc/html/rfc2616#section-4.2
73
- const headersSchemaLowerCase = {}
74
- Object.keys(headers).forEach(k => { headersSchemaLowerCase[k] = headers[k] })
75
- if (headersSchemaLowerCase.required instanceof Array) {
76
- headersSchemaLowerCase.required = headersSchemaLowerCase.required.map(h => h.toLowerCase())
77
- }
78
- if (headers.properties) {
79
- headersSchemaLowerCase.properties = {}
80
- Object.keys(headers.properties).forEach(k => {
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.content
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)
@@ -137,8 +305,13 @@ function validateParam (validatorFunction, request, paramName) {
137
305
  }
138
306
 
139
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.
140
313
  return ret
141
- .then((res) => { return answer(res) })
314
+ .then((res) => { return res === false ? validatorFunction.errors : false })
142
315
  .catch(err => { return err }) // return as simple error (not throw)
143
316
  }
144
317
 
package/lib/warnings.js CHANGED
@@ -6,6 +6,7 @@ const { createWarning } = require('process-warning')
6
6
  * Warning codes:
7
7
  * - FSTWRN001
8
8
  * - FSTSEC001
9
+ * - FSTSEC002
9
10
  *
10
11
  * Deprecation Codes FSTDEP001 - FSTDEP021 were used by v4 and MUST NOT be reused.
11
12
  * - FSTDEP022 - FSTDEP025 are used by v5 and MUST NOT be reused.
@@ -33,8 +34,16 @@ const FSTSEC001 = createWarning({
33
34
  unlimited: true
34
35
  })
35
36
 
37
+ const FSTSEC002 = createWarning({
38
+ name: 'FastifySecurity',
39
+ code: 'FSTSEC002',
40
+ message: 'The headers schema for %s: %s references an external $ref (%s) that is not case-normalized. Header names in the referenced schema keep their original case and will 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.',
41
+ unlimited: true
42
+ })
43
+
36
44
  module.exports = {
37
45
  FSTWRN001,
38
46
  FSTWRN003,
39
- FSTSEC001
47
+ FSTSEC001,
48
+ FSTSEC002
40
49
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fastify",
3
- "version": "6.0.0-alpha.2",
3
+ "version": "6.0.0-alpha.3",
4
4
  "description": "Fast and low overhead web framework, for Node.js",
5
5
  "main": "fastify.js",
6
6
  "type": "commonjs",