fastify 5.3.3 → 5.5.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 (137) hide show
  1. package/.vscode/settings.json +15 -15
  2. package/LICENSE +1 -1
  3. package/README.md +2 -0
  4. package/SECURITY.md +158 -2
  5. package/build/build-validation.js +20 -1
  6. package/docs/Guides/Delay-Accepting-Requests.md +8 -5
  7. package/docs/Guides/Ecosystem.md +20 -5
  8. package/docs/Guides/Migration-Guide-V5.md +6 -10
  9. package/docs/Guides/Recommendations.md +1 -1
  10. package/docs/Reference/ContentTypeParser.md +1 -1
  11. package/docs/Reference/Errors.md +5 -3
  12. package/docs/Reference/Hooks.md +16 -20
  13. package/docs/Reference/Lifecycle.md +2 -2
  14. package/docs/Reference/Logging.md +3 -3
  15. package/docs/Reference/Middleware.md +1 -1
  16. package/docs/Reference/Reply.md +8 -8
  17. package/docs/Reference/Request.md +2 -2
  18. package/docs/Reference/Routes.md +7 -6
  19. package/docs/Reference/Server.md +341 -200
  20. package/docs/Reference/TypeScript.md +1 -3
  21. package/docs/Reference/Validation-and-Serialization.md +56 -4
  22. package/docs/Reference/Warnings.md +2 -1
  23. package/fastify.d.ts +4 -3
  24. package/fastify.js +47 -34
  25. package/lib/configValidator.js +196 -28
  26. package/lib/contentTypeParser.js +41 -48
  27. package/lib/error-handler.js +3 -3
  28. package/lib/errors.js +11 -0
  29. package/lib/handleRequest.js +13 -17
  30. package/lib/pluginOverride.js +3 -1
  31. package/lib/promise.js +23 -0
  32. package/lib/reply.js +24 -30
  33. package/lib/request.js +3 -10
  34. package/lib/route.js +37 -3
  35. package/lib/server.js +36 -35
  36. package/lib/symbols.js +1 -0
  37. package/lib/warnings.js +19 -1
  38. package/package.json +14 -10
  39. package/test/404s.test.js +226 -325
  40. package/test/allow-unsafe-regex.test.js +19 -48
  41. package/test/als.test.js +28 -40
  42. package/test/async-await.test.js +84 -128
  43. package/test/async_hooks.test.js +18 -37
  44. package/test/body-limit.test.js +90 -63
  45. package/test/buffer.test.js +22 -0
  46. package/test/build-certificate.js +1 -1
  47. package/test/case-insensitive.test.js +44 -65
  48. package/test/check.test.js +17 -21
  49. package/test/close-pipelining.test.js +24 -15
  50. package/test/constrained-routes.test.js +231 -0
  51. package/test/custom-http-server.test.js +7 -15
  52. package/test/custom-parser-async.test.js +17 -22
  53. package/test/custom-parser.0.test.js +267 -348
  54. package/test/custom-parser.1.test.js +141 -191
  55. package/test/custom-parser.2.test.js +34 -44
  56. package/test/custom-parser.3.test.js +56 -104
  57. package/test/custom-parser.4.test.js +106 -144
  58. package/test/custom-parser.5.test.js +56 -75
  59. package/test/custom-querystring-parser.test.js +51 -77
  60. package/test/decorator-namespace.test._js_ +3 -4
  61. package/test/decorator.test.js +76 -259
  62. package/test/delete.test.js +101 -110
  63. package/test/diagnostics-channel/404.test.js +7 -15
  64. package/test/diagnostics-channel/async-delay-request.test.js +7 -16
  65. package/test/diagnostics-channel/async-request.test.js +8 -16
  66. package/test/diagnostics-channel/error-request.test.js +7 -15
  67. package/test/diagnostics-channel/sync-delay-request.test.js +7 -16
  68. package/test/diagnostics-channel/sync-request-reply.test.js +9 -16
  69. package/test/diagnostics-channel/sync-request.test.js +9 -16
  70. package/test/fastify-instance.test.js +1 -1
  71. package/test/header-overflow.test.js +18 -29
  72. package/test/helper.js +139 -135
  73. package/test/hooks-async.test.js +259 -235
  74. package/test/hooks.test.js +951 -996
  75. package/test/http-methods/copy.test.js +14 -19
  76. package/test/http-methods/get.test.js +131 -143
  77. package/test/http-methods/head.test.js +53 -84
  78. package/test/http-methods/lock.test.js +31 -31
  79. package/test/http-methods/mkcalendar.test.js +45 -72
  80. package/test/http-methods/mkcol.test.js +5 -9
  81. package/test/http-methods/move.test.js +6 -10
  82. package/test/http-methods/propfind.test.js +34 -44
  83. package/test/http-methods/proppatch.test.js +23 -29
  84. package/test/http-methods/report.test.js +44 -69
  85. package/test/http-methods/search.test.js +67 -82
  86. package/test/http-methods/unlock.test.js +5 -9
  87. package/test/http2/closing.test.js +38 -20
  88. package/test/http2/secure-with-fallback.test.js +31 -28
  89. package/test/https/custom-https-server.test.js +9 -13
  90. package/test/https/https.test.js +56 -53
  91. package/test/input-validation.js +139 -150
  92. package/test/internals/errors.test.js +50 -1
  93. package/test/internals/handle-request.test.js +72 -65
  94. package/test/internals/promise.test.js +63 -0
  95. package/test/internals/reply.test.js +277 -496
  96. package/test/issue-4959.test.js +12 -3
  97. package/test/listen.4.test.js +31 -43
  98. package/test/nullable-validation.test.js +33 -46
  99. package/test/output-validation.test.js +24 -26
  100. package/test/plugin.1.test.js +40 -68
  101. package/test/plugin.2.test.js +108 -120
  102. package/test/plugin.3.test.js +50 -72
  103. package/test/plugin.4.test.js +124 -119
  104. package/test/promises.test.js +42 -63
  105. package/test/proto-poisoning.test.js +78 -97
  106. package/test/register.test.js +8 -18
  107. package/test/request-error.test.js +57 -146
  108. package/test/request-id.test.js +30 -49
  109. package/test/route-hooks.test.js +117 -101
  110. package/test/route-prefix.test.js +194 -133
  111. package/test/route-shorthand.test.js +9 -27
  112. package/test/route.1.test.js +74 -131
  113. package/test/route.8.test.js +9 -17
  114. package/test/router-options.test.js +450 -0
  115. package/test/schema-serialization.test.js +177 -154
  116. package/test/schema-special-usage.test.js +165 -132
  117. package/test/schema-validation.test.js +254 -218
  118. package/test/server.test.js +143 -5
  119. package/test/set-error-handler.test.js +58 -1
  120. package/test/skip-reply-send.test.js +64 -69
  121. package/test/stream.1.test.js +33 -50
  122. package/test/stream.4.test.js +18 -28
  123. package/test/stream.5.test.js +11 -19
  124. package/test/trust-proxy.test.js +32 -58
  125. package/test/types/errors.test-d.ts +13 -1
  126. package/test/types/fastify.test-d.ts +3 -0
  127. package/test/types/request.test-d.ts +1 -0
  128. package/test/types/type-provider.test-d.ts +55 -0
  129. package/test/url-rewriting.test.js +45 -62
  130. package/test/use-semicolon-delimiter.test.js +117 -59
  131. package/test/versioned-routes.test.js +39 -56
  132. package/types/errors.d.ts +11 -1
  133. package/types/hooks.d.ts +1 -1
  134. package/types/instance.d.ts +1 -1
  135. package/types/reply.d.ts +2 -2
  136. package/types/request.d.ts +1 -0
  137. package/.taprc +0 -7
