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
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const { test } = require('node:test')
4
+ const http2 = require('node:http2')
4
5
  const Fastify = require('../..')
5
6
  const h2url = require('h2url')
6
7
  const msg = { hello: 'world' }
@@ -66,3 +67,137 @@ test('http2 plain test', async t => {
66
67
  t.assert.strictEqual(JSON.parse(res.body).port, parseInt(host.split(':')[1]))
67
68
  })
68
69
  })
70
+
71
+ test('http2 large non-stream replies are sent completely', async t => {
72
+ const modes = ['buffer', 'string']
73
+
74
+ for (const mode of modes) {
75
+ await t.test(mode, async t => {
76
+ const fastify = Fastify({ http2: true })
77
+ const payload = mode === 'buffer'
78
+ ? Buffer.alloc((64 * 1024) + 1, 'a')
79
+ : 'a'.repeat((64 * 1024) + 1)
80
+ const contentLength = Buffer.byteLength(payload)
81
+
82
+ fastify.get('/large', async (req, reply) => {
83
+ reply.header('content-type', 'application/octet-stream')
84
+ reply.header('content-length', contentLength)
85
+
86
+ return payload
87
+ })
88
+
89
+ await fastify.listen({ port: 0, host: '127.0.0.1' })
90
+
91
+ const client = http2.connect(`http://127.0.0.1:${fastify.server.address().port}`)
92
+ t.after(() => {
93
+ client.close()
94
+ return fastify.close()
95
+ })
96
+
97
+ await new Promise((resolve, reject) => {
98
+ const large = client.request({
99
+ [http2.constants.HTTP2_HEADER_METHOD]: http2.constants.HTTP2_METHOD_GET,
100
+ [http2.constants.HTTP2_HEADER_PATH]: '/large'
101
+ })
102
+ const chunks = []
103
+
104
+ large.on('error', reject)
105
+ large.on('data', chunk => {
106
+ chunks.push(chunk)
107
+ })
108
+ large.on('end', () => {
109
+ t.assert.strictEqual(Buffer.concat(chunks).length, contentLength)
110
+ resolve()
111
+ })
112
+ large.end()
113
+ })
114
+ })
115
+ }
116
+ })
117
+
118
+ test('http2 large buffer replies can be cancelled without rejecting the next stream', async t => {
119
+ const fastify = Fastify({ http2: true })
120
+ const payload = Buffer.alloc(32 * 1024 * 1024, 'a')
121
+ let smallHit = false
122
+
123
+ fastify.get('/large', async (req, reply) => {
124
+ reply.header('content-type', 'application/octet-stream')
125
+ reply.header('content-length', payload.length)
126
+
127
+ return payload
128
+ })
129
+
130
+ fastify.get('/small', async () => {
131
+ smallHit = true
132
+ return 'ok'
133
+ })
134
+
135
+ await fastify.listen({ port: 0, host: '127.0.0.1' })
136
+
137
+ const client = http2.connect(`http://127.0.0.1:${fastify.server.address().port}`)
138
+ t.after(() => {
139
+ client.close()
140
+ return fastify.close()
141
+ })
142
+
143
+ await new Promise((resolve, reject) => {
144
+ let cancelTimer
145
+ let requestTimer
146
+ let timeout
147
+
148
+ function cleanup () {
149
+ clearTimeout(cancelTimer)
150
+ clearTimeout(requestTimer)
151
+ clearTimeout(timeout)
152
+ }
153
+
154
+ const large = client.request({
155
+ [http2.constants.HTTP2_HEADER_METHOD]: http2.constants.HTTP2_METHOD_GET,
156
+ [http2.constants.HTTP2_HEADER_PATH]: '/large'
157
+ })
158
+ let largeResponded = false
159
+
160
+ large.on('error', err => {
161
+ if (!largeResponded) {
162
+ cleanup()
163
+ reject(err)
164
+ }
165
+ })
166
+ large.on('response', () => {
167
+ largeResponded = true
168
+ large.pause()
169
+ cancelTimer = setTimeout(() => {
170
+ large.close(http2.constants.NGHTTP2_CANCEL)
171
+ }, 100)
172
+ requestTimer = setTimeout(() => {
173
+ const small = client.request({
174
+ [http2.constants.HTTP2_HEADER_METHOD]: http2.constants.HTTP2_METHOD_GET,
175
+ [http2.constants.HTTP2_HEADER_PATH]: '/small'
176
+ })
177
+ let body = ''
178
+
179
+ timeout = setTimeout(() => {
180
+ cleanup()
181
+ reject(new Error('timed out waiting for /small response'))
182
+ }, 3000)
183
+
184
+ small.setEncoding('utf8')
185
+ small.on('error', err => {
186
+ cleanup()
187
+ reject(err)
188
+ })
189
+ small.on('data', chunk => {
190
+ body += chunk
191
+ })
192
+ small.on('end', () => {
193
+ cleanup()
194
+ t.assert.strictEqual(body, 'ok')
195
+ t.assert.strictEqual(smallHit, true)
196
+ resolve()
197
+ })
198
+ small.end()
199
+ }, 200)
200
+ })
201
+ large.end()
202
+ })
203
+ })
@@ -6,7 +6,7 @@ const h2url = require('h2url')
6
6
  const msg = { hello: 'world' }
