fastify 5.10.0 → 5.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/.pi/bash-confirm-whitelist.json +180 -0
  2. package/SPONSORS.md +1 -0
  3. package/docs/Guides/Ecosystem.md +2 -2
  4. package/docs/Guides/Getting-Started.md +8 -8
  5. package/docs/Reference/Errors.md +4 -0
  6. package/docs/Reference/Reply.md +2 -2
  7. package/docs/Reference/Request.md +11 -11
  8. package/docs/Reference/Server.md +34 -17
  9. package/docs/Reference/TypeScript.md +5 -7
  10. package/docs/Reference/Validation-and-Serialization.md +13 -2
  11. package/docs/Reference/Warnings.md +3 -2
  12. package/eslint.config.js +1 -1
  13. package/fastify.d.ts +57 -13
  14. package/fastify.js +17 -5
  15. package/lib/content-type.js +36 -18
  16. package/lib/errors.js +10 -0
  17. package/lib/handle-request.js +35 -7
  18. package/lib/hooks.js +5 -1
  19. package/lib/route.js +1 -1
  20. package/lib/schemas.js +3 -3
  21. package/lib/validation.js +1 -1
  22. package/lib/warnings.js +11 -2
  23. package/package.json +2 -2
  24. package/test/close.test.js +39 -0
  25. package/test/content-type.test.js +31 -4
  26. package/test/find-route.test.js +35 -0
  27. package/test/fix-6411.test.js +65 -0
  28. package/test/http-methods/custom-http-methods.test.js +61 -0
  29. package/test/internals/all.test.js +2 -1
  30. package/test/internals/errors.test.js +21 -1
  31. package/test/internals/reply.test.js +22 -0
  32. package/test/reply-error.test.js +35 -0
  33. package/test/rfc-10008.test.js +147 -0
  34. package/test/route-shorthand.test.js +14 -4
  35. package/test/schema-serialization.test.js +33 -0
  36. package/test/types/errors.tst.ts +2 -0
  37. package/test/types/fastify.tst.ts +11 -0
  38. package/test/types/instance.tst.ts +2 -0
  39. package/test/types/route.tst.ts +3 -3
  40. package/test/validation-error-handling.test.js +34 -0
  41. package/types/content-type-parser.d.ts +26 -6
  42. package/types/errors.d.ts +2 -0
  43. package/types/hooks.d.ts +137 -76
  44. package/types/instance.d.ts +153 -47
  45. package/types/logger.d.ts +36 -3
  46. package/types/plugin.d.ts +7 -3
  47. package/types/register.d.ts +53 -10
  48. package/types/reply.d.ts +62 -14
  49. package/types/route.d.ts +68 -34
  50. package/types/type-provider.d.ts +29 -8
  51. package/types/utils.d.ts +2 -2
