fastify 5.8.5 → 5.10.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 (126) hide show
  1. package/PROJECT_CHARTER.md +1 -1
  2. package/README.md +7 -10
  3. package/SECURITY.md +1 -1
  4. package/SPONSORS.md +6 -4
  5. package/build/build-validation.js +2 -2
  6. package/docs/Guides/Database.md +0 -28
  7. package/docs/Guides/Delay-Accepting-Requests.md +1 -1
  8. package/docs/Guides/Ecosystem.md +23 -9
  9. package/docs/Guides/Getting-Started.md +2 -2
  10. package/docs/Guides/Migration-Guide-V4.md +2 -2
  11. package/docs/Guides/Migration-Guide-V5.md +1 -1
  12. package/docs/Guides/Plugins-Guide.md +1 -1
  13. package/docs/Guides/Prototype-Poisoning.md +3 -3
  14. package/docs/Guides/Serverless.md +8 -15
  15. package/docs/Guides/Style-Guide.md +1 -1
  16. package/docs/Guides/Write-Plugin.md +1 -1
  17. package/docs/Reference/Encapsulation.md +27 -26
  18. package/docs/Reference/Errors.md +12 -4
  19. package/docs/Reference/HTTP2.md +10 -10
  20. package/docs/Reference/Hooks.md +5 -5
  21. package/docs/Reference/Index.md +14 -16
  22. package/docs/Reference/LTS.md +12 -13
  23. package/docs/Reference/Lifecycle.md +9 -8
  24. package/docs/Reference/Logging.md +47 -39
  25. package/docs/Reference/Middleware.md +21 -25
  26. package/docs/Reference/Principles.md +2 -2
  27. package/docs/Reference/Reply.md +6 -1
  28. package/docs/Reference/Request.md +29 -18
  29. package/docs/Reference/Routes.md +5 -2
  30. package/docs/Reference/Server.md +138 -11
  31. package/docs/Reference/Type-Providers.md +53 -9
  32. package/docs/Reference/TypeScript.md +3 -3
  33. package/docs/Reference/Validation-and-Serialization.md +15 -2
  34. package/docs/Reference/Warnings.md +11 -6
  35. package/eslint.config.js +7 -2
  36. package/fastify.d.ts +13 -3
  37. package/fastify.js +60 -31
  38. package/lib/content-type-parser.js +2 -0
  39. package/lib/content-type.js +34 -1
  40. package/lib/context.js +0 -3
  41. package/lib/decorate.js +11 -3
  42. package/lib/error-handler.js +10 -28
  43. package/lib/error-serializer.js +59 -59
  44. package/lib/errors.js +23 -1
  45. package/lib/four-oh-four.js +22 -19
  46. package/lib/handle-request.js +12 -5
  47. package/lib/log-controller.js +169 -0
  48. package/lib/logger-factory.js +25 -4
  49. package/lib/plugin-override.js +2 -1
  50. package/lib/plugin-utils.js +5 -5
  51. package/lib/reply.js +96 -50
  52. package/lib/req-id-gen-factory.js +4 -1
  53. package/lib/request.js +16 -6
  54. package/lib/route.js +47 -41
  55. package/lib/schema-controller.js +1 -1
  56. package/lib/schemas.js +37 -30
  57. package/lib/symbols.js +4 -2
  58. package/lib/validation.js +10 -13
  59. package/lib/warnings.js +22 -4
  60. package/package.json +15 -17
  61. package/scripts/validate-ecosystem-links.js +1 -0
  62. package/test/bundler/esbuild/package.json +1 -1
  63. package/test/close-pipelining.test.js +1 -2
  64. package/test/content-type.test.js +20 -0
  65. package/test/custom-http-server.test.js +38 -0
  66. package/test/decorator-instance-properties.test.js +63 -0
  67. package/test/diagnostics-channel/async-error-handler.test.js +74 -0
  68. package/test/genReqId.test.js +24 -0
  69. package/test/hooks.test.js +94 -0
  70. package/test/http-methods/get.test.js +1 -1
  71. package/test/http2/plain.test.js +135 -0
  72. package/test/http2/secure-with-fallback.test.js +1 -1
  73. package/test/https/https.test.js +1 -2
  74. package/test/internals/errors.test.js +31 -1
  75. package/test/internals/logger.test.js +322 -0
  76. package/test/internals/plugin.test.js +3 -1
  77. package/test/internals/reply.test.js +35 -4
  78. package/test/internals/request.test.js +37 -10
  79. package/test/internals/schema-controller-perf.test.js +33 -0
  80. package/test/logger/logging.test.js +57 -1
  81. package/test/logger/options.test.js +38 -1
  82. package/test/reply-error.test.js +1 -1
  83. package/test/reply-trailers.test.js +70 -0
  84. package/test/request-media-type.test.js +105 -0
  85. package/test/route-prefix.test.js +34 -0
  86. package/test/router-options.test.js +222 -11
  87. package/test/schema-serialization.test.js +108 -0
  88. package/test/schema-validation.test.js +24 -0
  89. package/test/scripts/validate-ecosystem-links.test.js +40 -57
  90. package/test/stream.4.test.js +2 -2
  91. package/test/throw.test.js +14 -0
  92. package/test/trust-proxy.test.js +70 -30
  93. package/test/types/content-type-parser.tst.ts +70 -0
  94. package/test/types/{decorate-request-reply.test-d.ts → decorate-request-reply.tst.ts} +4 -4
  95. package/test/types/{dummy-plugin.ts → dummy-plugin.mts} +1 -1
  96. package/test/types/errors.tst.ts +91 -0
  97. package/test/types/fastify.tst.ts +351 -0
  98. package/test/types/hooks.tst.ts +578 -0
  99. package/test/types/instance.tst.ts +597 -0
  100. package/test/types/{logger.test-d.ts → logger.tst.ts} +61 -62
  101. package/test/types/plugin.tst.ts +96 -0
  102. package/test/types/register.tst.ts +245 -0
  103. package/test/types/reply.tst.ts +297 -0
  104. package/test/types/request.tst.ts +199 -0
  105. package/test/types/route.tst.ts +576 -0
  106. package/test/types/{schema.test-d.ts → schema.tst.ts} +22 -22
  107. package/test/types/{serverFactory.test-d.ts → serverFactory.tst.ts} +4 -4
  108. package/test/types/tsconfig.json +9 -0
  109. package/test/types/{type-provider.test-d.ts → type-provider.tst.ts} +256 -250
  110. package/test/types/using.tst.ts +14 -0
  111. package/types/errors.d.ts +4 -0
  112. package/types/instance.d.ts +2 -0
  113. package/types/logger.d.ts +22 -0
  114. package/types/request.d.ts +23 -2
  115. package/test/types/content-type-parser.test-d.ts +0 -72
  116. package/test/types/errors.test-d.ts +0 -90
  117. package/test/types/fastify.test-d.ts +0 -352
  118. package/test/types/hooks.test-d.ts +0 -550
  119. package/test/types/import.ts +0 -2
  120. package/test/types/instance.test-d.ts +0 -588
  121. package/test/types/plugin.test-d.ts +0 -97
  122. package/test/types/register.test-d.ts +0 -237
  123. package/test/types/reply.test-d.ts +0 -254
  124. package/test/types/request.test-d.ts +0 -188
  125. package/test/types/route.test-d.ts +0 -553
  126. package/test/types/using.test-d.ts +0 -17