@@ -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
@@ -64,6 +64,12 @@ const codes = {
64
64
  500,
65
65
  TypeError
66
66
  ),
67
+ FST_ERR_ERROR_HANDLER_ALREADY_SET: createError(
68
+ 'FST_ERR_ERROR_HANDLER_ALREADY_SET',
69
+ "Error Handler already set in this scope. Set 'allowErrorHandlerOverride: true' to allow overriding.",
70
+ 500,
71
+ TypeError
72
+ ),
67
73
 
68
74
  /**
69
75
  * ContentTypeParser
@@ -118,6 +124,11 @@ const codes = {
118
124
  "Body cannot be empty when content-type is set to 'application/json'",
119
125
  400
120
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
+ ),
121
132
  FST_ERR_CTP_INSTANCE_ALREADY_STARTED: createError(
122
133
  'FST_ERR_CTP_INSTANCE_ALREADY_STARTED',
123
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
 
@@ -12,7 +12,8 @@ const {
12
12
  kReply,
13
13
  kRequest,
14
14
  kFourOhFour,
15
- kPluginNameChain
15
+ kPluginNameChain,
16
+ kErrorHandlerAlreadySet
16
17
  } = require('./symbols.js')
17
18
 
18
19
  const Reply = require('./reply')
@@ -57,6 +58,7 @@ module.exports = function override (old, fn, opts) {
57
58
  // Track the plugin chain since the root instance.
58
59
  // When an non-encapsulated plugin is added, the chain will be updated.
59
60
  instance[kPluginNameChain] = [fnName]
61
+ instance[kErrorHandlerAlreadySet] = false
60
62
 
61
63
  if (instance[kLogSerializers] || opts.logSerializers) {
62
64
  instance[kLogSerializers] = Object.assign(Object.create(instance[kLogSerializers]), opts.logSerializers)
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
@@ -215,12 +213,8 @@ Reply.prototype.send = function (payload) {
215
213
 
216
214
  Reply.prototype.getHeader = function (key) {
217
215
  key = key.toLowerCase()
218
- const res = this.raw
219
- let value = this[kReplyHeaders][key]
220
- if (value === undefined && res.hasHeader(key)) {
221
- value = res.getHeader(key)
222
- }
223
- return value
216
+ const value = this[kReplyHeaders][key]
217
+ return value !== undefined ? value : this.raw.getHeader(key)
224
218
  }
225
219
 
226
220
  Reply.prototype.getHeaders = function () {
@@ -315,12 +309,12 @@ Reply.prototype.removeTrailer = function (key) {
315
309
  }
316
310
 
317
311
  Reply.prototype.code = function (code) {
318
- const intValue = Number(code)
319
- if (isNaN(intValue) || intValue < 100 || intValue > 599) {
312
+ const statusCode = +code
313
+ if (!(statusCode >= 100 && statusCode <= 599)) {
320
314
  throw new FST_ERR_BAD_STATUS_CODE(code || String(code))
321
315
  }
322
316
 
323
- this.raw.statusCode = intValue
317
+ this.raw.statusCode = statusCode
324
318
  this[kReplyHasStatusCode] = true
325
319
  return this
326
320
  }
@@ -500,11 +494,11 @@ function preSerializationHook (reply, payload) {
500
494
  preSerializationHookEnd
501
495
  )
502
496
  } else {
503
- preSerializationHookEnd(null, reply.request, reply, payload)
497
+ preSerializationHookEnd(null, undefined, reply, payload)
504
498
  }
505
499
  }
506
500
 
507
- function preSerializationHookEnd (err, request, reply, payload) {
501
+ function preSerializationHookEnd (err, _request, reply, payload) {
508
502
  if (err != null) {
509
503
  onErrorHook(reply, err)
510
504
  return
package/lib/request.js CHANGED
@@ -188,19 +188,12 @@ Object.defineProperties(Request.prototype, {
188
188
  exposeHeadRoute: context.exposeHeadRoute,
189
189
  prefixTrailingSlash: context.prefixTrailingSlash,
190
190
  handler: context.handler,
191
+ config: context.config,
192
+ schema: context.schema,
191
193
  version
192
194
  }
193
195
 
194
- Object.defineProperties(options, {
195
- config: {
196
- get: () => context.config
197
- },
198
- schema: {
199
- get: () => context.schema
200
- }
201
- })
202
-
203
- return Object.freeze(options)
196
+ return options
204
197
  }
205
198
  },
206
199
  is404: {
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/symbols.js CHANGED
@@ -56,6 +56,7 @@ const keys = {
56
56
  // This symbol is only meant to be used for fastify tests and should not be used for any other purpose
57
57
  kTestInternals: Symbol('fastify.testInternals'),
58
58
  kErrorHandler: Symbol('fastify.errorHandler'),
59
+ kErrorHandlerAlreadySet: Symbol('fastify.errorHandlerAlreadySet'),
59
60
  kChildLoggerFactory: Symbol('fastify.childLoggerFactory'),
60
61
  kHasBeenDecorated: Symbol('fastify.hasBeenDecorated'),
61
62
  kKeepAliveConnections: Symbol('fastify.keepAliveConnections'),