fastify 5.4.0 → 5.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/LICENSE +1 -1
  2. package/SECURITY.md +158 -2
  3. package/build/build-validation.js +19 -1
  4. package/docs/Guides/Delay-Accepting-Requests.md +8 -5
  5. package/docs/Guides/Ecosystem.md +11 -0
  6. package/docs/Guides/Migration-Guide-V5.md +6 -10
  7. package/docs/Guides/Recommendations.md +1 -1
  8. package/docs/Reference/Errors.md +3 -1
  9. package/docs/Reference/Hooks.md +2 -6
  10. package/docs/Reference/Lifecycle.md +2 -2
  11. package/docs/Reference/Request.md +1 -1
  12. package/docs/Reference/Routes.md +5 -5
  13. package/docs/Reference/Server.md +306 -179
  14. package/docs/Reference/TypeScript.md +3 -5
  15. package/docs/Reference/Validation-and-Serialization.md +55 -3
  16. package/docs/Reference/Warnings.md +2 -1
  17. package/fastify.d.ts +19 -2
  18. package/fastify.js +34 -33
  19. package/lib/configValidator.js +196 -28
  20. package/lib/contentTypeParser.js +41 -48
  21. package/lib/error-handler.js +3 -3
  22. package/lib/errors.js +5 -0
  23. package/lib/handleRequest.js +13 -17
  24. package/lib/promise.js +23 -0
  25. package/lib/reply.js +17 -19
  26. package/lib/route.js +37 -3
  27. package/lib/server.js +36 -35
  28. package/lib/warnings.js +11 -1
  29. package/package.json +7 -7
  30. package/test/async-await.test.js +81 -134
  31. package/test/async_hooks.test.js +18 -37
  32. package/test/body-limit.test.js +51 -0
  33. package/test/buffer.test.js +22 -0
  34. package/test/case-insensitive.test.js +44 -65
  35. package/test/check.test.js +17 -21
  36. package/test/close-pipelining.test.js +24 -15
  37. package/test/constrained-routes.test.js +231 -0
  38. package/test/custom-http-server.test.js +7 -15
  39. package/test/custom-parser.0.test.js +267 -348
  40. package/test/custom-parser.1.test.js +141 -191
  41. package/test/custom-parser.2.test.js +34 -44
  42. package/test/custom-parser.3.test.js +56 -104
  43. package/test/custom-parser.4.test.js +106 -144
  44. package/test/custom-parser.5.test.js +56 -75
  45. package/test/custom-querystring-parser.test.js +51 -77
  46. package/test/decorator.test.js +76 -259
  47. package/test/delete.test.js +101 -110
  48. package/test/diagnostics-channel/404.test.js +7 -15
  49. package/test/diagnostics-channel/async-request.test.js +8 -16
  50. package/test/diagnostics-channel/error-request.test.js +7 -15
  51. package/test/diagnostics-channel/sync-request-reply.test.js +9 -16
  52. package/test/diagnostics-channel/sync-request.test.js +9 -16
  53. package/test/fastify-instance.test.js +1 -1
  54. package/test/header-overflow.test.js +18 -29
  55. package/test/helper.js +138 -134
  56. package/test/hooks-async.test.js +26 -32
  57. package/test/hooks.test.js +261 -447
  58. package/test/http-methods/copy.test.js +14 -19
  59. package/test/http-methods/get.test.js +131 -143
  60. package/test/http-methods/head.test.js +53 -84
  61. package/test/http-methods/mkcalendar.test.js +45 -72
  62. package/test/http-methods/move.test.js +6 -10
  63. package/test/http-methods/propfind.test.js +34 -44
  64. package/test/http-methods/unlock.test.js +5 -9
  65. package/test/http2/secure-with-fallback.test.js +3 -1
  66. package/test/https/custom-https-server.test.js +9 -13
  67. package/test/input-validation.js +139 -150
  68. package/test/internals/errors.test.js +50 -1
  69. package/test/internals/handle-request.test.js +29 -5
  70. package/test/internals/promise.test.js +63 -0
  71. package/test/internals/reply.test.js +277 -496
  72. package/test/plugin.1.test.js +40 -68
  73. package/test/plugin.2.test.js +40 -70
  74. package/test/plugin.3.test.js +25 -68
  75. package/test/promises.test.js +42 -63
  76. package/test/register.test.js +8 -18
  77. package/test/request-error.test.js +57 -100
  78. package/test/request-id.test.js +30 -49
  79. package/test/route-hooks.test.js +12 -16
  80. package/test/route-shorthand.test.js +9 -27
  81. package/test/route.1.test.js +74 -131
  82. package/test/route.8.test.js +9 -17
  83. package/test/router-options.test.js +450 -0
  84. package/test/schema-validation.test.js +30 -31
  85. package/test/server.test.js +143 -5
  86. package/test/stream.1.test.js +33 -50
  87. package/test/stream.4.test.js +18 -28
  88. package/test/stream.5.test.js +11 -19
  89. package/test/types/errors.test-d.ts +13 -1
  90. package/test/types/instance.test-d.ts +18 -1
  91. package/test/types/type-provider.test-d.ts +55 -0
  92. package/test/use-semicolon-delimiter.test.js +117 -59
  93. package/test/versioned-routes.test.js +39 -56
  94. package/types/errors.d.ts +11 -1
  95. package/types/hooks.d.ts +1 -1
  96. package/types/instance.d.ts +3 -1
  97. package/types/logger.d.ts +16 -14
  98. package/types/reply.d.ts +2 -2
