fastify 5.9.0 → 5.11.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 (74) hide show
  1. package/PROJECT_CHARTER.md +1 -1
  2. package/README.md +7 -10
  3. package/SPONSORS.md +1 -0
  4. package/build/build-validation.js +2 -2
  5. package/docs/Guides/Delay-Accepting-Requests.md +1 -1
  6. package/docs/Guides/Ecosystem.md +12 -9
  7. package/docs/Guides/Getting-Started.md +10 -10
  8. package/docs/Guides/Migration-Guide-V4.md +2 -2
  9. package/docs/Guides/Migration-Guide-V5.md +1 -1
  10. package/docs/Guides/Plugins-Guide.md +1 -1
  11. package/docs/Guides/Prototype-Poisoning.md +3 -3
  12. package/docs/Guides/Serverless.md +6 -13
  13. package/docs/Guides/Style-Guide.md +1 -1
  14. package/docs/Reference/Errors.md +6 -0
  15. package/docs/Reference/Hooks.md +1 -1
  16. package/docs/Reference/Logging.md +3 -0
  17. package/docs/Reference/Reply.md +2 -2
  18. package/docs/Reference/Request.md +13 -13
  19. package/docs/Reference/Server.md +117 -17
  20. package/docs/Reference/Type-Providers.md +24 -4
  21. package/docs/Reference/TypeScript.md +8 -10
  22. package/docs/Reference/Warnings.md +5 -0
  23. package/eslint.config.js +1 -1
  24. package/fastify.d.ts +60 -13
  25. package/fastify.js +28 -23
  26. package/lib/content-type-parser.js +1 -11
  27. package/lib/content-type.js +70 -19
  28. package/lib/context.js +0 -3
  29. package/lib/error-handler.js +7 -26
  30. package/lib/errors.js +17 -0
  31. package/lib/four-oh-four.js +8 -10
  32. package/lib/handle-request.js +37 -8
  33. package/lib/hooks.js +5 -1
  34. package/lib/log-controller.js +169 -0
  35. package/lib/logger-factory.js +25 -4
  36. package/lib/reply.js +35 -44
  37. package/lib/req-id-gen-factory.js +4 -1
  38. package/lib/request.js +3 -3
  39. package/lib/route.js +28 -36
  40. package/lib/schemas.js +3 -3
  41. package/lib/symbols.js +1 -1
  42. package/lib/validation.js +10 -1
  43. package/lib/warnings.js +19 -1
  44. package/package.json +4 -4
  45. package/test/content-type.test.js +51 -4
  46. package/test/find-route.test.js +35 -0
  47. package/test/fix-6411.test.js +65 -0
  48. package/test/genReqId.test.js +24 -0
  49. package/test/hooks.test.js +71 -0
  50. package/test/internals/all.test.js +2 -1
  51. package/test/internals/errors.test.js +21 -1
  52. package/test/internals/logger.test.js +322 -0
  53. package/test/internals/reply.test.js +57 -4
  54. package/test/internals/request.test.js +10 -7
  55. package/test/logger/logging.test.js +40 -1
  56. package/test/reply-error.test.js +35 -0
  57. package/test/rfc-10008.test.js +147 -0
  58. package/test/route-shorthand.test.js +14 -4
  59. package/test/schema-serialization.test.js +33 -0
  60. package/test/stream.4.test.js +2 -2
  61. package/test/trust-proxy.test.js +49 -30
  62. package/test/types/errors.tst.ts +2 -0
  63. package/test/types/route.tst.ts +3 -3
  64. package/types/content-type-parser.d.ts +26 -6
  65. package/types/errors.d.ts +3 -0
  66. package/types/hooks.d.ts +137 -76
  67. package/types/instance.d.ts +152 -47
  68. package/types/logger.d.ts +57 -2
  69. package/types/plugin.d.ts +7 -3
  70. package/types/register.d.ts +53 -10
  71. package/types/reply.d.ts +62 -14
  72. package/types/route.d.ts +68 -34
  73. package/types/type-provider.d.ts +29 -8
  74. package/types/utils.d.ts +2 -2
