fastify 6.0.0-alpha.2 → 6.0.0-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/SPONSORS.md +1 -0
  2. package/docs/Guides/Ecosystem.md +4 -5
  3. package/docs/Guides/Style-Guide.md +6 -5
  4. package/docs/Reference/ContentTypeParser.md +23 -1
  5. package/docs/Reference/Reply.md +3 -2
  6. package/docs/Reference/Validation-and-Serialization.md +11 -0
  7. package/docs/Reference/Warnings.md +10 -0
  8. package/fastify.js +1 -1
  9. package/lib/content-type-parser.js +7 -1
  10. package/lib/four-oh-four.js +10 -22
  11. package/lib/reply.js +5 -2
  12. package/lib/route.js +7 -4
  13. package/lib/schemas.js +5 -2
  14. package/lib/validation.js +202 -27
  15. package/lib/warnings.js +10 -1
  16. package/package.json +1 -1
  17. package/test/404s.test.js +105 -7
  18. package/test/content-type-parser-async-context.test.js +76 -0
  19. package/test/custom-parser.4.test.js +40 -0
  20. package/test/http2/plain.test.js +45 -0
  21. package/test/internals/validation.test.js +66 -0
  22. package/test/router-options.test.js +24 -0
  23. package/test/schema-feature.test.js +158 -0
  24. package/test/schema-special-usage.test.js +81 -0
  25. package/test/schema-validation.test.js +378 -0
  26. package/test/types/logger.tst.ts +6 -0
  27. package/.pi/self-learning-memory/README.md +0 -8
  28. package/.pi/self-learning-memory/core/CORE.md +0 -33
  29. package/.pi/self-learning-memory/core/index.json +0 -735
  30. package/.pi/self-learning-memory/daily/2026-07-06.md +0 -22
  31. package/.pi/self-learning-memory/daily/2026-07-29.md +0 -50
  32. package/.pi/self-learning-memory/daily/2026-08-04.md +0 -41
  33. package/.pi/self-learning-memory/daily/2026-08-18.md +0 -40
  34. package/.pi/self-learning-memory/long-term-memory.md +0 -89
  35. package/AGENTS.md +0 -290