@@ -24,7 +24,8 @@ const {
24
24
  FST_ERR_CTP_INVALID_MEDIA_TYPE,
25
25
  FST_ERR_CTP_INVALID_CONTENT_LENGTH,
26
26
  FST_ERR_CTP_EMPTY_JSON_BODY,
27
- FST_ERR_CTP_INSTANCE_ALREADY_STARTED
27
+ FST_ERR_CTP_INSTANCE_ALREADY_STARTED,
28
+ FST_ERR_CTP_INVALID_JSON_BODY
28
29
  } = require('./errors')
29
30
  const { FSTSEC001 } = require('./warnings')
30
31
 
@@ -174,17 +175,18 @@ ContentTypeParser.prototype.run = function (contentType, handler, request, reply
174
175
  const parser = this.getParser(contentType)
175
176
 
176
177
  if (parser === undefined) {
177
- if (request.is404) {
178
+ if (request.is404 === true) {
178
179
  handler(request, reply)
179
- } else {
180
- reply.send(new FST_ERR_CTP_INVALID_MEDIA_TYPE(contentType || undefined))
180
+ return
181
181
  }
182
182
 
183
- // Early return to avoid allocating an AsyncResource if it's not needed
183
+ reply[kReplyIsError] = true
184
+ reply.send(new FST_ERR_CTP_INVALID_MEDIA_TYPE(contentType || undefined))
184
185
  return
185
186
  }
186
187
 
187
188
  const resource = new AsyncResource('content-type-parser:run', request)
189
+ const done = resource.bind(onDone)
188
190
 
189
191
  if (parser.asString === true || parser.asBuffer === true) {
190
192
  rawBody(
@@ -194,48 +196,44 @@ ContentTypeParser.prototype.run = function (contentType, handler, request, reply
194
196
  parser,
195
197
  done
196
198
  )
197
- } else {
198
- const result = parser.fn(request, request[kRequestPayloadStream], done)
199
+ return
200
+ }
199
201
 
200
- if (result && typeof result.then === 'function') {
201
- result.then(body => done(null, body), done)
202
- }
202
+ const result = parser.fn(request, request[kRequestPayloadStream], done)
203
+ if (result && typeof result.then === 'function') {
204
+ result.then(body => { done(null, body) }, done)
203
205
  }
204
206
 
205
- function done (error, body) {
206
- // We cannot use resource.bind() because it is broken in node v12 and v14
207
- resource.runInAsyncScope(() => {
208
- resource.emitDestroy()
209
- if (error) {
210
- reply[kReplyIsError] = true
211
- reply.send(error)
212
- } else {
213
- request.body = body
214
- handler(request, reply)
215
- }
216
- })
207
+ function onDone (error, body) {
208
+ resource.emitDestroy()
209
+ if (error != null) {
210
+ // We must close the connection as the client may
211
+ // send more data
212
+ reply.header('connection', 'close')
213
+ reply[kReplyIsError] = true
214
+ reply.send(error)
215
+ return
216
+ }
217
+ request.body = body
218
+ handler(request, reply)
217
219
  }
218
220
  }
219
221
 
220
222
  function rawBody (request, reply, options, parser, done) {
221
- const asString = parser.asString
223
+ const asString = parser.asString === true
222
224
  const limit = options.limit === null ? parser.bodyLimit : options.limit
223
225
  const contentLength = Number(request.headers['content-length'])
224
226
 
225
227
  if (contentLength > limit) {
226
- // We must close the connection as the client is going
227
- // to send this data anyway
228
- reply.header('connection', 'close')
229
- reply.send(new FST_ERR_CTP_BODY_TOO_LARGE())
228
+ done(new FST_ERR_CTP_BODY_TOO_LARGE(), undefined)
230
229
  return
231
230
  }
232
231
 
233
232
  let receivedLength = 0
234
- let body = asString === true ? '' : []
235
-
233
+ let body = asString ? '' : []
236
234
  const payload = request[kRequestPayloadStream] || request.raw
237
235
 
238
- if (asString === true) {
236
+ if (asString) {
239
237
  payload.setEncoding('utf8')
240
238
  }
241
239
 
@@ -245,7 +243,7 @@ function rawBody (request, reply, options, parser, done) {
245
243
  payload.resume()
246
244
 
247
245
  function onData (chunk) {
248
- receivedLength += chunk.length
246
+ receivedLength += asString ? Buffer.byteLength(chunk) : chunk.length
249
247
  const { receivedEncodedLength = 0 } = payload
250
248
  // The resulting body length must not exceed bodyLimit (see "zip bomb").
251
249
  // The case when encoded length is larger than received length is rather theoretical,
@@ -254,11 +252,11 @@ function rawBody (request, reply, options, parser, done) {
254
252
  payload.removeListener('data', onData)
255
253
  payload.removeListener('end', onEnd)
256
254
  payload.removeListener('error', onEnd)
257
- reply.send(new FST_ERR_CTP_BODY_TOO_LARGE())
255
+ done(new FST_ERR_CTP_BODY_TOO_LARGE(), undefined)
258
256
  return
259
257
  }
260
258
 
261
- if (asString === true) {
259
+ if (asString) {
262
260
  body += chunk
263
261
  } else {
264
262
  body.push(chunk)
@@ -270,37 +268,33 @@ function rawBody (request, reply, options, parser, done) {
270
268
  payload.removeListener('end', onEnd)
271
269
  payload.removeListener('error', onEnd)
272
270
 
273
- if (err !== undefined) {
271
+ if (err != null) {
274
272
  if (!(typeof err.statusCode === 'number' && err.statusCode >= 400)) {
275
273
  err.statusCode = 400
276
274
  }
277
- reply[kReplyIsError] = true
278
- reply.code(err.statusCode).send(err)
275
+ done(err, undefined)
279
276
  return
280
277
  }
281
278
 
282
- if (asString === true) {
283
- receivedLength = Buffer.byteLength(body)
284
- }
285
-
286
279
  if (!Number.isNaN(contentLength) && (payload.receivedEncodedLength || receivedLength) !== contentLength) {
287
- reply.header('connection', 'close')
288
- reply.send(new FST_ERR_CTP_INVALID_CONTENT_LENGTH())
280
+ done(new FST_ERR_CTP_INVALID_CONTENT_LENGTH(), undefined)
289
281
  return
290
282
  }
291
283
 
292
- if (asString === false) {
284
+ if (!asString) {
293
285
  body = Buffer.concat(body)
294
286
  }
295
287
 
296
288
  const result = parser.fn(request, body, done)
297
289
  if (result && typeof result.then === 'function') {
298
- result.then(body => done(null, body), done)
290
+ result.then(body => { done(null, body) }, done)
299
291
  }
300
292
  }
301
293
  }
302
294
 
303
295
  function getDefaultJsonParser (onProtoPoisoning, onConstructorPoisoning) {
296
+ const parseOptions = { protoAction: onProtoPoisoning, constructorAction: onConstructorPoisoning }
297
+
304
298
  return defaultJsonParser
305
299
 
306
300
  function defaultJsonParser (req, body, done) {
@@ -309,10 +303,9 @@ function getDefaultJsonParser (onProtoPoisoning, onConstructorPoisoning) {
309
303
  return
310
304
  }
311
305
  try {
312
- done(null, secureJsonParse(body, { protoAction: onProtoPoisoning, constructorAction: onConstructorPoisoning }))
313
- } catch (err) {
314
- err.statusCode = 400
315
- done(err, undefined)
306
+ done(null, secureJsonParse(body, parseOptions))
307
+ } catch {
308
+ done(new FST_ERR_CTP_INVALID_JSON_BODY(), undefined)
316
309
  }
317
310
  }
318
311
  }
@@ -39,7 +39,7 @@ function handleError (reply, error, cb) {
39
39
  if (!reply.log[kDisableRequestLogging]) {
40
40
  reply.log.warn(
41
41
  { req: reply.request, res: reply, err: error },
42
- error && error.message
42
+ error?.message
43
43
  )
44
44
  }
45
45
  reply.raw.writeHead(reply.raw.statusCode)
@@ -89,14 +89,14 @@ function defaultErrorHandler (error, request, reply) {
89
89
  if (!reply.log[kDisableRequestLogging]) {
90
90
  reply.log.info(
91
91
  { res: reply, err: error },
92
- error && error.message
92
+ error?.message
93
93
  )
94
94
  }
95
95
  } else {
96
96
  if (!reply.log[kDisableRequestLogging]) {
97
97
  reply.log.error(
98
98
  { req: request, res: reply, err: error },
99
- error && error.message
99
+ error?.message
100
100
  )
101
101
  }
102
102
  }
package/lib/errors.js CHANGED
@@ -124,6 +124,11 @@ const codes = {
124
124
  "Body cannot be empty when content-type is set to 'application/json'",
125
125
  400
126
126
  ),
127
+ FST_ERR_CTP_INVALID_JSON_BODY: createError(
128
+ 'FST_ERR_CTP_INVALID_JSON_BODY',
129
+ "Body is not valid JSON but content-type is set to 'application/json'",
130
+ 400
131
+ ),
127
132
  FST_ERR_CTP_INSTANCE_ALREADY_STARTED: createError(
128
133
  'FST_ERR_CTP_INSTANCE_ALREADY_STARTED',
129
134
  'Cannot call "%s" when fastify instance is already started!',
@@ -21,9 +21,7 @@ function handleRequest (err, request, reply) {
21
21
  return
22
22
  }
23
23
 
24
- const method = request.raw.method
25
- const headers = request.headers
26
- const context = request[kRouteContext]
24
+ const method = request.method
27
25
 
28
26
  if (this[kSupportedHTTPMethods].bodyless.has(method)) {
29
27
  handler(request, reply)
@@ -31,28 +29,26 @@ function handleRequest (err, request, reply) {
31
29
  }
32
30
 
33
31
  if (this[kSupportedHTTPMethods].bodywith.has(method)) {
32
+ const headers = request.headers
34
33
  const contentType = headers['content-type']
35
- const contentLength = headers['content-length']
36
- const transferEncoding = headers['transfer-encoding']
37
34
 
38
35
  if (contentType === undefined) {
39
- if (
40
- (contentLength === undefined || contentLength === '0') &&
41
- transferEncoding === undefined
42
- ) {
36
+ const contentLength = headers['content-length']
37
+ const transferEncoding = headers['transfer-encoding']
38
+ const isEmptyBody = transferEncoding === undefined &&
39
+ (contentLength === undefined || contentLength === '0')
40
+
41
+ if (isEmptyBody) {
43
42
  // Request has no body to parse
44
43
  handler(request, reply)
45
- } else {
46
- context.contentTypeParser.run('', handler, request, reply)
47
- }
48
- } else {
49
- if (contentLength === undefined && transferEncoding === undefined && method === 'OPTIONS') {
50
- // OPTIONS can have a Content-Type header without a body
51
- handler(request, reply)
52
44
  return
53
45
  }
54
- context.contentTypeParser.run(contentType, handler, request, reply)
46
+
47
+ request[kRouteContext].contentTypeParser.run('', handler, request, reply)
48
+ return
55
49
  }
50
+
51
+ request[kRouteContext].contentTypeParser.run(contentType, handler, request, reply)
56
52
  return
57
53
  }
58
54
 
package/lib/promise.js ADDED
@@ -0,0 +1,23 @@
1
+ 'use strict'
2
+
3
+ const { kTestInternals } = require('./symbols')
4
+
5
+ function withResolvers () {
6
+ let res, rej
7
+ const promise = new Promise((resolve, reject) => {
8
+ res = resolve
9
+ rej = reject
10
+ })
11
+ return { promise, resolve: res, reject: rej }
12
+ }
13
+
14
+ module.exports = {
15
+ // TODO(20.x): remove when node@20 is not supported
16
+ withResolvers: typeof Promise.withResolvers === 'function'
17
+ ? Promise.withResolvers.bind(Promise) // Promise.withResolvers must bind to itself
18
+ /* c8 ignore next */
19
+ : withResolvers, // Tested using the kTestInternals
20
+ [kTestInternals]: {
21
+ withResolvers
22
+ }
23
+ }
package/lib/reply.js CHANGED
@@ -126,16 +126,16 @@ Reply.prototype.hijack = function () {
126
126
  }
127
127
 
128
128
  Reply.prototype.send = function (payload) {
129
- if (this[kReplyIsRunningOnErrorHook] === true) {
129
+ if (this[kReplyIsRunningOnErrorHook]) {
130
130
  throw new FST_ERR_SEND_INSIDE_ONERR()
131
131
  }
132
132
 
133
- if (this.sent) {
133
+ if (this.sent === true) {
134
134
  this.log.warn({ err: new FST_ERR_REP_ALREADY_SENT(this.request.url, this.request.method) })
135
135
  return this
136
136
  }
137
137
 
138
- if (payload instanceof Error || this[kReplyIsError] === true) {
138
+ if (this[kReplyIsError] || payload instanceof Error) {
139
139
  this[kReplyIsError] = false
140
140
  onErrorHook(this, payload, onSendHook)
141
141
  return this
@@ -162,8 +162,8 @@ Reply.prototype.send = function (payload) {
162
162
  return this
163
163
  }
164
164
 
165
- if (payload?.buffer instanceof ArrayBuffer) {
166
- if (hasContentType === false) {
165
+ if (payload.buffer instanceof ArrayBuffer) {
166
+ if (!hasContentType) {
167
167
  this[kReplyHeaders]['content-type'] = CONTENT_TYPE.OCTET
168
168
  }
169
169
  const payloadToSend = Buffer.isBuffer(payload) ? payload : Buffer.from(payload.buffer, payload.byteOffset, payload.byteLength)
@@ -171,7 +171,7 @@ Reply.prototype.send = function (payload) {
171
171
  return this
172
172
  }
173
173
 
174
- if (hasContentType === false && typeof payload === 'string') {
174
+ if (!hasContentType && typeof payload === 'string') {
175
175
  this[kReplyHeaders]['content-type'] = CONTENT_TYPE.PLAIN
176
176
  onSendHook(this, payload)
177
177
  return this
@@ -182,26 +182,24 @@ Reply.prototype.send = function (payload) {
182
182
  if (typeof payload !== 'string') {
183
183
  preSerializationHook(this, payload)
184
184
  return this
185
- } else {
186
- payload = this[kReplySerializer](payload)
187
185
  }
186
+ payload = this[kReplySerializer](payload)
188
187
 
189
188
  // The indexOf below also matches custom json mimetypes such as 'application/hal+json' or 'application/ld+json'
190
- } else if (hasContentType === false || contentType.indexOf('json') > -1) {
191
- if (hasContentType === false) {
189
+ } else if (!hasContentType || contentType.indexOf('json') !== -1) {
190
+ if (!hasContentType) {
192
191
  this[kReplyHeaders]['content-type'] = CONTENT_TYPE.JSON
193
- } else {
192
+ } else if (contentType.indexOf('charset') === -1) {
194
193
  // If user doesn't set charset, we will set charset to utf-8
195
- if (contentType.indexOf('charset') === -1) {
196
- const customContentType = contentType.trim()
197
- if (customContentType.endsWith(';')) {
198
- // custom content-type is ended with ';'
199
- this[kReplyHeaders]['content-type'] = `${customContentType} charset=utf-8`
200
- } else {
201
- this[kReplyHeaders]['content-type'] = `${customContentType}; charset=utf-8`
202
- }
194
+ const customContentType = contentType.trim()
195
+ if (customContentType.endsWith(';')) {
196
+ // custom content-type is ended with ';'
197
+ this[kReplyHeaders]['content-type'] = `${customContentType} charset=utf-8`
198
+ } else {
199
+ this[kReplyHeaders]['content-type'] = `${customContentType}; charset=utf-8`
203
200
  }
204
201
  }
202
+
205
203
  if (typeof payload !== 'string') {
206
204
  preSerializationHook(this, payload)
207
205
  return this
package/lib/route.js CHANGED
@@ -29,6 +29,8 @@ const {
29
29
  FST_ERR_HOOK_INVALID_ASYNC_HANDLER
30
30
  } = require('./errors')
31
31
 
32
+ const { FSTDEP022 } = require('./warnings')
33
+
32
34
  const {
33
35
  kRoutePrefix,
34
36
  kSupportedHTTPMethods,
@@ -52,6 +54,20 @@ const { buildErrorHandler } = require('./error-handler')
52
54
  const { createChildLogger } = require('./logger-factory.js')
53
55
  const { getGenReqId } = require('./reqIdGenFactory.js')
54
56
 
57
+ const routerKeys = [
58
+ 'allowUnsafeRegex',
59
+ 'buildPrettyMeta',
60
+ 'caseSensitive',
61
+ 'constraints',
62
+ 'defaultRoute',
63
+ 'ignoreDuplicateSlashes',
64
+ 'ignoreTrailingSlash',
65
+ 'maxParamLength',
66
+ 'onBadUrl',
67
+ 'querystringParser',
68
+ 'useSemicolonDelimiter'
69
+ ]
70
+
55
71
  function buildRouting (options) {
56
72
  const router = FindMyWay(options.config)
57
73
 
@@ -85,8 +101,8 @@ function buildRouting (options) {
85
101
 
86
102
  globalExposeHeadRoutes = options.exposeHeadRoutes
87
103
  disableRequestLogging = options.disableRequestLogging
88
- ignoreTrailingSlash = options.ignoreTrailingSlash
89
- ignoreDuplicateSlashes = options.ignoreDuplicateSlashes
104
+ ignoreTrailingSlash = options.routerOptions.ignoreTrailingSlash
105
+ ignoreDuplicateSlashes = options.routerOptions.ignoreDuplicateSlashes
90
106
  return503OnClosing = Object.hasOwn(options, 'return503OnClosing') ? options.return503OnClosing : true
91
107
  keepAliveConnections = fastifyArgs.keepAliveConnections
92
108
  },
@@ -572,6 +588,24 @@ function runPreParsing (err, request, reply) {
572
588
  }
573
589
  }
574
590
 
591
+ function buildRouterOptions (options, defaultOptions) {
592
+ const routerOptions = options.routerOptions || Object.create(null)
593
+
594
+ const usedDeprecatedOptions = routerKeys.filter(key => Object.hasOwn(options, key))
595
+
596
+ if (usedDeprecatedOptions.length > 0) {
597
+ FSTDEP022(usedDeprecatedOptions.join(', '))
598
+ }
599
+
600
+ for (const key of routerKeys) {
601
+ if (!Object.hasOwn(routerOptions, key)) {
602
+ routerOptions[key] = options[key] ?? defaultOptions[key]
603
+ }
604
+ }
605
+
606
+ return routerOptions
607
+ }
608
+
575
609
  /**
576
610
  * Used within the route handler as a `net.Socket.close` event handler.
577
611
  * The purpose is to remove a socket from the tracked sockets collection when
@@ -583,4 +617,4 @@ function removeTrackedSocket () {
583
617
 
584
618
  function noop () { }
585
619
 
586
- module.exports = { buildRouting, validateBodyLimitOption }
620
+ module.exports = { buildRouting, validateBodyLimitOption, buildRouterOptions }
package/lib/server.js CHANGED
@@ -14,6 +14,7 @@ const {
14
14
  FST_ERR_REOPENED_SERVER,
15
15
  FST_ERR_LISTEN_OPTIONS_INVALID
16
16
  } = require('./errors')
17
+ const PonyPromise = require('./promise')
17
18
 
18
19
  module.exports.createServer = createServer
19
20
 
@@ -41,10 +42,14 @@ function createServer (options, httpHandler) {
41
42
  throw new FST_ERR_LISTEN_OPTIONS_INVALID('Invalid options.signal')
42
43
  }
43
44
 
44
- if (listenOptions.signal.aborted) {
45
- this.close()
45
+ // copy the current signal state
46
+ this[kState].aborted = listenOptions.signal.aborted
47
+
48
+ if (this[kState].aborted) {
49
+ return this.close()
46
50
  } else {
47
51
  const onAborted = () => {
52
+ this[kState].aborted = true
48
53
  this.close()
49
54
  }
50
55
  listenOptions.signal.addEventListener('abort', onAborted, { once: true })
@@ -99,18 +104,18 @@ function createServer (options, httpHandler) {
99
104
  if (cb === undefined) {
100
105
  const listening = listenPromise.call(this, server, listenOptions)
101
106
  return listening.then(address => {
102
- return new Promise((resolve, reject) => {
103
- if (host === 'localhost') {
104
- multipleBindings.call(this, server, httpHandler, options, listenOptions, () => {
105
- this[kState].listening = true
106
- resolve(address)
107
- onListenHookRunner(this)
108
- })
109
- } else {
107
+ const { promise, resolve } = PonyPromise.withResolvers()
108
+ if (host === 'localhost') {
109
+ multipleBindings.call(this, server, httpHandler, options, listenOptions, () => {
110
+ this[kState].listening = true
110
111
  resolve(address)
111
112
  onListenHookRunner(this)
112
- }
113
- })
113
+ })
114
+ } else {
115
+ resolve(address)
116
+ onListenHookRunner(this)
117
+ }
118
+ return promise
114
119
  })
115
120
  }
116
121
 
@@ -126,7 +131,7 @@ function multipleBindings (mainServer, httpHandler, serverOpts, listenOptions, o
126
131
 
127
132
  // let's check if we need to bind additional addresses
128
133
  dns.lookup(listenOptions.host, { all: true }, (dnsErr, addresses) => {
129
- if (dnsErr) {
134
+ if (dnsErr || this[kState].aborted) {
130
135
  // not blocking the main server listening
131
136
  // this.log.warn('dns.lookup error:', dnsErr)
132
137
  onListen()
@@ -239,35 +244,31 @@ function listenPromise (server, listenOptions) {
239
244
  }
240
245
 
241
246
  return this.ready().then(() => {
242
- let errEventHandler
243
- let listeningEventHandler
247
+ // skip listen when aborted during ready
248
+ if (this[kState].aborted) return
249
+
250
+ const { promise, resolve, reject } = PonyPromise.withResolvers()
251
+
252
+ const errEventHandler = (err) => {
253
+ cleanup()
254
+ this[kState].listening = false
255
+ reject(err)
256
+ }
257
+ const listeningEventHandler = () => {
258
+ cleanup()
259
+ this[kState].listening = true
260
+ resolve(logServerAddress.call(this, server, listenOptions.listenTextResolver || defaultResolveServerListeningText))
261
+ }
244
262
  function cleanup () {
245
263
  server.removeListener('error', errEventHandler)
246
264
  server.removeListener('listening', listeningEventHandler)
247
265
  }
248
- const errEvent = new Promise((resolve, reject) => {
249
- errEventHandler = (err) => {
250
- cleanup()
251
- this[kState].listening = false
252
- reject(err)
253
- }
254
- server.once('error', errEventHandler)
255
- })
256
- const listeningEvent = new Promise((resolve, reject) => {
257
- listeningEventHandler = () => {
258
- cleanup()
259
- this[kState].listening = true
260
- resolve(logServerAddress.call(this, server, listenOptions.listenTextResolver || defaultResolveServerListeningText))
261
- }
262
- server.once('listening', listeningEventHandler)
263
- })
266
+ server.once('error', errEventHandler)
267
+ server.once('listening', listeningEventHandler)
264
268
 
265
269
  server.listen(listenOptions)
266
270
 
267
- return Promise.race([
268
- errEvent, // e.g invalid port range error is always emitted before the server listening
269
- listeningEvent
270
- ])
271
+ return promise
271
272
  })
272
273
  }
273
274
 
package/lib/warnings.js CHANGED
@@ -6,8 +6,10 @@ const { createWarning } = require('process-warning')
6
6
  * Deprecation codes:
7
7
  * - FSTWRN001
8
8
  * - FSTSEC001
9
+ * - FSTDEP022
9
10
  *
10
11
  * Deprecation Codes FSTDEP001 - FSTDEP021 were used by v4 and MUST NOT not be reused.
12
+ * - FSTDEP022 is used by v5 and MUST NOT be reused.
11
13
  * Warning Codes FSTWRN001 - FSTWRN002 were used by v4 and MUST NOT not be reused.
12
14
  */
13
15
 
@@ -39,9 +41,17 @@ const FSTSEC001 = createWarning({
39
41
  unlimited: true
40
42
  })
41
43
 
44
+ const FSTDEP022 = createWarning({
45
+ name: 'FastifyWarning',
46
+ code: 'FSTDPE022',
47
+ message: 'The router options for %s property access is deprecated. Please use "options.routerOptions" instead for accessing router options. The router options will be removed in `fastify@6`.',
48
+ unlimited: true
49
+ })
50
+
42
51
  module.exports = {
43
52
  FSTWRN001,
44
53
  FSTWRN003,
45
54
  FSTWRN004,
46
- FSTSEC001
55
+ FSTSEC001,
56
+ FSTDEP022
47
57
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fastify",
3
- "version": "5.4.0",
3
+ "version": "5.6.0",
4
4
  "description": "Fast and low overhead web framework, for Node.js",
5
5
  "main": "fastify.js",
6
6
  "type": "commonjs",
@@ -9,6 +9,7 @@
9
9
  "bench": "branchcmp -r 2 -g -s \"npm run benchmark\"",
10
10
  "benchmark": "concurrently -k -s first \"node ./examples/benchmark/simple.js\" \"autocannon -c 100 -d 30 -p 10 localhost:3000/\"",
11
11
  "benchmark:parser": "concurrently -k -s first \"node ./examples/benchmark/parser.js\" \"autocannon -c 100 -d 30 -p 10 -i ./examples/benchmark/body.json -H \"content-type:application/jsoff\" -m POST localhost:3000/\"",
12
+ "benchmark:parser:error": "concurrently -k -s first \"node ./examples/benchmark/parser.js\" \"autocannon -c 100 -d 30 -p 10 -i ./examples/benchmark/body.json -H \"content-type:application/jsoff\" -H \"content-length:123\" -m POST localhost:3000/\"",
12
13
  "build:validation": "node build/build-error-serializer.js && node build/build-validation.js",
13
14
  "coverage": "c8 --reporter html borp --reporter=@jsumners/line-reporter --coverage --check-coverage --lines 100 ",
14
15
  "coverage:ci-check-coverage": "borp --reporter=@jsumners/line-reporter --coverage --check-coverage --lines 100",
@@ -168,9 +169,9 @@
168
169
  "@jsumners/line-reporter": "^1.0.1",
169
170
  "@sinclair/typebox": "^0.34.13",
170
171
  "@sinonjs/fake-timers": "^11.2.2",
171
- "@stylistic/eslint-plugin": "^4.1.0",
172
+ "@stylistic/eslint-plugin": "^5.1.0",
172
173
  "@stylistic/eslint-plugin-js": "^4.1.0",
173
- "@types/node": "^22.0.0",
174
+ "@types/node": "^24.0.12",
174
175
  "ajv": "^8.12.0",
175
176
  "ajv-errors": "^3.0.0",
176
177
  "ajv-formats": "^3.0.1",
@@ -180,7 +181,7 @@
180
181
  "borp": "^0.20.0",
181
182
  "branch-comparer": "^1.1.0",
182
183
  "concurrently": "^9.1.2",
183
- "cross-env": "^7.0.3",
184
+ "cross-env": "^10.0.0",
184
185
  "eslint": "^9.0.0",
185
186
  "fast-json-body": "^1.1.0",
186
187
  "fastify-plugin": "^5.0.0",
@@ -194,11 +195,10 @@
194
195
  "neostandard": "^0.12.0",
195
196
  "node-forge": "^1.3.1",
196
197
  "proxyquire": "^2.1.3",
197
- "simple-get": "^4.0.1",
198
198
  "split2": "^4.2.0",
199
199
  "tsd": "^0.32.0",
200
- "typescript": "~5.8.2",
201
- "undici": "^6.13.0",
200
+ "typescript": "~5.9.2",
201
+ "undici": "^7.11.0",
202
202
  "vary": "^1.1.2",
203
203
  "yup": "^1.4.0"
204
204
  },