package/fastify.d.ts CHANGED
@@ -69,6 +69,7 @@ declare namespace fastify {
69
69
  Logger extends FastifyBaseLogger = FastifyBaseLogger
70
70
  > = FastifyServerOptions<Server, Logger> & {
71
71
  https: https.ServerOptions | null
72
+ http2?: false
72
73
  }
73
74
 
74
75
  export type FastifyHttpOptions<
@@ -76,9 +77,12 @@ declare namespace fastify {
76
77
  Logger extends FastifyBaseLogger = FastifyBaseLogger
77
78
  > = FastifyServerOptions<Server, Logger> & {
78
79
  http?: http.ServerOptions | null
80
+ http2?: false
79
81
  }
80
82
 
81
- type FindMyWayVersion<RawServer extends RawServerBase> = RawServer extends http.Server ? HTTPVersion.V1 : HTTPVersion.V2
83
+ type FindMyWayVersion<RawServer extends RawServerBase> = RawServer extends http.Server
84
+ ? HTTPVersion.V1
85
+ : HTTPVersion.V2
82
86
  type FindMyWayConfigForServer<RawServer extends RawServerBase> = FindMyWayConfig<FindMyWayVersion<RawServer>>
83
87
 
84
88
  export interface ConnectionError extends Error {
@@ -163,15 +167,39 @@ declare namespace fastify {
163
167
  };
164
168
  return503OnClosing?: boolean,
165
169
  ajv?: Parameters<BuildCompilerFromPool>[1],
166
- frameworkErrors?: <RequestGeneric extends RequestGenericInterface = RequestGenericInterface, TypeProvider extends FastifyTypeProvider = FastifyTypeProviderDefault, SchemaCompiler extends FastifySchema = FastifySchema>(
170
+ frameworkErrors?: <
171
+ RequestGeneric extends RequestGenericInterface = RequestGenericInterface,
172
+ TypeProvider extends FastifyTypeProvider = FastifyTypeProviderDefault,
173
+ SchemaCompiler extends FastifySchema = FastifySchema
174
+ >(
167
175
  error: FastifyError,
168
- req: FastifyRequest<RequestGeneric, RawServer, RawRequestDefaultExpression<RawServer>, FastifySchema, TypeProvider>,
169
- res: FastifyReply<RequestGeneric, RawServer, RawRequestDefaultExpression<RawServer>, RawReplyDefaultExpression<RawServer>, FastifyContextConfig, SchemaCompiler, TypeProvider>
176
+ req: FastifyRequest<
177
+ RequestGeneric,
178
+ RawServer,
179
+ RawRequestDefaultExpression<RawServer>,
180
+ FastifySchema,
181
+ TypeProvider
182
+ >,
183
+ res: FastifyReply<
184
+ RequestGeneric,
185
+ RawServer,
186
+ RawRequestDefaultExpression<RawServer>,
187
+ RawReplyDefaultExpression<RawServer>,
188
+ FastifyContextConfig,
189
+ SchemaCompiler,
190
+ TypeProvider
191
+ >
170
192
  ) => void,
171
193
  rewriteUrl?: (
172
194
  // The RawRequestDefaultExpression, RawReplyDefaultExpression, and FastifyTypeProviderDefault parameters
173
195
  // should be narrowed further but those generic parameters are not passed to this FastifyServerOptions type
174
- this: FastifyInstance<RawServer, RawRequestDefaultExpression<RawServer>, RawReplyDefaultExpression<RawServer>, Logger, FastifyTypeProviderDefault>,
196
+ this: FastifyInstance<
197
+ RawServer,
198
+ RawRequestDefaultExpression<RawServer>,
199
+ RawReplyDefaultExpression<RawServer>,
200
+ Logger,
201
+ FastifyTypeProviderDefault
202
+ >,
175
203
  req: RawRequestDefaultExpression<RawServer>
176
204
  ) => string,
177
205
  schemaErrorFormatter?: SchemaErrorFormatter,
@@ -198,13 +226,25 @@ declare namespace fastify {
198
226
  FastifyListenOptions, FastifyInstance, PrintRoutesOptions, // './types/instance'
199
227
  FastifyLoggerOptions, FastifyBaseLogger, FastifyLoggerInstance, FastifyLogFn, LogLevel, // './types/logger'
200
228
  FastifyRequestContext, FastifyContextConfig, FastifyReplyContext, // './types/context'
201
- RouteHandler, RouteHandlerMethod, RouteOptions, RouteShorthandMethod, RouteShorthandOptions, RouteShorthandOptionsWithHandler, RouteGenericInterface, // './types/route'
229
+ RouteHandler, RouteHandlerMethod, RouteOptions, RouteShorthandMethod, RouteShorthandOptions,
230
+ RouteShorthandOptionsWithHandler, RouteGenericInterface, // './types/route'
202
231
  FastifyRegister, FastifyRegisterOptions, RegisterOptions, // './types/register'
203
- FastifyBodyParser, FastifyContentTypeParser, AddContentTypeParser, hasContentTypeParser, getDefaultJsonParser, ProtoAction, ConstructorAction, // './types/content-type-parser'
232
+ FastifyBodyParser, FastifyContentTypeParser, AddContentTypeParser, hasContentTypeParser, getDefaultJsonParser,
233
+ ProtoAction, ConstructorAction, // './types/content-type-parser'
204
234
  FastifyError, // '@fastify/error'
205
235
  FastifySchema, FastifySchemaValidationError, FastifySchemaCompiler, FastifySerializerCompiler, // './types/schema'
206
- HTTPMethods, RawServerBase, RawRequestDefaultExpression, RawReplyDefaultExpression, RawServerDefault, ContextConfigDefault, RequestBodyDefault, RequestQuerystringDefault, RequestParamsDefault, RequestHeadersDefault, // './types/utils'
207
- DoneFuncWithErrOrRes, HookHandlerDoneFunction, RequestPayload, onCloseAsyncHookHandler, onCloseHookHandler, onErrorAsyncHookHandler, onErrorHookHandler, onReadyAsyncHookHandler, onReadyHookHandler, onListenAsyncHookHandler, onListenHookHandler, onRegisterHookHandler, onRequestAsyncHookHandler, onRequestHookHandler, onResponseAsyncHookHandler, onResponseHookHandler, onRouteHookHandler, onSendAsyncHookHandler, onSendHookHandler, onTimeoutAsyncHookHandler, onTimeoutHookHandler, preHandlerAsyncHookHandler, preHandlerHookHandler, preParsingAsyncHookHandler, preParsingHookHandler, preSerializationAsyncHookHandler, preSerializationHookHandler, preValidationAsyncHookHandler, preValidationHookHandler, onRequestAbortHookHandler, onRequestAbortAsyncHookHandler, preCloseAsyncHookHandler, preCloseHookHandler, // './types/hooks'
236
+ HTTPMethods, RawServerBase, RawRequestDefaultExpression, RawReplyDefaultExpression, RawServerDefault,
237
+ ContextConfigDefault, RequestBodyDefault, RequestQuerystringDefault, RequestParamsDefault, RequestHeadersDefault,
238
+ // './types/utils'
239
+ DoneFuncWithErrOrRes, HookHandlerDoneFunction, RequestPayload, onCloseAsyncHookHandler, onCloseHookHandler,
240
+ onErrorAsyncHookHandler, onErrorHookHandler, onReadyAsyncHookHandler, onReadyHookHandler,
241
+ onListenAsyncHookHandler, onListenHookHandler, onRegisterHookHandler, onRequestAsyncHookHandler,
242
+ onRequestHookHandler, onResponseAsyncHookHandler, onResponseHookHandler, onRouteHookHandler,
243
+ onSendAsyncHookHandler, onSendHookHandler, onTimeoutAsyncHookHandler, onTimeoutHookHandler,
244
+ preHandlerAsyncHookHandler, preHandlerHookHandler, preParsingAsyncHookHandler, preParsingHookHandler,
245
+ preSerializationAsyncHookHandler, preSerializationHookHandler, preValidationAsyncHookHandler,
246
+ preValidationHookHandler, onRequestAbortHookHandler, onRequestAbortAsyncHookHandler, preCloseAsyncHookHandler,
247
+ preCloseHookHandler, // './types/hooks'
208
248
  FastifyServerFactory, FastifyServerFactoryHandler, // './types/serverFactory'
209
249
  FastifyTypeProvider, FastifyTypeProviderDefault, SafePromiseLike, // './types/type-provider'
210
250
  FastifyErrorCodes // './types/errors'
@@ -232,7 +272,8 @@ declare function fastify<
232
272
  Reply extends RawReplyDefaultExpression<Server> = RawReplyDefaultExpression<Server>,
233
273
  Logger extends FastifyBaseLogger = FastifyBaseLogger,
234
274
  TypeProvider extends FastifyTypeProvider = FastifyTypeProviderDefault
235
- > (opts: fastify.FastifyHttp2SecureOptions<Server, Logger>): FastifyInstance<Server, Request, Reply, Logger, TypeProvider> & SafePromiseLike<FastifyInstance<Server, Request, Reply, Logger, TypeProvider>>
275
+ > (opts: fastify.FastifyHttp2SecureOptions<Server, Logger>): FastifyInstance<Server, Request, Reply, Logger,
276
+ TypeProvider> & SafePromiseLike<FastifyInstance<Server, Request, Reply, Logger, TypeProvider>>
236
277
 
237
278
  declare function fastify<
238
279
  Server extends http2.Http2Server,
@@ -240,7 +281,8 @@ declare function fastify<
240
281
  Reply extends RawReplyDefaultExpression<Server> = RawReplyDefaultExpression<Server>,
241
282
  Logger extends FastifyBaseLogger = FastifyBaseLogger,
242
283
  TypeProvider extends FastifyTypeProvider = FastifyTypeProviderDefault
243
- > (opts: fastify.FastifyHttp2Options<Server, Logger>): FastifyInstance<Server, Request, Reply, Logger, TypeProvider> & SafePromiseLike<FastifyInstance<Server, Request, Reply, Logger, TypeProvider>>
284
+ > (opts: fastify.FastifyHttp2Options<Server, Logger>): FastifyInstance<Server, Request, Reply, Logger,
285
+ TypeProvider> & SafePromiseLike<FastifyInstance<Server, Request, Reply, Logger, TypeProvider>>
244
286
 
245
287
  declare function fastify<
246
288
  Server extends https.Server,
@@ -248,7 +290,8 @@ declare function fastify<
248
290
  Reply extends RawReplyDefaultExpression<Server> = RawReplyDefaultExpression<Server>,
249
291
  Logger extends FastifyBaseLogger = FastifyBaseLogger,
250
292
  TypeProvider extends FastifyTypeProvider = FastifyTypeProviderDefault
251
- > (opts: fastify.FastifyHttpsOptions<Server, Logger>): FastifyInstance<Server, Request, Reply, Logger, TypeProvider> & SafePromiseLike<FastifyInstance<Server, Request, Reply, Logger, TypeProvider>>
293
+ > (opts: fastify.FastifyHttpsOptions<Server, Logger>): FastifyInstance<Server, Request, Reply, Logger,
294
+ TypeProvider> & SafePromiseLike<FastifyInstance<Server, Request, Reply, Logger, TypeProvider>>
252
295
 
253
296
  declare function fastify<
254
297
  Server extends http.Server,
@@ -256,7 +299,8 @@ declare function fastify<
256
299
  Reply extends RawReplyDefaultExpression<Server> = RawReplyDefaultExpression<Server>,
257
300
  Logger extends FastifyBaseLogger = FastifyBaseLogger,
258
301
  TypeProvider extends FastifyTypeProvider = FastifyTypeProviderDefault
259
- > (opts?: fastify.FastifyHttpOptions<Server, Logger>): FastifyInstance<Server, Request, Reply, Logger, TypeProvider> & SafePromiseLike<FastifyInstance<Server, Request, Reply, Logger, TypeProvider>>
302
+ > (opts?: fastify.FastifyHttpOptions<Server, Logger>): FastifyInstance<Server, Request, Reply, Logger,
303
+ TypeProvider> & SafePromiseLike<FastifyInstance<Server, Request, Reply, Logger, TypeProvider>>
260
304
 
261
305
  // CJS export
262
306
  // const fastify = require('fastify')
package/fastify.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const VERSION = '5.10.0'
3
+ const VERSION = '5.11.0'
4
4
 
5
5
  const Avvio = require('avvio')
6
6
  const http = require('node:http')
@@ -80,7 +80,7 @@ const {
80
80
  } = errorCodes
81
81
 
82
82
  const { buildErrorHandler } = require('./lib/error-handler.js')
83
- const { FSTWRN004, FSTDEP023, FSTDEP024 } = require('./lib/warnings.js')
83
+ const { FSTWRN004, FSTDEP023, FSTDEP024, FSTDEP025 } = require('./lib/warnings.js')
84
84
 
85
85
  const initChannel = diagnostics.channel('fastify.initialization')
86
86
 
@@ -143,7 +143,9 @@ function fastify (serverOptions) {
143
143
  'OPTIONS',
144
144
  'PATCH',
145
145
  'PUT',
146
- 'POST'
146
+ 'POST',
147
+ // RFC 10008
148
+ 'QUERY'
147
149
  ])
148
150
  },
149
151
  [kOptions]: options,
@@ -200,6 +202,9 @@ function fastify (serverOptions) {
200
202
  options: function _options (url, options, handler) {
201
203
  return router.prepareRoute.call(this, { method: 'OPTIONS', url, options, handler })
202
204
  },
205
+ query: function _query (url, options, handler) {
206
+ return router.prepareRoute.call(this, { method: 'QUERY', url, options, handler })
207
+ },
203
208
  all: function _all (url, options, handler) {
204
209
  return router.prepareRoute.call(this, { method: this.supportedMethods, url, options, handler })
205
210
  },
@@ -387,7 +392,7 @@ function fastify (serverOptions) {
387
392
  if (forceCloseConnections === 'idle' && options.serverFactory) {
388
393
  instance.server.closeIdleConnections()
389
394
  /* istanbul ignore next: Cannot test this without Node.js core support */
390
- } else if (serverHasCloseAllConnections && forceCloseConnections) {
395
+ } else if (serverHasCloseAllConnections && forceCloseConnections === true) {
391
396
  instance.server.closeAllConnections()
392
397
  } else if (forceCloseConnections === true) {
393
398
  for (const conn of fastify[kKeepAliveConnections]) {
@@ -818,11 +823,18 @@ function fastify (serverOptions) {
818
823
  return this
819
824
  }
820
825
 
821
- function addHttpMethod (method, { hasBody = false } = {}) {
826
+ function addHttpMethod (method, { hasBody = false, overrideExisting = false } = {}) {
822
827
  if (typeof method !== 'string' || http.METHODS.indexOf(method) === -1) {
823
828
  throw new FST_ERR_ROUTE_METHOD_INVALID()
824
829
  }
825
830
 
831
+ const alreadyExists = this[kSupportedHTTPMethods].bodyless.has(method) ||
832
+ this[kSupportedHTTPMethods].bodywith.has(method)
833
+
834
+ if (alreadyExists && !overrideExisting) {
835
+ FSTDEP025(method)
836
+ }
837
+
826
838
  if (hasBody === true) {
827
839
  this[kSupportedHTTPMethods].bodywith.add(method)
828
840
  this[kSupportedHTTPMethods].bodyless.delete(method)
@@ -3,15 +3,37 @@
3
3
  const { LruMap: Lru } = require('toad-cache')
4
4
 
5
5
  /**
6
- * keyValuePairsReg is used to split the parameters list into associated
7
- * key value pairings. The leading `(?:^|;)\s*` anchor ensures the regex
8
- * only attempts matches at parameter boundaries, preventing quadratic
9
- * backtracking on malformed input.
6
+ * keyValuePairsReg matches a single `parameter-name "=" parameter-value`
7
+ * at a parameter boundary, following RFC 9110 §5.6.6 grammar:
8
+ *
9
+ * parameter = parameter-name "=" parameter-value
10
+ * parameter-name = token
11
+ * parameter-value = ( token / quoted-string )
12
+ * quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE
13
+ * qdtext = HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text
14
+ * quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
15
+ * obs-text = %x80-FF
16
+ *
17
+ * The value alternative accepts either a bare token or a full quoted-string
18
+ * whose content matches the qdtext / quoted-pair character ranges. Because
19
+ * the quoted-string branch is aware of `\"` as an escape, embedded `"` and
20
+ * `;` characters inside a quoted value do not terminate the value.
10
21
  *
11
22
  * @see https://httpwg.org/specs/rfc9110.html#parameter
12
23
  * @type {RegExp}
13
24
  */
14
- const keyValuePairsReg = /(?:^|;)\s*([\w!#$%&'*+.^`|~-]+)=([^;]*)/gm
25
+ const keyValuePairsReg = /(?:^|;)\s*([\w!#$%&'*+.^`|~-]+)=("(?:[\t\u0020\u0021\u0023-\u005b\u005d-\u007e\u0080-\u00ff]|\\[\t\u0020-\u00ff])*"|[\w!#$%&'*+.^`|~-]+)/gu
26
+
27
+ /**
28
+ * quotedPairReg matches a quoted-pair (a backslash followed by a single
29
+ * quoted-pair-allowed octet) inside a quoted-string value. Per RFC 9110
30
+ * §5.6.4, recipients that process the value of a quoted-string MUST handle
31
+ * a quoted-pair as if it were replaced by the octet following the backslash.
32
+ *
33
+ * @see https://httpwg.org/specs/rfc9110.html#quoted.string
34
+ * @type {RegExp}
35
+ */
36
+ const quotedPairReg = /\\([\t\u0020-\u00ff])/gu
15
37
 
16
38
  /**
17
39
  * typeNameReg is used to validate that the first part of the media-type
@@ -140,21 +162,17 @@ class ContentType {
140
162
  const key = matches[1].toLowerCase()
141
163
  // Parameter values might or might not be case-sensitive,
142
164
  // depending on the semantics of the parameter name.
143
- const value = matches[2]
144
- if (value[0] === '"') {
145
- if (value.at(-1) !== '"') {
146
- this.#parameters.set(key, 'invalid quoted string')
147
- matches = keyValuePairsReg.exec(paramsList)
148
- continue
165
+ let value = matches[2]
166
+ if (value.charCodeAt(0) === 0x22 /* " */) {
167
+ // Strip the surrounding DQUOTEs and unescape quoted-pairs per
168
+ // RFC 9110 §5.6.4. The regex guarantees the value starts and ends
169
+ // with `"` and only contains permitted qdtext / quoted-pair octets.
170
+ value = value.slice(1, -1)
171
+ if (value.indexOf('\\') !== -1) {
172
+ value = value.replace(quotedPairReg, '$1')
149
173
  }
150
- // We should probably verify the value matches a quoted string
151
- // (https://httpwg.org/specs/rfc9110.html#rule.quoted-string) value.
152
- // But we are not really doing much with the parameter values, so we
153
- // are omitting that at this time.
154
- this.#parameters.set(key, value.slice(1, value.length - 1))
155
- } else {
156
- this.#parameters.set(key, value)
157
174
  }
175
+ this.#parameters.set(key, value)
158
176
  matches = keyValuePairsReg.exec(paramsList)
159
177
  }
160
178
  }
package/lib/errors.js CHANGED
@@ -455,6 +455,16 @@ const codes = {
455
455
  500,
456
456
  TypeError
457
457
  ),
458
+ FST_ERR_ROUTE_MISSING_CONTENT_TYPE: createError(
459
+ 'FST_ERR_ROUTE_MISSING_CONTENT_TYPE',
460
+ "Method '%s' must provide a 'Content-Type' header.",
461
+ 400
462
+ ),
463
+ FST_ERR_ROUTE_MISSING_CONTENT: createError(
464
+ 'FST_ERR_ROUTE_MISSING_CONTENT',
465
+ "Method '%s' must provide a request body.",
466
+ 400
467
+ ),
458
468
 
459
469
  /**
460
470
  * again listen when close server
@@ -4,7 +4,11 @@ const diagnostics = require('node:diagnostics_channel')
4
4
  const wrapThenable = require('./wrap-thenable')
5
5
  const { validate: validateSchema } = require('./validation')
6
6
  const { preValidationHookRunner, preHandlerHookRunner } = require('./hooks')
7
- const { FST_ERR_CTP_INVALID_MEDIA_TYPE } = require('./errors')
7
+ const {
8
+ FST_ERR_CTP_INVALID_MEDIA_TYPE,
9
+ FST_ERR_ROUTE_MISSING_CONTENT_TYPE,
10
+ FST_ERR_ROUTE_MISSING_CONTENT
11
+ } = require('./errors')
8
12
  const { setErrorStatusCode } = require('./error-status')
9
13
  const {
10
14
  kReplyIsError,
@@ -37,13 +41,30 @@ function handleRequest (err, request, reply) {
37
41
  const headers = request.headers
38
42
  const ctHeader = headers['content-type']
39
43
 
40
- if (ctHeader === undefined) {
41
- const contentLength = headers['content-length']
42
- const transferEncoding = headers['transfer-encoding']
43
- const isEmptyBody = transferEncoding === undefined &&
44
- (contentLength === undefined || contentLength === '0')
44
+ if (method === 'QUERY') {
45
+ if (ctHeader === undefined) {
46
+ // https://datatracker.ietf.org/doc/html/rfc10008#section-2
47
+ // Servers MUST fail the request if the Content-Type request field
48
+ // ([HTTP], Section 8.3) is missing or is inconsistent with the
49
+ // request content.
50
+ reply[kReplyIsError] = true
51
+ reply.status(400).send(new FST_ERR_ROUTE_MISSING_CONTENT_TYPE(method))
52
+ return
53
+ }
45
54
 
46
- if (isEmptyBody) {
55
+ if (isEmptyBody(headers)) {
56
+ // https://datatracker.ietf.org/doc/html/rfc10008#section-2
57
+ // Servers MUST fail the request if the Content-Type request field
58
+ // ([HTTP], Section 8.3) is missing or is inconsistent with the
59
+ // request content.
60
+ reply[kReplyIsError] = true
61
+ reply.status(400).send(new FST_ERR_ROUTE_MISSING_CONTENT(method))
62
+ return
63
+ }
64
+ }
65
+
66
+ if (ctHeader === undefined) {
67
+ if (isEmptyBody(headers)) {
47
68
  // Request has no body to parse
48
69
  handler(request, reply)
49
70
  return
@@ -88,6 +109,13 @@ function handler (request, reply) {
88
109
  }
89
110
  }
90
111
 
112
+ function isEmptyBody (headers) {
113
+ const contentLength = headers['content-length']
114
+ const transferEncoding = headers['transfer-encoding']
115
+ return transferEncoding === undefined &&
116
+ (contentLength === undefined || contentLength === '0')
117
+ }
118
+
91
119
  function preValidationCallback (err, request, reply) {
92
120
  if (reply.sent === true) return
93
121
 
package/lib/hooks.js CHANGED
@@ -306,7 +306,11 @@ function onSendHookRunner (functions, request, reply, payload, cb) {
306
306
  }
307
307
 
308
308
  function handleResolve (newPayload) {
309
- next(null, newPayload)
309
+ try {
310
+ next(null, newPayload)
311
+ } catch (err) {
312
+ cb(err, request, reply, payload)
313
+ }
310
314
  }
311
315
 
312
316
  function handleReject (err) {
package/lib/route.js CHANGED
@@ -179,7 +179,7 @@ function buildRouting (options) {
179
179
 
180
180
  function findRoute (options) {
181
181
  const route = router.find(
182
- options.method,
182
+ options.method?.toUpperCase() ?? '',
183
183
  options.url || '',
184
184
  options.constraints
185
185
  )
package/lib/schemas.js CHANGED
@@ -149,7 +149,7 @@ function getSchemaSerializer (context, statusCode, contentType) {
149
149
  }
150
150
  if (responseSchemaDef[statusCode]) {
151
151
  if (responseSchemaDef[statusCode].constructor === Object) {
152
- const ct = new ContentType(contentType)
152
+ const ct = ContentType.from(contentType)
153
153
  if (ct.isValid) {
154
154
  if (responseSchemaDef[statusCode][ct.mediaType]) {
155
155
  return responseSchemaDef[statusCode][ct.mediaType]
@@ -168,7 +168,7 @@ function getSchemaSerializer (context, statusCode, contentType) {
168
168
  const fallbackStatusCode = (statusCode + '')[0] + 'xx'
169
169
  if (responseSchemaDef[fallbackStatusCode]) {
170
170
  if (responseSchemaDef[fallbackStatusCode].constructor === Object) {
171
- const ct = new ContentType(contentType)
171
+ const ct = ContentType.from(contentType)
172
172
  if (ct.isValid) {
173
173
  if (responseSchemaDef[fallbackStatusCode][ct.mediaType]) {
174
174
  return responseSchemaDef[fallbackStatusCode][ct.mediaType]
@@ -187,7 +187,7 @@ function getSchemaSerializer (context, statusCode, contentType) {
187
187
  }
188
188
  if (responseSchemaDef.default) {
189
189
  if (responseSchemaDef.default.constructor === Object) {
190
- const ct = new ContentType(contentType)
190
+ const ct = ContentType.from(contentType)
191
191
  if (ct.isValid) {
192
192
  if (responseSchemaDef.default[ct.mediaType]) {
193
193
  return responseSchemaDef.default[ct.mediaType]
package/lib/validation.js CHANGED
@@ -138,7 +138,7 @@ function validateParam (validatorFunction, request, paramName) {
138
138
  function answer (ret) {
139
139
  if (ret === false) return validatorFunction.errors
140
140
  if (ret && ret.error) return ret.error
141
- if (ret && ret.value) request[paramName] = ret.value
141
+ if (ret && typeof ret === 'object' && 'value' in ret) request[paramName] = ret.value
142
142
  return false
143
143
  }
144
144
  }
package/lib/warnings.js CHANGED
@@ -9,9 +9,10 @@ const { createWarning } = require('process-warning')
9
9
  * - FSTDEP022
10
10
  * - FSTDEP023
11
11
  * - FSTDEP024
12
+ * - FSTDEP025
12
13
  *
13
14
  * Deprecation Codes FSTDEP001 - FSTDEP021 were used by v4 and MUST NOT be reused.
14
- * - FSTDEP022 is used by v5 and MUST NOT be reused.
15
+ * - FSTDEP022 - FSTDEP025 are used by v5 and MUST NOT be reused.
15
16
  * Warning Codes FSTWRN001 - FSTWRN002 were used by v4 and MUST NOT be reused.
16
17
  */
17
18
 
@@ -64,6 +65,13 @@ const FSTDEP024 = createWarning({
64
65
  unlimited: true
65
66
  })
66
67
 
68
+ const FSTDEP025 = createWarning({
69
+ name: 'FastifyDeprecation',
70
+ code: 'FSTDEP025',
71
+ message: 'Calling addHttpMethod for existing method "%s" without { overrideExisting: true } is deprecated. In Fastify v6, this will throw an error.',
72
+ unlimited: true
73
+ })
74
+
67
75
  module.exports = {
68
76
  FSTWRN001,
69
77
  FSTWRN003,
@@ -71,5 +79,6 @@ module.exports = {
71
79
  FSTSEC001,
72
80
  FSTDEP022,
73
81
  FSTDEP023,
74
- FSTDEP024
82
+ FSTDEP024,
83
+ FSTDEP025
75
84
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fastify",
3
- "version": "5.10.0",
3
+ "version": "5.11.1",
4
4
  "description": "Fast and low overhead web framework, for Node.js",
5
5
  "main": "fastify.js",
6
6
  "type": "commonjs",
@@ -191,7 +191,7 @@
191
191
  "joi": "^18.0.1",
192
192
  "json-schema-to-ts": "^3.0.1",
193
193
  "JSONStream": "^1.3.5",
194
- "markdownlint-cli2": "^0.22.0",
194
+ "markdownlint-cli2": "^0.23.0",
195
195
  "neostandard": "^0.12.0",
196
196
  "node-forge": "^1.3.1",
197
197
  "proxyquire": "^2.1.3",
@@ -704,3 +704,42 @@ test('preClose execution order', (t, done) => {
704
704
  })
705
705
  })
706
706
  })
707
+
708
+ test('does not destroy connections with in-flight requests (forceCloseConnections - idle)', { skip: noSupport }, async t => {
709
+ const fastify = Fastify({ forceCloseConnections: 'idle' })
710
+
711
+ fastify.get('/', async () => {
712
+ // the server starts closing while this request is still being served
713
+ fastify.close()
714
+ await sleep(200)
715
+ return { hello: 'world' }
716
+ })
717
+
718
+ await fastify.listen({ port: 0 })
719
+
720
+ const client = new Client('http://localhost:' + fastify.server.address().port)
721
+ t.after(() => client.close())
722
+
723
+ const response = await client.request({ path: '/', method: 'GET' })
724
+ t.assert.strictEqual(response.statusCode, 200)
725
+ t.assert.deepStrictEqual(await response.body.json(), { hello: 'world' })
726
+ })
727
+
728
+ test('does not destroy connections with in-flight requests (default options)', async t => {
729
+ const fastify = Fastify()
730
+
731
+ fastify.get('/', async () => {
732
+ fastify.close()
733
+ await sleep(200)
734
+ return { hello: 'world' }
735
+ })
736
+
737
+ await fastify.listen({ port: 0 })
738
+
739
+ const client = new Client('http://localhost:' + fastify.server.address().port)
740
+ t.after(() => client.close())
741
+
742
+ const response = await client.request({ path: '/', method: 'GET' })
743
+ t.assert.strictEqual(response.statusCode, 200)
744
+ t.assert.deepStrictEqual(await response.body.json(), { hello: 'world' })
745
+ })
@@ -161,12 +161,11 @@ describe('ContentType class', () => {
161
161
  t.assert.equal(found.mediaType, 'application/json')
162
162
  t.assert.equal(found.type, 'application')
163
163
  t.assert.equal(found.subtype, 'json')
164
- t.assert.equal(found.parameters.size, 3)
164
+ t.assert.equal(found.parameters.size, 2)
165
165
 
166
166
  const expected = [
167
167
  ['charset', 'utf-8'],
168
- ['foo', 'BaR'],
169
- ['baz', 'invalid quoted string']
168
+ ['foo', 'BaR']
170
169
  ]
171
170
  t.assert.deepStrictEqual(
172
171
  Array.from(found.parameters.entries()),
@@ -175,9 +174,37 @@ describe('ContentType class', () => {
175
174
 
176
175
  t.assert.equal(
177
176
  found.toString(),
178
- 'application/json; charset="utf-8"; foo="BaR"; baz="invalid quoted string"'
177
+ 'application/json; charset="utf-8"; foo="BaR"'
179
178
  )
180
179
  })
180
+
181
+ test('preserves a semicolon inside a quoted parameter value', (t) => {
182
+ // RFC 9110 §5.6.6: ';' is a literal qdtext octet inside a quoted-string
183
+ // and does not terminate the parameter value.
184
+ const found = new ContentType('application/json; name="foo;bar"; charset=utf-8')
185
+ t.assert.equal(found.isValid, true)
186
+ t.assert.equal(found.mediaType, 'application/json')
187
+ t.assert.equal(found.parameters.get('name'), 'foo;bar')
188
+ t.assert.equal(found.parameters.get('charset'), 'utf-8')
189
+ })
190
+
191
+ test('does not leak a fake parameter out of a quoted value', (t) => {
192
+ // A `key=value;` sequence inside a quoted-string is opaque content of the
193
+ // enclosing value, not a subsequent parameter.
194
+ const found = new ContentType('application/json; name="a=b;charset=fake"; boundary=xyz')
195
+ t.assert.equal(found.isValid, true)
196
+ t.assert.equal(found.parameters.get('name'), 'a=b;charset=fake')
197
+ t.assert.equal(found.parameters.get('boundary'), 'xyz')
198
+ t.assert.equal(found.parameters.has('charset'), false)
199
+ })
200
+
201
+ test('unescapes a quoted-pair inside a quoted-string', (t) => {
202
+ // RFC 9110 §5.6.4: a quoted-pair MUST be handled as if replaced by
203
+ // the octet following the backslash.
204
+ const found = new ContentType('application/json; name="he said \\"hi\\""')
205
+ t.assert.equal(found.isValid, true)
206
+ t.assert.equal(found.parameters.get('name'), 'he said "hi"')
207
+ })
181
208
  })
182
209
 
183
210
  describe('ContentType class cache', () => {
@@ -66,6 +66,22 @@ test('findRoute should return null when when url is not passed', t => {
66
66
  }), null)
67
67
  })
68
68
 
69
+ test('findRoute should return null when method is not passed', t => {
70
+ t.plan(1)
71
+ const fastify = Fastify()
72
+
73
+ fastify.get('/artists/:artistId', {
74
+ schema: {
75
+ params: { artistId: { type: 'integer' } }
76
+ },
77
+ handler: (req, reply) => reply.send(typeof req.params.artistId)
78
+ })
79
+
80
+ t.assert.strictEqual(fastify.findRoute({
81
+ url: '/artists/:artistId'
82
+ }), null)
83
+ })
84
+
69
85
  test('findRoute should return null when route cannot be found due to a different path', t => {
70
86
  t.plan(1)
71
87
  const fastify = Fastify()
@@ -103,6 +119,25 @@ test('findRoute should return the route when found', t => {
103
119
  t.assert.strictEqual(route.params.artistId, ':artistId')
104
120
  })
105
121
 
122
+ test('findRoute should find a route even if method is not uppercased', t => {
123
+ t.plan(2)
124
+ const fastify = Fastify()
125
+
126
+ fastify.get('/artists/:artistId', {
127
+ schema: {
128
+ params: { artistId: { type: 'integer' } }
129
+ },
130
+ handler: (req, reply) => reply.send(typeof req.params.artistId)
131
+ })
132
+
133
+ const route = fastify.findRoute({
134
+ method: 'get',
135
+ url: '/artists/:artistId'
136
+ })
137
+ t.assert.notStrictEqual(route, null)
138
+ t.assert.strictEqual(route.params.artistId, ':artistId')
139
+ })
140
+
106
141
  test('findRoute should work correctly when used within plugins', (t, done) => {
107
142
  t.plan(1)
108
143
  const fastify = Fastify()