package/fastify.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const VERSION = '5.9.0'
3
+ const VERSION = '5.11.0'
4
4
 
5
5
  const Avvio = require('avvio')
6
6
  const http = require('node:http')
@@ -33,7 +33,8 @@ const {
33
33
  kChildLoggerFactory,
34
34
  kGenReqId,
35
35
  kErrorHandlerAlreadySet,
36
- kHandlerTimeout
36
+ kHandlerTimeout,
37
+ kLogController
37
38
  } = require('./lib/symbols.js')
38
39
 
39
40
  const { createServer } = require('./lib/server')
@@ -44,7 +45,7 @@ const decorator = require('./lib/decorate')
44
45
  const ContentTypeParser = require('./lib/content-type-parser.js')
45
46
  const SchemaController = require('./lib/schema-controller')
46
47
  const { Hooks, hookRunnerApplication, supportedHooks } = require('./lib/hooks')
47
- const { createChildLogger, defaultChildLoggerFactory, createLogger } = require('./lib/logger-factory')
48
+ const { createChildLogger, defaultChildLoggerFactory, createLogger, createLogController, LogController } = require('./lib/logger-factory')
48
49
  const pluginUtils = require('./lib/plugin-utils.js')
49
50
  const { getGenReqId, reqIdGenFactory } = require('./lib/req-id-gen-factory.js')
50
51
  const { buildRouting, validateBodyLimitOption, buildRouterOptions } = require('./lib/route')
@@ -79,7 +80,7 @@ const {
79
80
  } = errorCodes
80
81
 
81
82
  const { buildErrorHandler } = require('./lib/error-handler.js')
82
- const { FSTWRN004 } = require('./lib/warnings.js')
83
+ const { FSTWRN004, FSTDEP023, FSTDEP024 } = require('./lib/warnings.js')
83
84
 
84
85
  const initChannel = diagnostics.channel('fastify.initialization')
85
86
 
@@ -90,7 +91,7 @@ function fastify (serverOptions) {
90
91
  const {
91
92
  options,
92
93
  genReqId,
93
- disableRequestLogging,
94
+ logController,
94
95
  hasLogger,
95
96
  initialConfig
96
97
  } = processOptions(serverOptions, defaultRoute, onBadUrl, onMaxParamLength)
@@ -142,7 +143,9 @@ function fastify (serverOptions) {
142
143
  'OPTIONS',
143
144
  'PATCH',
144
145
  'PUT',
145
- 'POST'
146
+ 'POST',
147
+ // RFC 10008
148
+ 'QUERY'
146
149
  ])
147
150
  },
148
151
  [kOptions]: options,
