fastify 5.9.0 → 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 (52) hide show
  1. package/PROJECT_CHARTER.md +1 -1
  2. package/README.md +7 -10
  3. package/build/build-validation.js +2 -2
  4. package/docs/Guides/Delay-Accepting-Requests.md +1 -1
  5. package/docs/Guides/Ecosystem.md +10 -7
  6. package/docs/Guides/Getting-Started.md +2 -2
  7. package/docs/Guides/Migration-Guide-V4.md +2 -2
  8. package/docs/Guides/Migration-Guide-V5.md +1 -1
  9. package/docs/Guides/Plugins-Guide.md +1 -1
  10. package/docs/Guides/Prototype-Poisoning.md +3 -3
  11. package/docs/Guides/Serverless.md +6 -13
  12. package/docs/Guides/Style-Guide.md +1 -1
  13. package/docs/Reference/Errors.md +2 -0
  14. package/docs/Reference/Hooks.md +1 -1
  15. package/docs/Reference/Logging.md +3 -0
  16. package/docs/Reference/Request.md +2 -2
  17. package/docs/Reference/Server.md +107 -8
  18. package/docs/Reference/Type-Providers.md +24 -4
  19. package/docs/Reference/TypeScript.md +3 -3
  20. package/docs/Reference/Warnings.md +4 -0
  21. package/fastify.d.ts +5 -0
  22. package/fastify.js +22 -22
  23. package/lib/content-type-parser.js +1 -11
  24. package/lib/content-type.js +34 -1
  25. package/lib/context.js +0 -3
  26. package/lib/error-handler.js +7 -26
  27. package/lib/errors.js +7 -0
  28. package/lib/four-oh-four.js +8 -10
  29. package/lib/handle-request.js +2 -1
  30. package/lib/log-controller.js +169 -0
  31. package/lib/logger-factory.js +25 -4
  32. package/lib/reply.js +35 -44
  33. package/lib/req-id-gen-factory.js +4 -1
  34. package/lib/request.js +3 -3
  35. package/lib/route.js +27 -35
  36. package/lib/symbols.js +1 -1
  37. package/lib/validation.js +9 -0
  38. package/lib/warnings.js +19 -1
  39. package/package.json +3 -3
  40. package/test/content-type.test.js +20 -0
  41. package/test/genReqId.test.js +24 -0
  42. package/test/hooks.test.js +71 -0
  43. package/test/internals/errors.test.js +1 -1
  44. package/test/internals/logger.test.js +322 -0
  45. package/test/internals/reply.test.js +35 -4
  46. package/test/internals/request.test.js +10 -7
  47. package/test/logger/logging.test.js +40 -1
  48. package/test/stream.4.test.js +2 -2
  49. package/test/trust-proxy.test.js +49 -30
  50. package/types/errors.d.ts +1 -0
  51. package/types/instance.d.ts +2 -0
  52. package/types/logger.d.ts +22 -0