package/SPONSORS.md CHANGED
@@ -10,6 +10,7 @@ or [GitHub Sponsors](https://github.com/sponsors/fastify)!
10
10
  ## Tier 4
11
11
 
12
12
  - [SerpApi](https://serpapi.com/?utm_source=fastify)
13
+ - [Cloudways](https://www.cloudways.com/en/velocity.php?id=1258368&data1=fastify)
13
14
 
14
15
  ## Tier 3
15
16
 
@@ -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)
@@ -280,6 +282,8 @@ section.
280
282
  plugin to authenticate HTTP requests based on API key and signature
281
283
  - [`fastify-appwrite`](https://github.com/maniecodes/fastify-appwrite) Fastify
282
284
  Plugin for interacting with Appwrite server.
285
+ - [`fastify-arktype`](https://github.com/BlairCurrey/fastify-arktype) ArkType
286
+ type provider with validation and @fastify/swagger OpenAPI generation.
283
287
  - [`fastify-asyncforge`](https://github.com/mcollina/fastify-asyncforge) Plugin
284
288
  to access Fastify instance, logger, request and reply from Node.js [Async
285
289
  Local Storage](https://nodejs.org/api/async_context.html#class-asynclocalstorage).
@@ -309,8 +313,6 @@ section.
309
313
  development servers that require Babel transformations of JavaScript sources.
310
314
  - [`fastify-bcrypt`](https://github.com/beliven-it/fastify-bcrypt) A Bcrypt hash
311
315
  generator & checker.
312
- - [`fastify-better-sqlite3`](https://github.com/punkish/fastify-better-sqlite3)
313
- Plugin for better-sqlite3.
314
316
  - [`fastify-blipp`](https://github.com/PavelPolyakov/fastify-blipp) Prints your
315
317
  routes to the console, so you definitely know which endpoints are available.
316
318
  - [`fastify-bookshelf`](https://github.com/butlerx/fastify-bookshelfjs) Fastify
@@ -698,9 +700,6 @@ middlewares into Fastify plugins
698
700
  [uws](https://github.com/uNetworking/uWebSockets).
699
701
  - [`fastify-xml-body-parser`](https://github.com/NaturalIntelligence/fastify-xml-body-parser)
700
702
  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
703
  - [`i18next-http-middleware`](https://github.com/i18next/i18next-http-middleware#fastify-usage)
705
704
  An [i18next](https://www.i18next.com) based i18n (internationalization)
706
705
  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) {
@@ -277,8 +277,9 @@ requires heavy resources to be sent after the `data`, for example,
277
277
  as soon as possible.
278
278
 
279
279
  > ℹ️ Note:
280
- > The header `Transfer-Encoding: chunked` will be added once you use
281
- > the trailer. It is a hard requirement for using trailer in Node.js.
280
+ > For HTTP/1 responses, the header `Transfer-Encoding: chunked` will be added
281
+ > once you use a trailer. HTTP/2 does not use chunked transfer encoding and
282
+ > sends trailers using its native trailing headers support.
282
283
 
283
284
  > ℹ️ Note:
284
285
  > Any error passed to `done` callback will be ignored. If you are interested
@@ -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.4'
4
4
 
5
5
  const Avvio = require('avvio')
6
6
  const http = require('node:http')
@@ -140,6 +140,7 @@ ContentTypeParser.prototype.getParser = function (contentType) {
140
140
  // with that type.
141
141
  parser = this.customParsers.get(contentType.mediaType)
142
142
  if (parser !== undefined) {
143
+ this.cache.set(ct, parser)
143
144
  return parser
144
145
  }
145
146
 
@@ -203,7 +204,12 @@ ContentTypeParser.prototype.run = function (contentType, handler, request, reply
203
204
  }
204
205
 
205
206
  const resource = new AsyncResource('content-type-parser:run', request)
206
- const done = resource.bind(onDone)
207
+
208
+ // Preserve the request's async context without the per-request function
209
+ // properties and wrappers created by AsyncResource.bind().
210
+ function done (error, body) {
211
+ resource.runInAsyncScope(onDone, undefined, error, body)
212
+ }
207
213
 
208
214
  if (parser.asString === true || parser.asBuffer === true) {
209
215
  rawBody(
@@ -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/reply.js CHANGED
@@ -606,8 +606,11 @@ function onSendEnd (reply, payload) {
606
606
  header += trailerName
607
607
  }
608
608
  if (header !== '') {
609
- // it must be chunked for trailer to work
610
- reply.header('Transfer-Encoding', 'chunked')
609
+ // HTTP/1 must use chunked encoding for trailers. HTTP/2 forbids
610
+ // the transfer-encoding header and handles trailers natively.
611
+ if (!isHttp2Reply(reply)) {
612
+ reply.header('Transfer-Encoding', 'chunked')
613
+ }
611
614
  reply.header('Trailer', header.trim())
612
615
  } else {
613
616
  reply[kReplyTrailers] = null
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)
@@ -116,19 +284,21 @@ function compileSchemasForValidation (context, compile, isCustom) {
116
284
  }
117
285
 
118
286
  function validateParam (validatorFunction, request, paramName) {
119
- const isUndefined = request[paramName] === undefined
287
+ // The headers getter may allocate a new object, so only read it when needed.
288
+ if (validatorFunction == null) return false
289
+ const value = request[paramName]
120
290
  let ret
121
291
 
122
292
  try {
123
- const data = isUndefined ? null : request[paramName]
293
+ const data = value === undefined ? null : value
124
294
  // Ajv can only mutate a root value when its parent is provided.
125
- if (validatorFunction?.schemaEnv) {
295
+ if (validatorFunction.schemaEnv) {
126
296
  ret = validatorFunction(data, {
127
297
  parentData: request,
128
298
  parentDataProperty: paramName
129
299
  })
130
300
  } else {
131
- ret = validatorFunction?.(data)
301
+ ret = validatorFunction(data)
132
302
  }
133
303
  } catch (err) {
134
304
  // If validator throws synchronously, ensure it propagates as an internal error
@@ -137,8 +307,13 @@ function validateParam (validatorFunction, request, paramName) {
137
307
  }
138
308
 
139
309
  if (ret && typeof ret.then === 'function') {
310
+ // An async validator (for example an `$async` Ajv schema) resolves with the
311
+ // validated data itself, not a `{ value, error }` result. Its properties must
312
+ // not be unwrapped the way a synchronous custom compiler result is, otherwise
313
+ // a payload carrying an `error` or `value` key could inject a validation error
314
+ // or replace the whole request part. Treat the async result as pass/fail only.
140
315
  return ret
141
- .then((res) => { return answer(res) })
316
+ .then((res) => { return res === false ? validatorFunction.errors : false })
142
317
  .catch(err => { return err }) // return as simple error (not throw)
143
318
  }
144
319
 
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.4",
4
4
  "description": "Fast and low overhead web framework, for Node.js",
5
5
  "main": "fastify.js",
6
6
  "type": "commonjs",