7
7
 
8
8
  const { buildCertificate } = require('../build-certificate')
9
- const { Agent } = require('undici')
9
+ const { Agent, fetch } = require('undici')
10
10
  test.before(buildCertificate)
11
11
 
12
12
  test('secure with fallback', async (t) => {
@@ -1,11 +1,10 @@
1
1
  'use strict'
2
2
 
3
3
  const { test } = require('node:test')
4
- const { request } = require('undici')
4
+ const { request, Agent, fetch } = require('undici')
5
5
  const Fastify = require('../..')
6
6
 
7
7
  const { buildCertificate } = require('../build-certificate')
8
- const { Agent } = require('undici')
9
8
  test.before(buildCertificate)
10
9
 
11
10
  test('https', async (t) => {
@@ -5,7 +5,7 @@ const errors = require('../../lib/errors')
5
5
  const { readFileSync } = require('node:fs')
6
6
  const { resolve } = require('node:path')
7
7
 
8
- const expectedErrors = 88
8
+ const expectedErrors = 92
9
9
 
10
10
  test(`should expose ${expectedErrors} errors`, t => {
11
11
  t.plan(1)
@@ -630,6 +630,16 @@ test('FST_ERR_BAD_URL', t => {
630
630
  t.assert.ok(error instanceof Error)
631
631
  })
632
632
 
633
+ test('FST_ERR_MAX_PARAM_LENGTH', t => {
634
+ t.plan(5)
635
+ const error = new errors.FST_ERR_MAX_PARAM_LENGTH()
636
+ t.assert.strictEqual(error.name, 'FastifyError')
637
+ t.assert.strictEqual(error.code, 'FST_ERR_MAX_PARAM_LENGTH')
638
+ t.assert.strictEqual(error.message, "'%s' is exceeding the max param length")
639
+ t.assert.strictEqual(error.statusCode, 414)
640
+ t.assert.ok(error instanceof Error)
641
+ })
642
+
633
643
  test('FST_ERR_ASYNC_CONSTRAINT', t => {
634
644
  t.plan(5)
635
645
  const error = new errors.FST_ERR_ASYNC_CONSTRAINT()
@@ -710,6 +720,16 @@ test('FST_ERR_ROUTE_METHOD_NOT_SUPPORTED', t => {
710
720
  t.assert.ok(error instanceof Error)
711
721
  })
712
722
 
723
+ test('FST_ERR_ROUTE_LOG_LEVEL_INVALID', t => {
724
+ t.plan(5)
725
+ const error = new errors.FST_ERR_ROUTE_LOG_LEVEL_INVALID()
726
+ t.assert.strictEqual(error.name, 'FastifyError')
727
+ t.assert.strictEqual(error.code, 'FST_ERR_ROUTE_LOG_LEVEL_INVALID')
728
+ t.assert.strictEqual(error.message, "Log level for '%s:%s' route must be a valid logger level. Received: '%s'")
729
+ t.assert.strictEqual(error.statusCode, 500)
730
+ t.assert.ok(error instanceof TypeError)
731
+ })
732
+
713
733
  test('FST_ERR_ROUTE_BODY_VALIDATION_SCHEMA_NOT_SUPPORTED', t => {
714
734
  t.plan(5)
715
735
  const error = new errors.FST_ERR_ROUTE_BODY_VALIDATION_SCHEMA_NOT_SUPPORTED()
@@ -810,6 +830,16 @@ test('FST_ERR_PLUGIN_INVALID_ASYNC_HANDLER', t => {
810
830
  t.assert.ok(error instanceof TypeError)
811
831
  })
812
832
 
833
+ test('FST_ERR_PLUGIN_DEPENDENCY_NOT_REGISTERED', t => {
834
+ t.plan(5)
835
+ const error = new errors.FST_ERR_PLUGIN_DEPENDENCY_NOT_REGISTERED('my-dep', 'my-plugin')
836
+ t.assert.strictEqual(error.name, 'FastifyError')
837
+ t.assert.strictEqual(error.code, 'FST_ERR_PLUGIN_DEPENDENCY_NOT_REGISTERED')
838
+ t.assert.strictEqual(error.message, "The dependency 'my-dep' of plugin 'my-plugin' is not registered")
839
+ t.assert.strictEqual(error.statusCode, 500)
840
+ t.assert.ok(error instanceof Error)
841
+ })
842
+
813
843
  test('FST_ERR_PLUGIN_CALLBACK_NOT_FN', t => {
814
844
  t.plan(5)
815
845
  const error = new errors.FST_ERR_PLUGIN_CALLBACK_NOT_FN()
@@ -3,6 +3,7 @@
3
3
  const { test } = require('node:test')
4
4
  const Fastify = require('../..')
5
5
  const loggerUtils = require('../../lib/logger-factory')
6
+ const { createLogController, LogController } = require('../../lib/logger-factory')
6
7
  const { serializers } = require('../../lib/logger-pino')
7
8
 
8
9
  test('time resolution', t => {
@@ -142,6 +143,327 @@ test('The logger should error if both stream and file destination are given', t
142
143
  }
143
144
  })
144
145
 
146
+ test('LogController defaults', t => {
147
+ t.plan(3)
148
+ const dispatcher = new LogController()
149
+ t.assert.strictEqual(dispatcher.disableRequestLogging, false)
150
+ t.assert.strictEqual(dispatcher.requestIdLogLabel, 'reqId')
151
+ t.assert.strictEqual(dispatcher.isLogDisabled({}), false)
152
+ })
153
+
154
+ test('LogController with custom options', t => {
155
+ t.plan(2)
156
+ const dispatcher = new LogController({
157
+ disableRequestLogging: true,
158
+ requestIdLogLabel: 'traceId'
159
+ })
160
+ t.assert.strictEqual(dispatcher.disableRequestLogging, true)
161
+ t.assert.strictEqual(dispatcher.requestIdLogLabel, 'traceId')
162
+ })
163
+
164
+ test('LogController with function disableRequestLogging', t => {
165
+ t.plan(2)
166
+ const dispatcher = new LogController({
167
+ disableRequestLogging: (req) => req.skip === true
168
+ })
169
+ t.assert.strictEqual(dispatcher.isLogDisabled({ skip: true }), true)
170
+ t.assert.strictEqual(dispatcher.isLogDisabled({ skip: false }), false)
171
+ })
172
+
173
+ test('requestCompleted should not log when logging is disabled (boolean)', t => {
174
+ t.plan(1)
175
+ const logController = new LogController({ disableRequestLogging: true })
176
+ const log = {
177
+ error: () => { t.assert.fail('error should not be called') },
178
+ info: () => { t.assert.fail('info should not be called') }
179
+ }
180
+ const request = {}
181
+ const reply = { request, log }
182
+ logController.requestCompleted(new Error('test'), request, reply)
183
+ t.assert.ok(true, 'logger was not called')
184
+ })
185
+
186
+ test('requestCompleted should not log when logging is disabled (function)', t => {
187
+ t.plan(1)
188
+ const logController = new LogController({ disableRequestLogging: () => true })
189
+ const log = {
190
+ error: () => { t.assert.fail('error should not be called') },
191
+ info: () => { t.assert.fail('info should not be called') }
192
+ }
193
+ const request = {}
194
+ const reply = { request, log }
195
+ logController.requestCompleted(new Error('test'), request, reply)
196
+ t.assert.ok(true, 'logger was not called')
197
+ })
198
+
199
+ test('requestCompleted should log error when err is present', t => {
200
+ t.plan(1)
201
+ const logController = new LogController({ disableRequestLogging: false })
202
+ const err = new Error('test')
203
+ const log = {
204
+ error: (data, msg) => {
205
+ t.assert.strictEqual(msg, 'request errored')
206
+ }
207
+ }
208
+ const request = {}
209
+ const reply = { request, log, elapsedTime: 42 }
210
+ logController.requestCompleted(err, request, reply)
211
+ })
212
+
213
+ test('requestCompleted should log info when no error', t => {
214
+ t.plan(1)
215
+ const logController = new LogController({ disableRequestLogging: false })
216
+ const log = {
217
+ info: (data, msg) => {
218
+ t.assert.strictEqual(msg, 'request completed')
219
+ }
220
+ }
221
+ const request = {}
222
+ const reply = { request, log, elapsedTime: 42 }
223
+ logController.requestCompleted(null, request, reply)
224
+ })
225
+
226
+ test('defaultErrorLog should not log when logging is disabled (boolean)', t => {
227
+ t.plan(1)
228
+ const logController = new LogController({ disableRequestLogging: true })
229
+ const log = {
230
+ info: () => { t.assert.fail('info should not be called') },
231
+ error: () => { t.assert.fail('error should not be called') }
232
+ }
233
+ const request = {}
234
+ const reply = { request, log, statusCode: 404 }
235
+ logController.defaultErrorLog(new Error('not found'), request, reply)
236
+ t.assert.ok(true, 'logger was not called')
237
+ })
238
+
239
+ test('defaultErrorLog should not log when logging is disabled (function)', t => {
240
+ t.plan(1)
241
+ const logController = new LogController({ disableRequestLogging: () => true })
242
+ const log = {
243
+ info: () => { t.assert.fail('info should not be called') },
244
+ error: () => { t.assert.fail('error should not be called') }
245
+ }
246
+ const request = {}
247
+ const reply = { request, log, statusCode: 404 }
248
+ logController.defaultErrorLog(new Error('not found'), request, reply)
249
+ t.assert.ok(true, 'logger was not called')
250
+ })
251
+
252
+ test('defaultErrorLog should log info for 4xx errors', t => {
253
+ t.plan(1)
254
+ const logController = new LogController({ disableRequestLogging: false })
255
+ const err = new Error('not found')
256
+ const log = {
257
+ info: (data, msg) => {
258
+ t.assert.strictEqual(msg, 'not found')
259
+ }
260
+ }
261
+ const request = {}
262
+ const reply = { request, log, statusCode: 404 }
263
+ logController.defaultErrorLog(err, request, reply)
264
+ })
265
+
266
+ test('defaultErrorLog should log error for 5xx errors', t => {
267
+ t.plan(1)
268
+ const logController = new LogController({ disableRequestLogging: false })
269
+ const err = new Error('internal error')
270
+ const log = {
271
+ error: (data, msg) => {
272
+ t.assert.strictEqual(msg, 'internal error')
273
+ }
274
+ }
275
+ const request = {}
276
+ const reply = { request, log, statusCode: 500 }
277
+ logController.defaultErrorLog(err, request, reply)
278
+ })
279
+
280
+ test('writeHeadError should not log when logging is disabled (boolean)', t => {
281
+ t.plan(1)
282
+ const logController = new LogController({ disableRequestLogging: true })
283
+ const log = {
284
+ warn: () => { t.assert.fail('warn should not be called') }
285
+ }
286
+ const request = {}
287
+ const reply = { request, log }
288
+ logController.writeHeadError(new Error('write head failed'), request, reply)
289
+ t.assert.ok(true, 'logger was not called')
290
+ })
291
+
292
+ test('writeHeadError should not log when logging is disabled (function)', t => {
293
+ t.plan(1)
294
+ const logController = new LogController({ disableRequestLogging: () => true })
295
+ const log = {
296
+ warn: () => { t.assert.fail('warn should not be called') }
297
+ }
298
+ const request = {}
299
+ const reply = { request, log }
300
+ logController.writeHeadError(new Error('write head failed'), request, reply)
301
+ t.assert.ok(true, 'logger was not called')
302
+ })
303
+
304
+ test('writeHeadError should log warn with error message', t => {
305
+ t.plan(2)
306
+ const logController = new LogController({ disableRequestLogging: false })
307
+ const error = new Error('write head failed')
308
+ const request = {}
309
+ const reply = {
310
+ request,
311
+ log: {
312
+ warn: (data, msg) => {
313
+ t.assert.strictEqual(msg, 'write head failed')
314
+ t.assert.strictEqual(data.err, error)
315
+ }
316
+ }
317
+ }
318
+ logController.writeHeadError(error, request, reply)
319
+ })
320
+
321
+ test('serializerError should not log when logging is disabled (boolean)', t => {
322
+ t.plan(1)
323
+ const logController = new LogController({ disableRequestLogging: true })
324
+ const log = {
325
+ error: () => { t.assert.fail('error should not be called') }
326
+ }
327
+ const request = {}
328
+ const reply = { request, log }
329
+ logController.serializerError(new Error('serializer failed'), request, reply, { statusCode: 500 })
330
+ t.assert.ok(true, 'logger was not called')
331
+ })
332
+
333
+ test('serializerError should not log when logging is disabled (function)', t => {
334
+ t.plan(1)
335
+ const logController = new LogController({ disableRequestLogging: () => true })
336
+ const log = {
337
+ error: () => { t.assert.fail('error should not be called') }
338
+ }
339
+ const request = {}
340
+ const reply = { request, log }
341
+ logController.serializerError(new Error('serializer failed'), request, reply, { statusCode: 500 })
342
+ t.assert.ok(true, 'logger was not called')
343
+ })
344
+
345
+ test('serializerError should log error with status code', t => {
346
+ t.plan(2)
347
+ const logController = new LogController({ disableRequestLogging: false })
348
+ const err = new Error('serializer failed')
349
+ const request = {}
350
+ const reply = {
351
+ request,
352
+ log: {
353
+ error: (data, msg) => {
354
+ t.assert.strictEqual(msg, 'The serializer for the given status code failed')
355
+ t.assert.strictEqual(data.statusCode, 500)
356
+ }
357
+ }
358
+ }
359
+ logController.serializerError(err, request, reply, { statusCode: 500 })
360
+ })
361
+
362
+ test('createLogController should use LogController instance directly', t => {
363
+ t.plan(2)
364
+ const custom = new LogController({ disableRequestLogging: true, requestIdLogLabel: 'traceId' })
365
+ const dispatcher = createLogController({ logController: custom })
366
+ t.assert.strictEqual(dispatcher, custom)
367
+ t.assert.strictEqual(dispatcher.requestIdLogLabel, 'traceId')
368
+ })
369
+
370
+ test('createLogController should create default when no instance provided', t => {
371
+ t.plan(2)
372
+ const dispatcher = createLogController({ disableRequestLogging: false, requestIdLogLabel: 'reqId' })
373
+ t.assert.ok(dispatcher instanceof LogController)
374
+ t.assert.strictEqual(dispatcher.requestIdLogLabel, 'reqId')
375
+ })
376
+
377
+ test('createLogController should throw on invalid type', t => {
378
+ t.plan(1)
379
+ t.assert.throws(() => {
380
+ createLogController({ disableRequestLogging: false, logController: 'bad' })
381
+ }, { code: 'FST_ERR_LOG_INVALID_LOG_CONTROLLER' })
382
+ })
383
+
384
+ test('createLogController should throw when plain object is provided', t => {
385
+ t.plan(1)
386
+ t.assert.throws(() => {
387
+ createLogController({ disableRequestLogging: false, logController: { incomingRequest: () => { } } })
388
+ }, { code: 'FST_ERR_LOG_INVALID_LOG_CONTROLLER' })
389
+ })
390
+
391
+ test('createLogController should accept null', t => {
392
+ t.plan(1)
393
+ const logController = createLogController({ disableRequestLogging: false, logController: null })
394
+ t.assert.ok(logController)
395
+ })
396
+
397
+ test('LogController subclass should work', t => {
398
+ t.plan(2)
399
+
400
+ class MyDispatcher extends LogController {
401
+ constructor () {
402
+ super({ requestIdLogLabel: 'traceId' })
403
+ }
404
+
405
+ incomingRequest (request, reply, metadata) {
406
+ t.assert.ok(true, 'custom incomingRequest called')
407
+ }
408
+ }
409
+
410
+ const dispatcher = new MyDispatcher()
411
+ t.assert.strictEqual(dispatcher.requestIdLogLabel, 'traceId')
412
+ dispatcher.incomingRequest({})
413
+ })
414
+
415
+ test('LogController subclass keeps defaults for non-overridden methods', t => {
416
+ t.plan(1)
417
+
418
+ class MyDispatcher extends LogController {
419
+ incomingRequest () { }
420
+ }
421
+
422
+ const dispatcher = new MyDispatcher()
423
+ const log = {
424
+ info: (data, msg) => {
425
+ t.assert.strictEqual(msg, 'request completed')
426
+ }
427
+ }
428
+ const request = {}
429
+ const reply = { request, log, elapsedTime: 42 }
430
+ dispatcher.requestCompleted(null, request, reply)
431
+ })
432
+
433
+ test('serviceUnavailable should log with logger and receive server', t => {
434
+ t.plan(2)
435
+ const dispatcher = new LogController()
436
+ const fakeLogger = {
437
+ info: (data, msg) => {
438
+ t.assert.deepStrictEqual(data, { res: { statusCode: 503 } })
439
+ t.assert.strictEqual(msg, 'request aborted - refusing to accept new requests as server is closing')
440
+ }
441
+ }
442
+ const fakeServer = { name: 'test-server' }
443
+ dispatcher.serviceUnavailable(fakeLogger, fakeServer)
444
+ })
445
+
446
+ test('serviceUnavailable subclass can use server', t => {
447
+ t.plan(1)
448
+
449
+ const fakeServer = { name: 'test-server' }
450
+
451
+ class MyDispatcher extends LogController {
452
+ serviceUnavailable (logger, server) {
453
+ t.assert.strictEqual(server, fakeServer)
454
+ }
455
+ }
456
+
457
+ const dispatcher = new MyDispatcher()
458
+ dispatcher.serviceUnavailable({}, fakeServer)
459
+ })
460
+
461
+ test('LogController is exported from fastify', t => {
462
+ t.plan(2)
463
+ t.assert.ok(Fastify.LogController)
464
+ t.assert.strictEqual(Fastify.LogController, LogController)
465
+ })
466
+
145
467
  test('The serializer prevent fails if the request socket is undefined', t => {
146
468
  t.plan(1)
147
469
 
@@ -149,7 +149,7 @@ test('checkDependencies should check if the given dependency is present in the i
149
149
  })
150
150
 
151
151
  test('checkDependencies should check if the given dependency is present in the instance (errored)', t => {
152
- t.plan(1)
152
+ t.plan(3)
153
153
 
154
154
  fn[Symbol.for('plugin-meta')] = {
155
155
  name: 'test-plugin',
@@ -164,6 +164,8 @@ test('checkDependencies should check if the given dependency is present in the i
164
164
  t.assert.fail('should throw')
165
165
  } catch (err) {
166
166
  t.assert.strictEqual(err.message, "The dependency 'plugin' of plugin 'test-plugin' is not registered")
167
+ t.assert.strictEqual(err.code, 'FST_ERR_PLUGIN_DEPENDENCY_NOT_REGISTERED')
168
+ t.assert.strictEqual(err.name, 'FastifyError')
167
169
  }
168
170
 
169
171
  function fn () {}
@@ -5,6 +5,7 @@ const http = require('node:http')
5
5
  const NotFound = require('http-errors').NotFound
6
6
  const Request = require('../../lib/request')
7
7
  const Reply = require('../../lib/reply')
8
+ const { LogController } = require('../../lib/log-controller')
8
9
  const Fastify = require('../..')
9
10
  const { Readable, Writable } = require('node:stream')
10
11
  const {
@@ -13,7 +14,8 @@ const {
13
14
  kReplySerializer,
14
15
  kReplyIsError,
15
16
  kReplySerializerDefault,
16
- kRouteContext
17
+ kRouteContext,
18
+ kLogController
17
19
  } = require('../../lib/symbols')
18
20
  const fs = require('node:fs')
19
21
  const path = require('node:path')
@@ -85,7 +87,14 @@ test('reply.send will logStream error and destroy the stream', t => {
85
87
  warn: () => { }
86
88
  }
87
89
 
88
- const reply = new Reply(response, { [kRouteContext]: { onSend: null } }, log)
90
+ const fakeRequest = {
91
+ [kRouteContext]: {
92
+ onSend: null,
93
+ server: { [kLogController]: new LogController() }
94
+ }
95
+ }
96
+
97
+ const reply = new Reply(response, fakeRequest, log)
89
98
  reply.send(payload)
90
99
  payload.destroy(new Error('stream error'))
91
100
 
@@ -764,7 +773,7 @@ test('non-string with custom json\'s content-type SHOULD be serialized as json',
764
773
 
765
774
  const result = await fetch(fastifyServer)
766
775
  t.assert.ok(result.ok)
767
- t.assert.strictEqual(result.headers.get('content-type'), 'application/json; version=2; charset=utf-8')
776
+ t.assert.strictEqual(result.headers.get('content-type'), 'application/json; version="2"; charset=utf-8')
768
777
  t.assert.deepStrictEqual(await result.text(), JSON.stringify({ key: 'hello world!' }))
769
778
  })
770
779
 
@@ -1149,7 +1158,7 @@ test('reply.hasHeader computes raw and fastify headers', async t => {
1149
1158
  })
1150
1159
 
1151
1160
  test('Reply should handle JSON content type with a charset', async t => {
1152
- t.plan(8)
1161
+ t.plan(10)
1153
1162
 
1154
1163
  const fastify = require('../../')()
1155
1164
 
@@ -1199,6 +1208,18 @@ test('Reply should handle JSON content type with a charset', async t => {
1199
1208
  .send({ hello: 'world' })
1200
1209
  })
1201
1210
 
1211
+ fastify.get('/upper-charset', function (req, reply) {
1212
+ reply
1213
+ .header('content-type', 'application/json; CHARSET=utf-8')
1214
+ .send({ hello: 'world' })
1215
+ })
1216
+
1217
+ fastify.get('/random-case', function (req, reply) {
1218
+ reply
1219
+ .header('content-type', 'ApPlIcAtIoN/JsOn; ChArSeT=utf-8')
1220
+ .send({ hello: 'world' })
1221
+ })
1222
+
1202
1223
  {
1203
1224
  const res = await fastify.inject('/default')
1204
1225
  t.assert.strictEqual(res.headers['content-type'], 'application/json; charset=utf-8')
@@ -1233,6 +1254,16 @@ test('Reply should handle JSON content type with a charset', async t => {
1233
1254
  t.assert.strictEqual(res.headers['content-type'], 'application/json; charset=utf-32')
1234
1255
  }
1235
1256
 
1257
+ {
1258
+ const res = await fastify.inject('/upper-charset')
1259
+ t.assert.strictEqual(res.headers['content-type'], 'application/json; CHARSET=utf-8')
1260
+ }
1261
+
1262
+ {
1263
+ const res = await fastify.inject('/random-case')
1264
+ t.assert.strictEqual(res.headers['content-type'], 'ApPlIcAtIoN/JsOn; ChArSeT=utf-8')
1265
+ }
1266
+
1236
1267
  {
1237
1268
  const res = await fastify.inject('/no-space-type-utf32')
1238
1269
  t.assert.strictEqual(res.headers['content-type'], 'application/json;charset=utf-32')