@@ -10,6 +10,8 @@
10
10
  - [FSTWRN004](#FSTWRN004)
11
11
  - [Fastify Deprecation Codes](#fastify-deprecation-codes)
12
12
  - [FSTDEP022](#FSTDEP022)
13
+ - [FSTDEP023](#FSTDEP023)
14
+ - [FSTDEP024](#FSTDEP024)
13
15
 
14
16
  ## Warnings
15
17
 
@@ -57,3 +59,5 @@ Deprecation codes are supported by the Node.js CLI options:
57
59
  | Code | Description | How to solve | Discussion |
58
60
  | ---- | ----------- | ------------ | ---------- |
59
61
  | <a id="FSTDEP022">FSTDEP022</a> | You are trying to access the deprecated router options on top option properties. | Use `options.routerOptions`. | [#5985](https://github.com/fastify/fastify/pull/5985)
62
+ | <a id="FSTDEP023">FSTDEP023</a> | `disableRequestLogging` top-level option is deprecated. | Pass a `LogController` instance via the `logController` option with `disableRequestLogging` in its constructor instead. |
63
+ | <a id="FSTDEP024">FSTDEP024</a> | `requestIdLogLabel` top-level option is deprecated. | Pass a `LogController` instance via the `logController` option with `requestIdLogLabel` in its constructor instead. |
package/fastify.d.ts CHANGED
@@ -17,6 +17,7 @@ import { FastifyInstance, FastifyListenOptions, PrintRoutesOptions } from './typ
17
17
  import {
18
18
  FastifyBaseLogger,
19
19
  FastifyChildLoggerFactory,
20
+ LogController as LogControllerClass,
20
21
  FastifyLogFn,
21
22
  FastifyLoggerInstance,
22
23
  FastifyLoggerOptions,
@@ -44,6 +45,7 @@ type Fastify = typeof fastify
44
45
 
45
46
  declare namespace fastify {
46
47
  export const errorCodes: FastifyErrorCodes
48
+ export { LogControllerClass as LogController }
47
49
 
48
50
  export type FastifyHttp2SecureOptions<
49
51
  Server extends http2.Http2SecureServer,
@@ -126,7 +128,9 @@ declare namespace fastify {
126
128
  bodyLimit?: number,
127
129
  handlerTimeout?: number,
128
130
  maxParamLength?: number,
131
+ /** @deprecated Use the `logController` option with `disableRequestLogging` or `isLogDisabled` override instead. Will be removed in `fastify@6`. */
129
132
  disableRequestLogging?: boolean | ((req: FastifyRequest) => boolean),
133
+ logController?: LogControllerClass,
130
134
  exposeHeadRoutes?: boolean,
131
135
  onProtoPoisoning?: ProtoAction,
132
136
  onConstructorPoisoning?: ConstructorAction,
@@ -137,6 +141,7 @@ declare namespace fastify {
137
141
  caseSensitive?: boolean,
138
142
  allowUnsafeRegex?: boolean,
139
143
  requestIdHeader?: string | false,
144
+ /** @deprecated Use the `logController` option with `requestIdLogLabel` instead. Will be removed in `fastify@6`. */
140
145
  requestIdLogLabel?: string;
141
146
  useSemicolonDelimiter?: boolean,
142
147
  genReqId?: (req: RawRequestDefaultExpression<RawServer>) => string,
package/fastify.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const VERSION = '5.9.0'
3
+ const VERSION = '5.10.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)
@@ -216,6 +217,7 @@ function fastify (serverOptions) {
216
217
  },
217
218
  // expose logger instance
218
219
  log: options.logger,
220
+ [kLogController]: logController,
219
221
  // type provider
220
222
  withTypeProvider,
221
223
  // hooks
@@ -643,10 +645,7 @@ function fastify (serverOptions) {
643
645
  const request = new Request(id, null, req, null, childLogger, routeEventContext)
644
646
  const reply = new Reply(res, request, childLogger)
645
647
 
646
- const resolvedDisableRequestLogging = typeof disableRequestLogging === 'function' ? disableRequestLogging(req) : disableRequestLogging
647
- if (resolvedDisableRequestLogging === false) {
648
- childLogger.info({ req: request }, 'incoming request')
649
- }
648
+ routeEventContext.server[kLogController].incomingRequest(request, reply)
650
649
 
651
650
  return options.frameworkErrors(new FST_ERR_BAD_URL(path), request, reply)
652
651
  }
@@ -671,10 +670,7 @@ function fastify (serverOptions) {
671
670
  const request = new Request(id, null, req, null, childLogger, routeEventContext)
672
671
  const reply = new Reply(res, request, childLogger)
673
672
 
674
- const resolvedDisableRequestLogging = typeof disableRequestLogging === 'function' ? disableRequestLogging(req) : disableRequestLogging
675
- if (resolvedDisableRequestLogging === false) {
676
- childLogger.info({ req: request }, 'incoming request')
677
- }
673
+ routeEventContext.server[kLogController].incomingRequest(request, reply)
678
674
 
679
675
  return options.frameworkErrors(new FST_ERR_MAX_PARAM_LENGTH(path), request, reply)
680
676
  }
@@ -702,10 +698,7 @@ function fastify (serverOptions) {
702
698
  const request = new Request(id, null, req, null, childLogger, routeEventContext)
703
699
  const reply = new Reply(res, request, childLogger)
704
700
 
705
- const resolvedDisableRequestLogging = typeof disableRequestLogging === 'function' ? disableRequestLogging(req) : disableRequestLogging
706
- if (resolvedDisableRequestLogging === false) {
707
- childLogger.info({ req: request }, 'incoming request')
708
- }
701
+ routeEventContext.server[kLogController].incomingRequest(request, reply)
709
702
 
710
703
  return options.frameworkErrors(new FST_ERR_ASYNC_CONSTRAINT(), request, reply)
711
704
  }
@@ -876,9 +869,13 @@ function processOptions (options, defaultRoute, onBadUrl, onMaxParamLength) {
876
869
 
877
870
  const requestIdHeader = typeof options.requestIdHeader === 'string' && options.requestIdHeader.length !== 0 ? options.requestIdHeader.toLowerCase() : (options.requestIdHeader === true && 'request-id')
878
871
  const genReqId = reqIdGenFactory(requestIdHeader, options.genReqId)
879
- const requestIdLogLabel = options.requestIdLogLabel || 'reqId'
872
+ if (options.requestIdLogLabel !== undefined) {
873
+ FSTDEP024()
874
+ }
880
875
  options.bodyLimit = options.bodyLimit || defaultInitOptions.bodyLimit
881
- const disableRequestLogging = options.disableRequestLogging || false
876
+ if (options.disableRequestLogging !== undefined) {
877
+ FSTDEP023()
878
+ }
882
879
 
883
880
  const ajvOptions = Object.assign({
884
881
  customOptions: {},
@@ -894,6 +891,10 @@ function processOptions (options, defaultRoute, onBadUrl, onMaxParamLength) {
894
891
 
895
892
  const { logger, hasLogger } = createLogger(options)
896
893
 
894
+ // the internal logger uses the input logger to execute the logging. This allows the user
895
+ // to customize every internal log line
896
+ const logController = createLogController(options)
897
+
897
898
  // Update the options with the fixed values
898
899
  options.connectionTimeout = options.connectionTimeout || defaultInitOptions.connectionTimeout
899
900
  options.keepAliveTimeout = options.keepAliveTimeout || defaultInitOptions.keepAliveTimeout
@@ -901,8 +902,6 @@ function processOptions (options, defaultRoute, onBadUrl, onMaxParamLength) {
901
902
  options.requestTimeout = options.requestTimeout || defaultInitOptions.requestTimeout
902
903
  options.logger = logger
903
904
  options.requestIdHeader = requestIdHeader
904
- options.requestIdLogLabel = requestIdLogLabel
905
- options.disableRequestLogging = disableRequestLogging
906
905
  options.ajv = ajvOptions
907
906
  options.clientErrorHandler = options.clientErrorHandler || defaultClientErrorHandler
908
907
  options.allowErrorHandlerOverride = options.allowErrorHandlerOverride ?? defaultInitOptions.allowErrorHandlerOverride
@@ -930,7 +929,7 @@ function processOptions (options, defaultRoute, onBadUrl, onMaxParamLength) {
930
929
  return {
931
930
  options,
932
931
  genReqId,
933
- disableRequestLogging,
932
+ logController,
934
933
  hasLogger,
935
934
  initialConfig
936
935
  }
@@ -1010,5 +1009,6 @@ function validateSchemaErrorFormatter (schemaErrorFormatter) {
1010
1009
  */
1011
1010
  module.exports = fastify
1012
1011
  module.exports.errorCodes = errorCodes
1012
+ module.exports.LogController = LogController
1013
1013
  module.exports.fastify = fastify
1014
1014
  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,5 +1,7 @@
1
1
  'use strict'
2
2
 
3
+ const { LruMap: Lru } = require('toad-cache')
4
+
3
5
  /**
4
6
  * keyValuePairsReg is used to split the parameters list into associated
5
7
  * key value pairings. The leading `(?:^|;)\s*` anchor ensures the regex
@@ -35,6 +37,12 @@ const typeNameReg = /^[\w!#$%&'*+.^`|~-]+$/
35
37
  */
36
38
  const subtypeNameReg = /^[\w!#$%&'*+.^`|~-]+\s*$/
37
39
 
40
+ /**
41
+ * Content-Type internal shared cache
42
+ * @type {Lru<ContentType>}
43
+ */
44
+ const cache = new Lru(100)
45
+
38
46
  /**
39
47
  * ContentType parses and represents the value of the content-type header.
40
48
  *
@@ -49,6 +57,27 @@ class ContentType {
49
57
  #parameters = new Map()
50
58
  #string
51
59
 
60
+ /**
61
+ * The shared cache of ContentType instances. The cache is used to avoid
62
+ * creating multiple instances of ContentType for the same header value.
63
+ * @type {Lru<ContentType>}
64
+ */
65
+ static get cache () { return cache }
66
+
67
+ /**
68
+ * Create a ContentType instance from a header value. If the value has been
69
+ * previously parsed, the cached instance will be returned.
70
+ * @param {string} headerValue
71
+ * @returns {ContentType | undefined}
72
+ */
73
+ static from (headerValue) {
74
+ let contentType = cache.get(headerValue)
75
+ if (contentType !== undefined) return contentType
76
+ contentType = new ContentType(headerValue)
77
+ cache.set(headerValue, contentType)
78
+ return contentType
79
+ }
80
+
52
81
  constructor (headerValue) {
53
82
  if (headerValue == null || headerValue === '' || headerValue === 'undefined') {
54
83
  return
@@ -106,7 +135,11 @@ class ContentType {
106
135
 
107
136
  let matches = keyValuePairsReg.exec(paramsList)
108
137
  while (matches) {
109
- const key = matches[1]
138
+ // https://httpwg.org/specs/rfc9110.html#parameter
139
+ // Parameter names are case-insensitive.
140
+ const key = matches[1].toLowerCase()
141
+ // Parameter values might or might not be case-sensitive,
142
+ // depending on the semantics of the parameter name.
110
143
  const value = matches[2]
111
144
  if (value[0] === '"') {
112
145
  if (value.at(-1) !== '"') {
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
  */
@@ -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())
@@ -14,6 +14,7 @@ const {
14
14
  kRequestContentType,
15
15
  kDiagnosticsStore
16
16
  } = require('./symbols')
17
+ const ContentType = require('./content-type')
17
18
 
18
19
  const channels = diagnostics.tracingChannel('fastify.request.handler')
19
20
 
@@ -55,7 +56,7 @@ function handleRequest (err, request, reply) {
55
56
  // Conditional assignment to avoid creating a new ContentType instance
56
57
  // It can be assigned when accessing the .mediaType in hooks
57
58
  if (!request[kRequestContentType]) {
58
- request[kRequestContentType] = request[kRouteContext].contentTypeParser.getContentType(ctHeader)
59
+ request[kRequestContentType] = ContentType.from(ctHeader)
59
60
  }
60
61
  if (request[kRequestContentType].isValid === false) {
61
62
  reply[kReplyIsError] = true
@@ -0,0 +1,169 @@
1
+ 'use strict'
2
+
3
+ const { defaultInitOptions } = require('./initial-config-validation')
4
+
5
+ /**
6
+ * Internal logger layer that wraps Fastify's log calls with
7
+ * request-lifecycle awareness (disable-check, level selection).
8
+ * Keeps the surface intentionally small so hot paths stay fast.
9
+ *
10
+ * Users can extend this class to customize internal log lines.
11
+ */
12
+ class LogController {
13
+ /**
14
+ * @param {object} [options]
15
+ * @param {boolean | ((req: object) => boolean)} [options.disableRequestLogging=false]
16
+ * When `true` (or a function returning `true`), per-request log lines
17
+ * (incoming, completed, errors) are suppressed.
18
+ * @param {string} [options.requestIdLogLabel='reqId']
19
+ * The label used for the request identifier when logging the request.
20
+ */
21
+ constructor (options) {
22
+ const opts = options || {}
23
+ this.disableRequestLogging = opts.disableRequestLogging || defaultInitOptions.disableRequestLogging
24
+ this.isDisableRequestLoggingFunction = typeof this.disableRequestLogging === 'function'
25
+ this.requestIdLogLabel = opts.requestIdLogLabel || defaultInitOptions.requestIdLogLabel
26
+ }
27
+
28
+ /**
29
+ * Checks whether request logging is disabled for the given request.
30
+ *
31
+ * @param {object} req Raw or Fastify request object.
32
+ * @returns {boolean} `true` when logging should be skipped.
33
+ */
34
+ isLogDisabled (req) {
35
+ return this.isDisableRequestLoggingFunction
36
+ ? this.disableRequestLogging(req)
37
+ : this.disableRequestLogging
38
+ }
39
+
40
+ /**
41
+ * Logs an incoming request at `info` level.
42
+ *
43
+ * @param {object} request Fastify request object.
44
+ * @param {object} reply Fastify reply object.
45
+ * @param {object} [metadata] Extra contextual data (unused).
46
+ */
47
+ incomingRequest (request, reply, metadata) {
48
+ if (this.isLogDisabled(request)) { return }
49
+
50
+ request.log.info({ req: request }, 'incoming request')
51
+ }
52
+
53
+ /**
54
+ * Logs the outcome of a completed request.
55
+ * Uses `error` level when an error is present, `info` otherwise.
56
+ *
57
+ * @param {Error | null} error Error that occurred during the response, if any.
58
+ * @param {object} request Fastify request object.
59
+ * @param {object} reply Fastify reply object.
60
+ * @param {object} [metadata] Extra contextual data (unused).
61
+ */
62
+ requestCompleted (error, request, reply, metadata) {
63
+ if (this.isLogDisabled(request)) { return }
64
+
65
+ if (error) {
66
+ reply.log.error({ res: reply, err: error, responseTime: reply.elapsedTime }, 'request errored')
67
+ } else {
68
+ reply.log.info({ res: reply, responseTime: reply.elapsedTime }, 'request completed')
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Logs an error handled by the default error handler.
74
+ * Uses `error` for 5xx status codes, `info` for 4xx.
75
+ *
76
+ * @param {Error} error The error being handled.
77
+ * @param {object} request Fastify request object.
78
+ * @param {object} reply Fastify reply object (must have `statusCode`).
79
+ * @param {object} [metadata] Extra contextual data (unused).
80
+ */
81
+ defaultErrorLog (error, request, reply, metadata) {
82
+ if (this.isLogDisabled(request)) { return }
83
+
84
+ if (reply.statusCode >= 500) {
85
+ reply.log.error({ req: request, res: reply, err: error }, error?.message)
86
+ } else {
87
+ reply.log.info({ res: reply, err: error }, error?.message)
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Logs stream-level errors that occur after headers have been sent.
93
+ * Premature closes are logged at `info`; other errors at `warn`.
94
+ *
95
+ * @param {Error} error The stream error.
96
+ * @param {object} request Fastify request object.
97
+ * @param {object} reply Fastify reply object.
98
+ * @param {object} [metadata] Extra contextual data (unused).
99
+ */
100
+ streamError (error, request, reply, metadata) {
101
+ if (this.isLogDisabled(request)) { return }
102
+
103
+ if (error.code === 'ERR_STREAM_PREMATURE_CLOSE') {
104
+ reply.log.info({ res: reply }, 'stream closed prematurely')
105
+ } else {
106
+ reply.log.warn({ err: error }, 'response terminated with an error with headers already sent')
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Logs a "route not found" message at `info` level.
112
+ * Used by the default 404 handler.
113
+ *
114
+ * @param {object} request Fastify request object.
115
+ * @param {object} reply Fastify reply object.
116
+ * @param {object} [metadata] Extra contextual data (unused).
117
+ */
118
+ routeNotFound (request, reply, metadata) {
119
+ if (this.isLogDisabled(request)) { return }
120
+
121
+ const { url, method } = request.raw
122
+ request.log.info(`Route ${method}:${url} not found`)
123
+ }
124
+
125
+ /**
126
+ * Logs a warning when writeHead fails during error handling.
127
+ *
128
+ * @param {Error} error The error thrown by writeHead.
129
+ * @param {object} request Fastify request object.
130
+ * @param {object} reply Fastify reply object.
131
+ * @param {object} [metadata] Extra contextual data (unused).
132
+ */
133
+ writeHeadError (error, request, reply, metadata) {
134
+ if (this.isLogDisabled(request)) { return }
135
+
136
+ reply.log.warn(
137
+ { req: request, res: reply, err: error },
138
+ error?.message
139
+ )
140
+ }
141
+
142
+ /**
143
+ * Logs an error when the serializer for a given status code fails.
144
+ *
145
+ * @param {Error} error The serialization error.
146
+ * @param {object} request Fastify request object.
147
+ * @param {object} reply Fastify reply object.
148
+ * @param {object} metadata Extra contextual data.
149
+ * @param {number} metadata.statusCode The status code that triggered the serializer.
150
+ */
151
+ serializerError (error, request, reply, metadata) {
152
+ if (this.isLogDisabled(request)) { return }
153
+
154
+ reply.log.error({ err: error, statusCode: metadata.statusCode }, 'The serializer for the given status code failed')
155
+ }
156
+
157
+ /**
158
+ * Logs a 503 Service Unavailable when the server is closing.
159
+ * Always emitted (not gated by `disableRequestLogging`).
160
+ *
161
+ * @param {import('../fastify').FastifyBaseLogger} logger
162
+ * @param {import('../fastify').FastifyInstance} server
163
+ */
164
+ serviceUnavailable (logger, server) {
165
+ logger.info({ res: { statusCode: 503 } }, 'request aborted - refusing to accept new requests as server is closing')
166
+ }
167
+ }
168
+
169
+ module.exports = { LogController }
@@ -1,11 +1,18 @@
1
1
  'use strict'
2
2
 
3
+ const { performance } = require('node:perf_hooks')
4
+
3
5
  const {
4
6
  FST_ERR_LOG_LOGGER_AND_LOGGER_INSTANCE_PROVIDED,
5
7
  FST_ERR_LOG_INVALID_LOGGER_CONFIG,
6
8
  FST_ERR_LOG_INVALID_LOGGER_INSTANCE,
7
- FST_ERR_LOG_INVALID_LOGGER
9
+ FST_ERR_LOG_INVALID_LOGGER,
10
+ FST_ERR_LOG_INVALID_LOG_CONTROLLER
8
11
  } = require('./errors')
12
+ const {
13
+ kLogController
14
+ } = require('./symbols.js')
15
+ const { LogController } = require('./log-controller.js')
9
16
 
10
17
  /**
11
18
  * Utility for creating a child logger with the appropriate bindings, logger factory
@@ -21,7 +28,7 @@ const {
21
28
  */
22
29
  function createChildLogger (context, logger, req, reqId, loggerOpts) {
23
30
  const loggerBindings = {
24
- [context.requestIdLogLabel]: reqId
31
+ [context.server[kLogController].requestIdLogLabel]: reqId
25
32
  }
26
33
  const child = context.childLoggerFactory.call(context.server, logger, loggerBindings, loggerOpts || {}, req)
27
34
 
@@ -122,15 +129,29 @@ function createLogger (options) {
122
129
  return { logger, hasLogger: true }
123
130
  }
124
131
 
132
+ function createLogController (options) {
133
+ const userController = options.logController
134
+ if (!userController) {
135
+ return new LogController(options)
136
+ }
137
+
138
+ if (userController instanceof LogController) {
139
+ return userController
140
+ }
141
+
142
+ throw new FST_ERR_LOG_INVALID_LOG_CONTROLLER(typeof userController)
143
+ }
144
+
125
145
  function now () {
126
- const ts = process.hrtime()
127
- return (ts[0] * 1e3) + (ts[1] / 1e6)
146
+ return performance.now()
128
147
  }
129
148
 
130
149
  module.exports = {
131
150
  createChildLogger,
132
151
  defaultChildLoggerFactory,
152
+ createLogController,
133
153
  createLogger,
154
+ LogController,
134
155
  validateLogger,
135
156
  now
136
157
  }