@@ -199,6 +202,9 @@ function fastify (serverOptions) {
199
202
  options: function _options (url, options, handler) {
200
203
  return router.prepareRoute.call(this, { method: 'OPTIONS', url, options, handler })
201
204
  },
205
+ query: function _query (url, options, handler) {
206
+ return router.prepareRoute.call(this, { method: 'QUERY', url, options, handler })
207
+ },
202
208
  all: function _all (url, options, handler) {
203
209
  return router.prepareRoute.call(this, { method: this.supportedMethods, url, options, handler })
204
210
  },
@@ -216,6 +222,7 @@ function fastify (serverOptions) {
216
222
  },
217
223
  // expose logger instance
218
224
  log: options.logger,
225
+ [kLogController]: logController,
219
226
  // type provider
220
227
  withTypeProvider,
221
228
  // hooks
@@ -643,10 +650,7 @@ function fastify (serverOptions) {
643
650
  const request = new Request(id, null, req, null, childLogger, routeEventContext)
644
651
  const reply = new Reply(res, request, childLogger)
645
652
 
646
- const resolvedDisableRequestLogging = typeof disableRequestLogging === 'function' ? disableRequestLogging(req) : disableRequestLogging
647
- if (resolvedDisableRequestLogging === false) {
648
- childLogger.info({ req: request }, 'incoming request')
649
- }
653
+ routeEventContext.server[kLogController].incomingRequest(request, reply)
650
654
 
651
655
  return options.frameworkErrors(new FST_ERR_BAD_URL(path), request, reply)
652
656
  }
@@ -671,10 +675,7 @@ function fastify (serverOptions) {
671
675
  const request = new Request(id, null, req, null, childLogger, routeEventContext)
672
676
  const reply = new Reply(res, request, childLogger)
673
677
 
674
- const resolvedDisableRequestLogging = typeof disableRequestLogging === 'function' ? disableRequestLogging(req) : disableRequestLogging
675
- if (resolvedDisableRequestLogging === false) {
676
- childLogger.info({ req: request }, 'incoming request')
677
- }
678
+ routeEventContext.server[kLogController].incomingRequest(request, reply)
678
679
 
679
680
  return options.frameworkErrors(new FST_ERR_MAX_PARAM_LENGTH(path), request, reply)
680
681
  }
@@ -702,10 +703,7 @@ function fastify (serverOptions) {
702
703
  const request = new Request(id, null, req, null, childLogger, routeEventContext)
703
704
  const reply = new Reply(res, request, childLogger)
704
705
 
705
- const resolvedDisableRequestLogging = typeof disableRequestLogging === 'function' ? disableRequestLogging(req) : disableRequestLogging
706
- if (resolvedDisableRequestLogging === false) {
707
- childLogger.info({ req: request }, 'incoming request')
708
- }
706
+ routeEventContext.server[kLogController].incomingRequest(request, reply)
709
707
 
710
708
  return options.frameworkErrors(new FST_ERR_ASYNC_CONSTRAINT(), request, reply)
711
709
  }
@@ -876,9 +874,13 @@ function processOptions (options, defaultRoute, onBadUrl, onMaxParamLength) {
876
874
 
877
875
  const requestIdHeader = typeof options.requestIdHeader === 'string' && options.requestIdHeader.length !== 0 ? options.requestIdHeader.toLowerCase() : (options.requestIdHeader === true && 'request-id')
878
876
  const genReqId = reqIdGenFactory(requestIdHeader, options.genReqId)
879
- const requestIdLogLabel = options.requestIdLogLabel || 'reqId'
877
+ if (options.requestIdLogLabel !== undefined) {
878
+ FSTDEP024()
879
+ }
880
880
  options.bodyLimit = options.bodyLimit || defaultInitOptions.bodyLimit
881
- const disableRequestLogging = options.disableRequestLogging || false
881
+ if (options.disableRequestLogging !== undefined) {
882
+ FSTDEP023()
883
+ }
882
884
 
883
885
  const ajvOptions = Object.assign({
884
886
  customOptions: {},
@@ -894,6 +896,10 @@ function processOptions (options, defaultRoute, onBadUrl, onMaxParamLength) {
894
896
 
895
897
  const { logger, hasLogger } = createLogger(options)
896
898
 
899
+ // the internal logger uses the input logger to execute the logging. This allows the user
900
+ // to customize every internal log line
901
+ const logController = createLogController(options)
902
+
897
903
  // Update the options with the fixed values
898
904
  options.connectionTimeout = options.connectionTimeout || defaultInitOptions.connectionTimeout
899
905
  options.keepAliveTimeout = options.keepAliveTimeout || defaultInitOptions.keepAliveTimeout
@@ -901,8 +907,6 @@ function processOptions (options, defaultRoute, onBadUrl, onMaxParamLength) {
901
907
  options.requestTimeout = options.requestTimeout || defaultInitOptions.requestTimeout
902
908
  options.logger = logger
903
909
  options.requestIdHeader = requestIdHeader
904
- options.requestIdLogLabel = requestIdLogLabel
905
- options.disableRequestLogging = disableRequestLogging
906
910
  options.ajv = ajvOptions
907
911
  options.clientErrorHandler = options.clientErrorHandler || defaultClientErrorHandler
908
912
  options.allowErrorHandlerOverride = options.allowErrorHandlerOverride ?? defaultInitOptions.allowErrorHandlerOverride
@@ -930,7 +934,7 @@ function processOptions (options, defaultRoute, onBadUrl, onMaxParamLength) {
930
934
  return {
931
935
  options,
932
936
  genReqId,
933
- disableRequestLogging,
937
+ logController,
934
938
  hasLogger,
935
939
  initialConfig
936
940
  }
@@ -1010,5 +1014,6 @@ function validateSchemaErrorFormatter (schemaErrorFormatter) {
1010
1014
  */
1011
1015
  module.exports = fastify
1012
1016
  module.exports.errorCodes = errorCodes
1017
+ module.exports.LogController = LogController
1013
1018
  module.exports.fastify = fastify
1014
1019
  module.exports.default = fastify
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const { AsyncResource } = require('node:async_hooks')
4
- const { FifoMap: Fifo, LruMap: Lru } = require('toad-cache')
4
+ const { FifoMap: Fifo } = require('toad-cache')
5
5
  const { parse: secureJsonParse } = require('secure-json-parse')
6
6
  const ContentType = require('./content-type')
7
7
  const {
@@ -39,15 +39,6 @@ function ContentTypeParser (bodyLimit, onProtoPoisoning, onConstructorPoisoning)
39
39
  this.parserList = ['application/json', 'text/plain']
40
40
  this.parserRegExpList = []
41
41
  this.cache = new Fifo(100)
42
- this.ctCache = new Lru(100)
43
- }
44
-
45
- ContentTypeParser.prototype.getContentType = function (raw) {
46
- let ct = this.ctCache.get(raw)
47
- if (ct !== undefined) return ct
48
- ct = new ContentType(raw)
49
- this.ctCache.set(raw, ct)
50
- return ct
51
42
  }
52
43
 
53
44
  ContentTypeParser.prototype.add = function (contentType, opts, parserFn) {
@@ -169,7 +160,6 @@ ContentTypeParser.prototype.removeAll = function () {
169
160
  this.parserRegExpList = []
170
161
  this.parserList = []
171
162
  this.cache = new Fifo(100)
172
- this.ctCache = new Lru(100)
173
163
  }
174
164
 
175
165
  ContentTypeParser.prototype.remove = function (contentType) {
@@ -1,15 +1,39 @@
1
1
  'use strict'
2
2
 
3
+ const { LruMap: Lru } = require('toad-cache')
4
+
3
5
  /**
4
- * keyValuePairsReg is used to split the parameters list into associated
5
- * key value pairings. The leading `(?:^|;)\s*` anchor ensures the regex
6
- * only attempts matches at parameter boundaries, preventing quadratic
7
- * backtracking on malformed input.
6
+ * keyValuePairsReg matches a single `parameter-name "=" parameter-value`
7
+ * at a parameter boundary, following RFC 9110 §5.6.6 grammar:
8
+ *
9
+ * parameter = parameter-name "=" parameter-value
10
+ * parameter-name = token
11
+ * parameter-value = ( token / quoted-string )
12
+ * quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE
13
+ * qdtext = HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text
14
+ * quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
15
+ * obs-text = %x80-FF
16
+ *
17
+ * The value alternative accepts either a bare token or a full quoted-string
18
+ * whose content matches the qdtext / quoted-pair character ranges. Because
19
+ * the quoted-string branch is aware of `\"` as an escape, embedded `"` and
20
+ * `;` characters inside a quoted value do not terminate the value.
8
21
  *
9
22
  * @see https://httpwg.org/specs/rfc9110.html#parameter
10
23
  * @type {RegExp}
11
24
  */
12
- const keyValuePairsReg = /(?:^|;)\s*([\w!#$%&'*+.^`|~-]+)=([^;]*)/gm
25
+ const keyValuePairsReg = /(?:^|;)\s*([\w!#$%&'*+.^`|~-]+)=("(?:[\t\u0020\u0021\u0023-\u005b\u005d-\u007e\u0080-\u00ff]|\\[\t\u0020-\u00ff])*"|[\w!#$%&'*+.^`|~-]+)/gu
26
+
27
+ /**
28
+ * quotedPairReg matches a quoted-pair (a backslash followed by a single
29
+ * quoted-pair-allowed octet) inside a quoted-string value. Per RFC 9110
30
+ * §5.6.4, recipients that process the value of a quoted-string MUST handle
31
+ * a quoted-pair as if it were replaced by the octet following the backslash.
32
+ *
33
+ * @see https://httpwg.org/specs/rfc9110.html#quoted.string
34
+ * @type {RegExp}
35
+ */
36
+ const quotedPairReg = /\\([\t\u0020-\u00ff])/gu
13
37
 
14
38
  /**
15
39
  * typeNameReg is used to validate that the first part of the media-type
@@ -35,6 +59,12 @@ const typeNameReg = /^[\w!#$%&'*+.^`|~-]+$/
35
59
  */
36
60
  const subtypeNameReg = /^[\w!#$%&'*+.^`|~-]+\s*$/
37
61
 
62
+ /**
63
+ * Content-Type internal shared cache
64
+ * @type {Lru<ContentType>}
65
+ */
66
+ const cache = new Lru(100)
67
+
38
68
  /**
39
69
  * ContentType parses and represents the value of the content-type header.
40
70
  *
@@ -49,6 +79,27 @@ class ContentType {
49
79
  #parameters = new Map()
50
80
  #string
51
81
 
82
+ /**
83
+ * The shared cache of ContentType instances. The cache is used to avoid
84
+ * creating multiple instances of ContentType for the same header value.
85
+ * @type {Lru<ContentType>}
86
+ */
87
+ static get cache () { return cache }
88
+
89
+ /**
90
+ * Create a ContentType instance from a header value. If the value has been
91
+ * previously parsed, the cached instance will be returned.
92
+ * @param {string} headerValue
93
+ * @returns {ContentType | undefined}
94
+ */
95
+ static from (headerValue) {
96
+ let contentType = cache.get(headerValue)
97
+ if (contentType !== undefined) return contentType
98
+ contentType = new ContentType(headerValue)
99
+ cache.set(headerValue, contentType)
100
+ return contentType
101
+ }
102
+
52
103
  constructor (headerValue) {
53
104
  if (headerValue == null || headerValue === '' || headerValue === 'undefined') {
54
105
  return
@@ -106,22 +157,22 @@ class ContentType {
106
157
 
107
158
  let matches = keyValuePairsReg.exec(paramsList)
108
159
  while (matches) {
109
- const key = matches[1]
110
- const value = matches[2]
111
- if (value[0] === '"') {
112
- if (value.at(-1) !== '"') {
113
- this.#parameters.set(key, 'invalid quoted string')
114
- matches = keyValuePairsReg.exec(paramsList)
115
- continue
160
+ // https://httpwg.org/specs/rfc9110.html#parameter
161
+ // Parameter names are case-insensitive.
162
+ const key = matches[1].toLowerCase()
163
+ // Parameter values might or might not be case-sensitive,
164
+ // depending on the semantics of the parameter name.
165
+ let value = matches[2]
166
+ if (value.charCodeAt(0) === 0x22 /* " */) {
167
+ // Strip the surrounding DQUOTEs and unescape quoted-pairs per
168
+ // RFC 9110 §5.6.4. The regex guarantees the value starts and ends
169
+ // with `"` and only contains permitted qdtext / quoted-pair octets.
170
+ value = value.slice(1, -1)
171
+ if (value.indexOf('\\') !== -1) {
172
+ value = value.replace(quotedPairReg, '$1')
116
173
  }
117
- // We should probably verify the value matches a quoted string
118
- // (https://httpwg.org/specs/rfc9110.html#rule.quoted-string) value.
119
- // But we are not really doing much with the parameter values, so we
120
- // are omitting that at this time.
121
- this.#parameters.set(key, value.slice(1, value.length - 1))
122
- } else {
123
- this.#parameters.set(key, value)
124
174
  }
175
+ this.#parameters.set(key, value)
125
176
  matches = keyValuePairsReg.exec(paramsList)
126
177
  }
127
178
  }
package/lib/context.js CHANGED
@@ -6,7 +6,6 @@ const {
6
6
  kSchemaErrorFormatter,
7
7
  kErrorHandler,
8
8
  kChildLoggerFactory,
9
- kOptions,
10
9
  kReply,
11
10
  kRequest,
12
11
  kBodyLimit,
@@ -24,7 +23,6 @@ function Context ({
24
23
  schema,
25
24
  handler,
26
25
  config,
27
- requestIdLogLabel,
28
26
  childLoggerFactory,
29
27
  errorHandler,
30
28
  bodyLimit,
@@ -56,7 +54,6 @@ function Context ({
56
54
  this.onRequestAbort = null
57
55
  this.config = config
58
56
  this.errorHandler = errorHandler || server[kErrorHandler]
59
- this.requestIdLogLabel = requestIdLogLabel || server[kOptions].requestIdLogLabel
60
57
  this.childLoggerFactory = childLoggerFactory || server[kChildLoggerFactory]
61
58
  this._middie = null
62
59
  this._parserOptions = {
@@ -8,7 +8,7 @@ const {
8
8
  kReplyNextErrorHandler,
9
9
  kReplyIsRunningOnErrorHook,
10
10
  kRouteContext,
11
- kDisableRequestLogging,
11
+ kLogController,
12
12
  kDiagnosticsStore
13
13
  } = require('./symbols.js')
14
14
 
@@ -36,12 +36,7 @@ function handleError (reply, error, cb) {
36
36
  try {
37
37
  reply.raw.writeHead(reply.raw.statusCode, reply[kReplyHeaders])
38
38
  } catch (error) {
39
- if (!reply.log[kDisableRequestLogging]) {
40
- reply.log.warn(
41
- { req: reply.request, res: reply, err: error },
42
- error?.message
43
- )
44
- }
39
+ reply.server[kLogController].writeHeadError(error, reply.request, reply)
45
40
  reply.raw.writeHead(reply.raw.statusCode)
46
41
  }
47
42
  reply.raw.end(payload)
@@ -83,21 +78,7 @@ function handleError (reply, error, cb) {
83
78
  function defaultErrorHandler (error, request, reply) {
84
79
  setErrorHeaders(error, reply)
85
80
  setErrorStatusCode(reply, error)
86
- if (reply.statusCode < 500) {
87
- if (!reply.log[kDisableRequestLogging]) {
88
- reply.log.info(
89
- { res: reply, err: error },
90
- error?.message
91
- )
92
- }
93
- } else {
94
- if (!reply.log[kDisableRequestLogging]) {
95
- reply.log.error(
96
- { req: request, res: reply, err: error },
97
- error?.message
98
- )
99
- }
100
- }
81
+ request.server[kLogController].defaultErrorLog(error, request, reply)
101
82
  reply.send(error)
102
83
  }
103
84
 
@@ -123,10 +104,10 @@ function fallbackErrorHandler (error, reply, cb) {
123
104
  }))
124
105
  }
125
106
  } catch (err) {
126
- if (!reply.log[kDisableRequestLogging]) {
127
- // error is always FST_ERR_SCH_SERIALIZATION_BUILD because this is called from route/compileSchemasForSerialization
128
- reply.log.error({ err, statusCode: res.statusCode }, 'The serializer for the given status code failed')
129
- }
107
+ // error is always FST_ERR_SCH_SERIALIZATION_BUILD
108
+ // because this is called from route/compileSchemasForSerialization
109
+ reply.server[kLogController].serializerError(err, reply.request, reply, { statusCode: res.statusCode })
110
+
130
111
  reply.code(500)
131
112
  payload = serializeError(new FST_ERR_FAILED_ERROR_SERIALIZATION(err.message, error.message))
132
113
  }
package/lib/errors.js CHANGED
@@ -243,6 +243,13 @@ const codes = {
243
243
  TypeError
244
244
  ),
245
245
 
246
+ FST_ERR_LOG_INVALID_LOG_CONTROLLER: createError(
247
+ 'FST_ERR_LOG_INVALID_LOG_CONTROLLER',
248
+ "logController option must be an instance of LogController, got '%s'.",
249
+ 500,
250
+ TypeError
251
+ ),
252
+
246
253
  /**
247
254
  * reply
248
255
  */
@@ -448,6 +455,16 @@ const codes = {
448
455
  500,
449
456
  TypeError
450
457
  ),
458
+ FST_ERR_ROUTE_MISSING_CONTENT_TYPE: createError(
459
+ 'FST_ERR_ROUTE_MISSING_CONTENT_TYPE',
460
+ "Method '%s' must provide a 'Content-Type' header.",
461
+ 400
462
+ ),
463
+ FST_ERR_ROUTE_MISSING_CONTENT: createError(
464
+ 'FST_ERR_ROUTE_MISSING_CONTENT',
465
+ "Method '%s' must provide a request body.",
466
+ 400
467
+ ),
451
468
 
452
469
  /**
453
470
  * again listen when close server
@@ -11,7 +11,8 @@ const {
11
11
  kFourOhFourLevelInstance,
12
12
  kFourOhFourContext,
13
13
  kHooks,
14
- kErrorHandler
14
+ kErrorHandler,
15
+ kLogController
15
16
  } = require('./symbols.js')
16
17
  const { lifecycleHooks } = require('./hooks')
17
18
  const { buildErrorHandler } = require('./error-handler.js')
@@ -29,7 +30,7 @@ const { getGenReqId } = require('./req-id-gen-factory.js')
29
30
  * kFourOhFourContext: the context in the reply object where the handler will be executed
30
31
  */
31
32
  function fourOhFour (options) {
32
- const { logger, disableRequestLogging } = options
33
+ const { logger } = options
33
34
 
34
35
  // 404 router, used for handling encapsulated 404 handlers
35
36
  const router = FindMyWay({
@@ -52,14 +53,10 @@ function fourOhFour (options) {
52
53
  }
53
54
 
54
55
  function basic404 (request, reply) {
56
+ request.server[kLogController].routeNotFound(request, reply)
55
57
  const { url, method } = request.raw
56
- const message = `Route ${method}:${url} not found`
57
- const resolvedDisableRequestLogging = typeof disableRequestLogging === 'function' ? disableRequestLogging(request.raw) : disableRequestLogging
58
- if (!resolvedDisableRequestLogging) {
59
- request.log.info(message)
60
- }
61
58
  reply.code(404).send({
62
- message,
59
+ message: `Route ${method}:${url} not found`,
63
60
  error: 'Not Found',
64
61
  statusCode: 404
65
62
  })
@@ -181,11 +178,12 @@ function fourOhFour (options) {
181
178
  const id = getGenReqId(fourOhFourContext.server, req)
182
179
  const childLogger = createChildLogger(fourOhFourContext, logger, req, id)
183
180
 
184
- childLogger.info({ req }, 'incoming request')
185
-
186
181
  const request = new Request(id, null, req, null, childLogger, fourOhFourContext)
187
182
  const reply = new Reply(res, request, childLogger)
188
183
 
184
+ fourOhFourContext.server[kLogController].incomingRequest(request, reply)
185
+
186
+ // Since this could be a bug, we log it without checking if the logging is disabled
189
187
  request.log.warn('the default handler for 404 did not catch this, this is likely a fastify bug, please report it')
190
188
  request.log.warn(router.prettyPrint())
191
189
  reply.code(404).send(new FST_ERR_NOT_FOUND())
@@ -4,7 +4,11 @@ const diagnostics = require('node:diagnostics_channel')
4
4
  const wrapThenable = require('./wrap-thenable')
5
5
  const { validate: validateSchema } = require('./validation')
6
6
  const { preValidationHookRunner, preHandlerHookRunner } = require('./hooks')
7
- const { FST_ERR_CTP_INVALID_MEDIA_TYPE } = require('./errors')
7
+ const {
8
+ FST_ERR_CTP_INVALID_MEDIA_TYPE,
9
+ FST_ERR_ROUTE_MISSING_CONTENT_TYPE,
10
+ FST_ERR_ROUTE_MISSING_CONTENT
11
+ } = require('./errors')
8
12
  const { setErrorStatusCode } = require('./error-status')
9
13
  const {
10
14
  kReplyIsError,
@@ -14,6 +18,7 @@ const {
14
18
  kRequestContentType,
15
19
  kDiagnosticsStore
16
20
  } = require('./symbols')
21
+ const ContentType = require('./content-type')
17
22
 
18
23
  const channels = diagnostics.tracingChannel('fastify.request.handler')
19
24
 
@@ -36,13 +41,30 @@ function handleRequest (err, request, reply) {
36
41
  const headers = request.headers
37
42
  const ctHeader = headers['content-type']
38
43
 
39
- if (ctHeader === undefined) {
40
- const contentLength = headers['content-length']
41
- const transferEncoding = headers['transfer-encoding']
42
- const isEmptyBody = transferEncoding === undefined &&
43
- (contentLength === undefined || contentLength === '0')
44
+ if (method === 'QUERY') {
45
+ if (ctHeader === undefined) {
46
+ // https://datatracker.ietf.org/doc/html/rfc10008#section-2
47
+ // Servers MUST fail the request if the Content-Type request field
48
+ // ([HTTP], Section 8.3) is missing or is inconsistent with the
49
+ // request content.
50
+ reply[kReplyIsError] = true
51
+ reply.status(400).send(new FST_ERR_ROUTE_MISSING_CONTENT_TYPE(method))
52
+ return
53
+ }
44
54
 
45
- if (isEmptyBody) {
55
+ if (isEmptyBody(headers)) {
56
+ // https://datatracker.ietf.org/doc/html/rfc10008#section-2
57
+ // Servers MUST fail the request if the Content-Type request field
58
+ // ([HTTP], Section 8.3) is missing or is inconsistent with the
59
+ // request content.
60
+ reply[kReplyIsError] = true
61
+ reply.status(400).send(new FST_ERR_ROUTE_MISSING_CONTENT(method))
62
+ return
63
+ }
64
+ }
65
+
66
+ if (ctHeader === undefined) {
67
+ if (isEmptyBody(headers)) {
46
68
  // Request has no body to parse
47
69
  handler(request, reply)
48
70
  return
@@ -55,7 +77,7 @@ function handleRequest (err, request, reply) {
55
77
  // Conditional assignment to avoid creating a new ContentType instance
56
78
  // It can be assigned when accessing the .mediaType in hooks
57
79
  if (!request[kRequestContentType]) {
58
- request[kRequestContentType] = request[kRouteContext].contentTypeParser.getContentType(ctHeader)
80
+ request[kRequestContentType] = ContentType.from(ctHeader)
59
81
  }
60
82
  if (request[kRequestContentType].isValid === false) {
61
83
  reply[kReplyIsError] = true
@@ -87,6 +109,13 @@ function handler (request, reply) {
87
109
  }
88
110
  }
89
111
 
112
+ function isEmptyBody (headers) {
113
+ const contentLength = headers['content-length']
114
+ const transferEncoding = headers['transfer-encoding']
115
+ return transferEncoding === undefined &&
116
+ (contentLength === undefined || contentLength === '0')
117
+ }
118
+
90
119
  function preValidationCallback (err, request, reply) {
91
120
  if (reply.sent === true) return
92
121
 
package/lib/hooks.js CHANGED
@@ -306,7 +306,11 @@ function onSendHookRunner (functions, request, reply, payload, cb) {
306
306
  }
307
307
 
308
308
  function handleResolve (newPayload) {
309
- next(null, newPayload)
309
+ try {
310
+ next(null, newPayload)
311
+ } catch (err) {
312
+ cb(err, request, reply, payload)
313
+ }
310
314
  }
311
315
 
312
316
  function handleReject (err) {