package/lib/reply.js CHANGED
@@ -16,7 +16,6 @@ const {
16
16
  kReplyHasStatusCode,
17
17
  kReplyIsRunningOnErrorHook,
18
18
  kReplyNextErrorHandler,
19
- kDisableRequestLogging,
20
19
  kSchemaResponse,
21
20
  kReplyCacheSerializeFns,
22
21
  kSchemaController,
@@ -24,7 +23,8 @@ const {
24
23
  kRouteContext,
25
24
  kTimeoutTimer,
26
25
  kOnAbort,
27
- kRequestSignal
26
+ kRequestSignal,
27
+ kLogController
28
28
  } = require('./symbols.js')
29
29
  const {
30
30
  onSendHookRunner,
@@ -58,8 +58,10 @@ const {
58
58
  FST_ERR_DEC_UNDECLARED
59
59
  } = require('./errors')
60
60
  const decorators = require('./decorate')
61
+ const ContentType = require('./content-type.js')
61
62
 
62
63
  const toString = Object.prototype.toString
64
+ const HTTP2_WRITE_CHUNK_SIZE = 64 * 1024
63
65
 
64
66
  function Reply (res, request, log) {
65
67
  this.raw = res
@@ -75,6 +77,7 @@ function Reply (res, request, log) {
75
77
  this.log = log
76
78
  }
77
79
  Reply.props = []
80
+ Reply.instanceProperties = new Set(['raw', 'request', 'log'])
78
81
 
79
82
  Object.defineProperties(Reply.prototype, {
80
83
  [kRouteContext]: {
@@ -133,6 +136,14 @@ Reply.prototype.hijack = function () {
133
136
  this.request[kOnAbort] = null
134
137
  }
135
138
  }
139
+ // Clear socket._meta so hijacked replies (e.g. WebSocket upgrades) do not
140
+ // retain request/reply objects for the lifetime of the keep-alive socket
141
+ // when an onTimeout hook is registered.
142
+ const socket = this.request.raw.socket
143
+ if (socket?._meta?.request === this.request) {
144
+ socket.removeListener('timeout', socket._meta.onTimeout)
145
+ socket._meta = null
146
+ }
136
147
  return this
137
148
  }
138
149
 
@@ -157,8 +168,8 @@ Reply.prototype.send = function (payload) {
157
168
  return this
158
169
  }
159
170
 
160
- const contentType = this.getHeader('content-type')
161
- const hasContentType = contentType !== undefined
171
+ const contentType = ContentType.from(this.getHeader('content-type'))
172
+ const hasContentType = contentType.isEmpty === false
162
173
 
163
174
  if (payload !== null) {
164
175
  if (
@@ -167,7 +178,7 @@ Reply.prototype.send = function (payload) {
167
178
  // node:stream/web
168
179
  typeof payload.getReader === 'function' ||
169
180
  // Response
170
- toString.call(payload) === '[object Response]'
181
+ (typeof payload === 'object' && toString.call(payload) === '[object Response]')
171
182
  ) {
172
183
  onSendHook(this, payload)
173
184
  return this
@@ -199,18 +210,17 @@ Reply.prototype.send = function (payload) {
199
210
  payload = this[kReplySerializer](payload)
200
211
 
201
212
  // The indexOf below also matches custom json mimetypes such as 'application/hal+json' or 'application/ld+json'
202
- } else if (!hasContentType || contentType.indexOf('json') !== -1) {
213
+ } else if (!hasContentType || contentType.mediaType.indexOf('json') !== -1) {
203
214
  if (!hasContentType) {
204
215
  this[kReplyHeaders]['content-type'] = CONTENT_TYPE.JSON
205
- } else if (contentType.indexOf('charset') === -1) {
216
+ } else if (contentType.parameters.has('charset') === false) {
217
+ // The lookup is case-insensitive because HTTP parameter names are
218
+ // case-insensitive (RFC 9110 section 5.6.6).
206
219
  // If user doesn't set charset, we will set charset to utf-8
207
- const customContentType = contentType.trim()
208
- if (customContentType.endsWith(';')) {
209
- // custom content-type is ended with ';'
210
- this[kReplyHeaders]['content-type'] = `${customContentType} charset=utf-8`
211
- } else {
212
- this[kReplyHeaders]['content-type'] = `${customContentType}; charset=utf-8`
213
- }
220
+ const customContentType = contentType.toString()
221
+ // ContentType.toString already handle the trailing ;
222
+ // We can safely append the charset parameter to the end of the string
223
+ this[kReplyHeaders]['content-type'] = `${customContentType}; charset=utf-8`
214
224
  }
215
225
 
216
226
  if (typeof payload !== 'string') {
@@ -593,7 +603,7 @@ function onSendEnd (reply, payload) {
593
603
  // since Response contain status code, headers and body,
594
604
  // we need to update the status, add the headers and use it's body as payload
595
605
  // before continuing
596
- if (toString.call(payload) === '[object Response]') {
606
+ if (payload != null && typeof payload === 'object' && toString.call(payload) === '[object Response]') {
597
607
  // https://developer.mozilla.org/en-US/docs/Web/API/Response/status
598
608
  if (typeof payload.status === 'number') {
599
609
  reply.code(payload.status)
@@ -677,19 +687,62 @@ function onSendEnd (reply, payload) {
677
687
 
678
688
  safeWriteHead(reply, statusCode)
679
689
  // write payload first
680
- res.write(payload)
681
- // then send trailers
682
- sendTrailer(payload, res, reply)
690
+ writePayload(payload, res, reply)
691
+ }
692
+
693
+ function isHttp2Reply (reply) {
694
+ return reply.request.raw?.httpVersionMajor === 2
683
695
  }
684
696
 
685
- function logStreamError (logger, err, res) {
686
- if (err.code === 'ERR_STREAM_PREMATURE_CLOSE') {
687
- if (!logger[kDisableRequestLogging]) {
688
- logger.info({ res }, 'stream closed prematurely')
697
+ function writePayload (payload, res, reply) {
698
+ if (!isHttp2Reply(reply) || Buffer.byteLength(payload) <= HTTP2_WRITE_CHUNK_SIZE) {
699
+ if (reply[kReplyTrailers] === null) {
700
+ // end(payload) corks the socket, so headers, payload and the
701
+ // finishing chunk are flushed with a single write
702
+ res.end(payload, null, null) // avoid ArgumentsAdaptorTrampoline from V8
703
+ return
689
704
  }
690
- } else {
691
- logger.warn({ err }, 'response terminated with an error with headers already sent')
705
+ res.write(payload)
706
+ // then send trailers
707
+ sendTrailer(payload, res, reply)
708
+ return
692
709
  }
710
+
711
+ writeHttp2Payload(payload, res, () => {
712
+ // then send trailers
713
+ sendTrailer(payload, res, reply)
714
+ })
715
+ }
716
+
717
+ function writeHttp2Payload (payload, res, done) {
718
+ const buffer = Buffer.isBuffer(payload) ? payload : Buffer.from(payload)
719
+ let offset = 0
720
+
721
+ function writeChunk () {
722
+ while (offset < buffer.length) {
723
+ /* c8 ignore next 3 - defensive guard for aborted responses */
724
+ if (res.destroyed || res.writableEnded) {
725
+ return
726
+ }
727
+
728
+ const end = Math.min(offset + HTTP2_WRITE_CHUNK_SIZE, buffer.length)
729
+ const shouldContinue = res.write(buffer.subarray(offset, end))
730
+ offset = end
731
+
732
+ if (shouldContinue === false) {
733
+ res.once('drain', writeChunk)
734
+ return
735
+ }
736
+ }
737
+
738
+ done()
739
+ }
740
+
741
+ writeChunk()
742
+ }
743
+
744
+ function logStreamError (err, reply, res) {
745
+ reply.server[kLogController].streamError(err, reply.request, reply)
693
746
  }
694
747
 
695
748
  function sendWebStream (payload, res, reply) {
@@ -706,7 +759,7 @@ function sendWebStream (payload, res, reply) {
706
759
  if (sourceOpen) {
707
760
  if (err != null && res.headersSent && !errorLogged) {
708
761
  errorLogged = true
709
- logStreamError(reply.log, err, res)
762
+ logStreamError(err, reply, res)
710
763
  }
711
764
  reader.cancel().catch(noop)
712
765
  }
@@ -754,7 +807,7 @@ function sendWebStream (payload, res, reply) {
754
807
  if (res.headersSent || reply.request.raw.aborted === true) {
755
808
  if (!errorLogged) {
756
809
  errorLogged = true
757
- logStreamError(reply.log, err, reply)
810
+ logStreamError(err, reply, reply)
758
811
  }
759
812
  res.destroy()
760
813
  } else {
@@ -778,7 +831,7 @@ function sendStream (payload, res, reply) {
778
831
  if (res.headersSent || reply.request.raw.aborted === true) {
779
832
  if (!errorLogged) {
780
833
  errorLogged = true
781
- logStreamError(reply.log, err, reply)
834
+ logStreamError(err, reply, reply)
782
835
  }
783
836
  res.destroy()
784
837
  } else {
@@ -792,7 +845,7 @@ function sendStream (payload, res, reply) {
792
845
  if (sourceOpen) {
793
846
  if (err != null && res.headersSent && !errorLogged) {
794
847
  errorLogged = true
795
- logStreamError(reply.log, err, res)
848
+ logStreamError(err, reply, res)
796
849
  }
797
850
  if (typeof payload.destroy === 'function') {
798
851
  payload.destroy()
@@ -830,10 +883,12 @@ function sendTrailer (payload, res, reply) {
830
883
  const trailers = {}
831
884
  let handled = 0
832
885
  let skipped = true
886
+ let sent = false
833
887
  function send () {
834
888
  // add trailers when all handler handled
835
889
  /* istanbul ignore else */
836
- if (handled === 0) {
890
+ if (handled === 0 && !sent) {
891
+ sent = true
837
892
  res.addTrailers(trailers)
838
893
  // we need to properly close the stream
839
894
  // after trailers sent
@@ -846,9 +901,10 @@ function sendTrailer (payload, res, reply) {
846
901
  skipped = false
847
902
  handled--
848
903
 
904
+ let cbAlreadyCalled = false
849
905
  function cb (err, value) {
850
- // TODO: we may protect multiple callback calls
851
- // or mixing async-await with callback
906
+ if (cbAlreadyCalled) return
907
+ cbAlreadyCalled = true
852
908
  handled++
853
909
 
854
910
  // we can safely ignore error for trailer
@@ -914,6 +970,14 @@ function setupResponseListeners (reply) {
914
970
  }
915
971
  }
916
972
 
973
+ // Fix: release socket._meta so request/reply objects are not retained
974
+ // past the response on keep-alive connections.
975
+ const socket = reply.request.raw.socket
976
+ if (socket && socket._meta && socket._meta.request === reply.request) {
977
+ socket.removeListener('timeout', socket._meta.onTimeout)
978
+ socket._meta = null
979
+ }
980
+
917
981
  if (ctx && ctx.onResponse !== null) {
918
982
  onResponseHookRunner(
919
983
  ctx.onResponse,
@@ -931,25 +995,7 @@ function setupResponseListeners (reply) {
931
995
  }
932
996
 
933
997
  function onResponseCallback (err, request, reply) {
934
- if (reply.log[kDisableRequestLogging]) {
935
- return
936
- }
937
-
938
- const responseTime = reply.elapsedTime
939
-
940
- if (err != null) {
941
- reply.log.error({
942
- res: reply,
943
- err,
944
- responseTime
945
- }, 'request errored')
946
- return
947
- }
948
-
949
- reply.log.info({
950
- res: reply,
951
- responseTime
952
- }, 'request completed')
998
+ reply.server[kLogController].requestCompleted(err, request, reply)
953
999
  }
954
1000
 
955
1001
  function buildReply (R) {
@@ -1,5 +1,7 @@
1
1
  'use strict'
2
2
 
3
+ const { kGenReqId } = require('./symbols.js')
4
+
3
5
  /**
4
6
  * @callback GenerateRequestId
5
7
  * @param {Object} req
@@ -22,7 +24,8 @@ function reqIdGenFactory (requestIdHeader, optGenReqId) {
22
24
  }
23
25
 
24
26
  function getGenReqId (contextServer, req) {
25
- return contextServer.genReqId(req)
27
+ // direct symbol access skips the `genReqId` getter indirection
28
+ return contextServer[kGenReqId](req)
26
29
  }
27
30
 
28
31
  function buildDefaultGenReqId () {
package/lib/request.js CHANGED
@@ -10,6 +10,7 @@ const {
10
10
  kSchemaController,
11
11
  kOptions,
12
12
  kRequestCacheValidateFns,
13
+ kRequestContentType,
13
14
  kRouteContext,
14
15
  kRequestOriginalUrl,
15
16
  kRequestSignal,
@@ -17,6 +18,7 @@ const {
17
18
  } = require('./symbols')
18
19
  const { FST_ERR_REQ_INVALID_VALIDATION_INVOCATION, FST_ERR_DEC_UNDECLARED } = require('./errors')
19
20
  const decorators = require('./decorate')
21
+ const ContentType = require('./content-type')
20
22
 
21
23
  const HTTP_PART_SYMBOL_MAP = {
22
24
  body: kSchemaBody,
@@ -36,6 +38,7 @@ function Request (id, params, req, query, log, context) {
36
38
  this.body = undefined
37
39
  }
38
40
  Request.props = []
41
+ Request.instanceProperties = new Set(['id', 'params', 'raw', 'query', 'log', 'body'])
39
42
 
40
43
  function getTrustProxyFn (tp) {
41
44
  if (typeof tp === 'function') {
@@ -117,8 +120,8 @@ function buildRequestWithTrustProxy (R, trustProxy) {
117
120
  },
118
121
  host: {
119
122
  get () {
120
- const socketAddr = this.raw.socket?.remoteAddress
121
- if (this.headers['x-forwarded-host'] && socketAddr !== null && proxyFn(socketAddr, 0)) {
123
+ const socket = this.raw.socket
124
+ if (this.headers['x-forwarded-host'] && socket != null && proxyFn(socket.remoteAddress, 0)) {
122
125
  return getLastEntryInMultiHeaderValue(this.headers['x-forwarded-host'])
123
126
  }
124
127
  /**
@@ -132,8 +135,8 @@ function buildRequestWithTrustProxy (R, trustProxy) {
132
135
  },
133
136
  protocol: {
134
137
  get () {
135
- const socketAddr = this.raw.socket?.remoteAddress
136
- if (this.headers['x-forwarded-proto'] && socketAddr !== null && proxyFn(socketAddr, 0)) {
138
+ const socket = this.raw.socket
139
+ if (this.headers['x-forwarded-proto'] && socket != null && proxyFn(socket.remoteAddress, 0)) {
137
140
  return getLastEntryInMultiHeaderValue(this.headers['x-forwarded-proto'])
138
141
  }
139
142
  if (this.socket) {
@@ -256,8 +259,7 @@ Object.defineProperties(Request.prototype, {
256
259
  port: {
257
260
  get () {
258
261
  const portReg = /(?<port>:\d+)$/
259
- const host = this.headers.host ?? this.headers[':authority'] ?? ''
260
- const matches = portReg.exec(host)
262
+ const matches = portReg.exec(this.host)
261
263
  if (matches === null || matches[1] === undefined) {
262
264
  return null
263
265
  }
@@ -282,6 +284,14 @@ Object.defineProperties(Request.prototype, {
282
284
  this.additionalHeaders = headers
283
285
  }
284
286
  },
287
+ mediaType: {
288
+ get () {
289
+ if (!this[kRequestContentType] && this.headers['content-type'] !== undefined) {
290
+ this[kRequestContentType] = ContentType.from(this.headers['content-type'])
291
+ }
292
+ return this[kRequestContentType]?.mediaType
293
+ }
294
+ },
285
295
  getValidationFunction: {
286
296
  value: function (httpPartOrSchema) {
287
297
  if (typeof httpPartOrSchema === 'string') {
package/lib/route.js CHANGED
@@ -24,6 +24,7 @@ const {
24
24
  FST_ERR_ROUTE_MISSING_HANDLER,
25
25
  FST_ERR_ROUTE_METHOD_NOT_SUPPORTED,
26
26
  FST_ERR_ROUTE_METHOD_INVALID,
27
+ FST_ERR_ROUTE_LOG_LEVEL_INVALID,
27
28
  FST_ERR_ROUTE_BODY_VALIDATION_SCHEMA_NOT_SUPPORTED,
28
29
  FST_ERR_ROUTE_BODY_LIMIT_OPTION_NOT_INT,
29
30
  FST_ERR_ROUTE_HANDLER_TIMEOUT_OPTION_NOT_INT,
@@ -42,7 +43,6 @@ const {
42
43
  kReplySerializerDefault,
43
44
  kReplyIsError,
44
45
  kRequestPayloadStream,
45
- kDisableRequestLogging,
46
46
  kSchemaErrorFormatter,
47
47
  kErrorHandler,
48
48
  kHasBeenDecorated,
@@ -51,10 +51,11 @@ const {
51
51
  kRouteContext,
52
52
  kRequestSignal,
53
53
  kTimeoutTimer,
54
- kOnAbort
54
+ kOnAbort,
55
+ kLogController
55
56
  } = require('./symbols.js')
56
57
  const { buildErrorHandler } = require('./error-handler')
57
- const { createChildLogger } = require('./logger-factory.js')
58
+ const { createChildLogger, defaultChildLoggerFactory } = require('./logger-factory.js')
58
59
  const { getGenReqId } = require('./req-id-gen-factory.js')
59
60
  const { FSTDEP022 } = require('./warnings')
60
61
 
@@ -68,6 +69,7 @@ const routerKeys = [
68
69
  'ignoreTrailingSlash',
69
70
  'maxParamLength',
70
71
  'onBadUrl',
72
+ 'onMaxParamLength',
71
73
  'querystringParser',
72
74
  'useSemicolonDelimiter'
73
75
  ]
@@ -81,8 +83,6 @@ function buildRouting (options) {
81
83
  let hasLogger
82
84
  let setupResponseListeners
83
85
  let throwIfAlreadyStarted
84
- let disableRequestLogging
85
- let disableRequestLoggingFn
86
86
  let ignoreTrailingSlash
87
87
  let ignoreDuplicateSlashes
88
88
  let return503OnClosing
@@ -105,10 +105,6 @@ function buildRouting (options) {
105
105
  throwIfAlreadyStarted = fastifyArgs.throwIfAlreadyStarted
106
106
 
107
107
  globalExposeHeadRoutes = options.exposeHeadRoutes
108
- disableRequestLogging = options.disableRequestLogging
109
- if (typeof disableRequestLogging === 'function') {
110
- disableRequestLoggingFn = options.disableRequestLogging
111
- }
112
108
 
113
109
  ignoreTrailingSlash = options.routerOptions.ignoreTrailingSlash
114
110
  ignoreDuplicateSlashes = options.routerOptions.ignoreDuplicateSlashes
@@ -284,6 +280,7 @@ function buildRouting (options) {
284
280
  opts.routePath = path
285
281
  opts.prefix = prefix
286
282
  opts.logLevel = opts.logLevel || this[kLogLevel]
283
+ validateLogLevelOption(opts.logLevel, opts.method, opts.url, logger)
287
284
 
288
285
  if (this[kLogSerializers] || opts.logSerializers) {
289
286
  opts.logSerializers = Object.assign(Object.create(this[kLogSerializers]), opts.logSerializers)
@@ -365,7 +362,8 @@ function buildRouting (options) {
365
362
  // any route insertion error created by fastify can be safely ignore
366
363
  // because it only duplicate route for head
367
364
  if (!context[kRouteByFastify]) {
368
- const isDuplicatedRoute = error.message.includes(`Method '${opts.method}' already declared for route`)
365
+ const methods = Array.isArray(opts.method) ? opts.method : [opts.method]
366
+ const isDuplicatedRoute = methods.some(method => error.message.includes(`Method '${method}' already declared for route`))
369
367
  if (isDuplicatedRoute) {
370
368
  throw new FST_ERR_DUPLICATED_ROUTE(opts.method, opts.url)
371
369
  }
@@ -459,16 +457,21 @@ function buildRouting (options) {
459
457
  function routeHandler (req, res, params, context, query) {
460
458
  const id = getGenReqId(context.server, req)
461
459
 
462
- const loggerOpts = {
463
- level: context.logLevel
464
- }
460
+ let childLogger
461
+ if (hasLogger === false && context.childLoggerFactory === defaultChildLoggerFactory) {
462
+ // the null logger children are the logger itself, so we can skip
463
+ // the child logger creation altogether
464
+ childLogger = logger
465
+ } else {
466
+ const loggerOpts = {
467
+ level: context.logLevel
468
+ }
465
469
 
466
- if (context.logSerializers) {
467
- loggerOpts.serializers = context.logSerializers
470
+ if (context.logSerializers) {
471
+ loggerOpts.serializers = context.logSerializers
472
+ }
473
+ childLogger = createChildLogger(context, logger, req, id, loggerOpts)
468
474
  }
469
- const childLogger = createChildLogger(context, logger, req, id, loggerOpts)
470
- // Set initial value; will be re-evaluated after FastifyRequest is constructed if it's a function
471
- childLogger[kDisableRequestLogging] = disableRequestLoggingFn ? false : disableRequestLogging
472
475
 
473
476
  if (closing === true) {
474
477
  /* istanbul ignore next mac, windows */
@@ -476,19 +479,22 @@ function buildRouting (options) {
476
479
  res.setHeader('Connection', 'close')
477
480
  }
478
481
 
479
- // TODO remove return503OnClosing after Node v18 goes EOL
480
- /* istanbul ignore else */
482
+ // Load-shedding fast path during drain. server.close() and
483
+ // closeIdleConnections() only reap idle sockets; requests already
484
+ // pipelined or arriving on an active keep-alive connection still reach
485
+ // this point with closing === true. Short-circuiting with 503 avoids
486
+ // running the full handler chain (and any downstream calls) for work
487
+ // the load balancer should redirect elsewhere.
481
488
  if (return503OnClosing) {
482
- // On Node v19 we cannot test this behavior as it won't be necessary
483
- // anymore. It will close all the idle connections before they reach this
484
- // stage.
485
489
  const headers = {
486
490
  'Content-Type': 'application/json',
487
491
  'Content-Length': '80'
488
492
  }
489
493
  res.writeHead(503, headers)
490
494
  res.end('{"error":"Service Unavailable","message":"Service Unavailable","statusCode":503}')
491
- childLogger.info({ res: { statusCode: 503 } }, 'request aborted - refusing to accept new requests as server is closing')
495
+
496
+ context.server[kLogController].serviceUnavailable(childLogger, context.server)
497
+
492
498
  return
493
499
  }
494
500
  }
@@ -513,16 +519,7 @@ function buildRouting (options) {
513
519
  const request = new context.Request(id, params, req, query, childLogger, context)
514
520
  const reply = new context.Reply(res, request, childLogger)
515
521
 
516
- // Evaluate disableRequestLogging after FastifyRequest is constructed
517
- // so the caller has access to decorations and customizations
518
- const resolvedDisableRequestLogging = disableRequestLoggingFn
519
- ? disableRequestLoggingFn(request)
520
- : disableRequestLogging
521
- childLogger[kDisableRequestLogging] = resolvedDisableRequestLogging
522
-
523
- if (resolvedDisableRequestLogging === false) {
524
- childLogger.info({ req: request }, 'incoming request')
525
- }
522
+ context.server[kLogController].incomingRequest(request, reply)
526
523
 
527
524
  // Handler timeout setup — only when configured (zero overhead otherwise)
528
525
  const handlerTimeout = context.handlerTimeout
@@ -553,6 +550,13 @@ function buildRouting (options) {
553
550
  setupResponseListeners(reply)
554
551
  }
555
552
 
553
+ if (context.onTimeout !== null) {
554
+ if (!request.raw.socket._meta) {
555
+ request.raw.socket.on('timeout', handleTimeout)
556
+ }
557
+ request.raw.socket._meta = { context, request, reply, onTimeout: handleTimeout }
558
+ }
559
+
556
560
  if (context.onRequest !== null) {
557
561
  onRequestHookRunner(
558
562
  context.onRequest,
@@ -576,13 +580,6 @@ function buildRouting (options) {
576
580
  }
577
581
  })
578
582
  }
579
-
580
- if (context.onTimeout !== null) {
581
- if (!request.raw.socket._meta) {
582
- request.raw.socket.on('timeout', handleTimeout)
583
- }
584
- request.raw.socket._meta = { context, request, reply }
585
- }
586
583
  }
587
584
  }
588
585
 
@@ -635,6 +632,15 @@ function validateHandlerTimeoutOption (handlerTimeout) {
635
632
  }
636
633
  }
637
634
 
635
+ function validateLogLevelOption (logLevel, method, path, logger) {
636
+ if (logLevel == null || logLevel === '') return
637
+ if (logger?.levels?.values == null) return
638
+
639
+ if (typeof logLevel !== 'string' || logger.levels.values[logLevel] === undefined) {
640
+ throw new FST_ERR_ROUTE_LOG_LEVEL_INVALID(method, path, logLevel)
641
+ }
642
+ }
643
+
638
644
  function runPreParsing (err, request, reply) {
639
645
  if (reply.sent === true) return
640
646
  if (err != null) {
@@ -31,7 +31,7 @@ function buildSchemaController (parentSchemaCtrl, opts) {
31
31
  bucket: (opts && opts.bucket) || buildSchemas,
32
32
  compilersFactory,
33
33
  isCustomValidatorCompiler: typeof opts?.compilersFactory?.buildValidator === 'function',
34
- isCustomSerializerCompiler: typeof opts?.compilersFactory?.buildValidator === 'function'
34
+ isCustomSerializerCompiler: typeof opts?.compilersFactory?.buildSerializer === 'function'
35
35
  }
36
36
 
37
37
  return new SchemaController(undefined, option)
package/lib/schemas.js CHANGED
@@ -10,6 +10,7 @@ const {
10
10
  FST_ERR_SCH_DUPLICATE,
11
11
  FST_ERR_SCH_CONTENT_MISSING_SCHEMA
12
12
  } = require('./errors')
13
+ const ContentType = require('./content-type')
13
14
 
14
15
  const SCHEMAS_SOURCE = ['params', 'body', 'querystring', 'query', 'headers']
15
16
 
@@ -147,52 +148,58 @@ function getSchemaSerializer (context, statusCode, contentType) {
147
148
  return false
148
149
  }
149
150
  if (responseSchemaDef[statusCode]) {
150
- if (responseSchemaDef[statusCode].constructor === Object && contentType) {
151
- const mediaName = contentType.split(';', 1)[0]
152
- if (responseSchemaDef[statusCode][mediaName]) {
153
- return responseSchemaDef[statusCode][mediaName]
154
- }
151
+ if (responseSchemaDef[statusCode].constructor === Object) {
152
+ const ct = new ContentType(contentType)
153
+ if (ct.isValid) {
154
+ if (responseSchemaDef[statusCode][ct.mediaType]) {
155
+ return responseSchemaDef[statusCode][ct.mediaType]
156
+ }
155
157
 
156
- // fallback to match all media-type
157
- if (responseSchemaDef[statusCode]['*/*']) {
158
- return responseSchemaDef[statusCode]['*/*']
159
- }
158
+ // fallback to match all media-type
159
+ if (responseSchemaDef[statusCode]['*/*']) {
160
+ return responseSchemaDef[statusCode]['*/*']
161
+ }
160
162
 
161
- return false
163
+ return false
164
+ }
162
165
  }
163
166
  return responseSchemaDef[statusCode]
164
167
  }
165
168
  const fallbackStatusCode = (statusCode + '')[0] + 'xx'
166
169
  if (responseSchemaDef[fallbackStatusCode]) {
167
- if (responseSchemaDef[fallbackStatusCode].constructor === Object && contentType) {
168
- const mediaName = contentType.split(';', 1)[0]
169
- if (responseSchemaDef[fallbackStatusCode][mediaName]) {
170
- return responseSchemaDef[fallbackStatusCode][mediaName]
171
- }
170
+ if (responseSchemaDef[fallbackStatusCode].constructor === Object) {
171
+ const ct = new ContentType(contentType)
172
+ if (ct.isValid) {
173
+ if (responseSchemaDef[fallbackStatusCode][ct.mediaType]) {
174
+ return responseSchemaDef[fallbackStatusCode][ct.mediaType]
175
+ }
172
176
 
173
- // fallback to match all media-type
174
- if (responseSchemaDef[fallbackStatusCode]['*/*']) {
175
- return responseSchemaDef[fallbackStatusCode]['*/*']
176
- }
177
+ // fallback to match all media-type
178
+ if (responseSchemaDef[fallbackStatusCode]['*/*']) {
179
+ return responseSchemaDef[fallbackStatusCode]['*/*']
180
+ }
177
181
 
178
- return false
182
+ return false
183
+ }
179
184
  }
180
185
 
181
186
  return responseSchemaDef[fallbackStatusCode]
182
187
  }
183
188
  if (responseSchemaDef.default) {
184
- if (responseSchemaDef.default.constructor === Object && contentType) {
185
- const mediaName = contentType.split(';', 1)[0]
186
- if (responseSchemaDef.default[mediaName]) {
187
- return responseSchemaDef.default[mediaName]
188
- }
189
+ if (responseSchemaDef.default.constructor === Object) {
190
+ const ct = new ContentType(contentType)
191
+ if (ct.isValid) {
192
+ if (responseSchemaDef.default[ct.mediaType]) {
193
+ return responseSchemaDef.default[ct.mediaType]
194
+ }
189
195
 
190
- // fallback to match all media-type
191
- if (responseSchemaDef.default['*/*']) {
192
- return responseSchemaDef.default['*/*']
193
- }
196
+ // fallback to match all media-type
197
+ if (responseSchemaDef.default['*/*']) {
198
+ return responseSchemaDef.default['*/*']
199
+ }
194
200
 
195
- return false
201
+ return false
202
+ }
196
203
  }
197
204
 
198
205
  return responseSchemaDef.default