fastify 5.6.1 → 5.7.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 (97) hide show
  1. package/.claude/settings.local.json +11 -0
  2. package/README.md +34 -33
  3. package/SECURITY.md +45 -7
  4. package/SPONSORS.md +1 -1
  5. package/build/build-validation.js +2 -3
  6. package/docs/Guides/Detecting-When-Clients-Abort.md +1 -1
  7. package/docs/Guides/Ecosystem.md +17 -15
  8. package/docs/Guides/Fluent-Schema.md +2 -2
  9. package/docs/Guides/Migration-Guide-V4.md +2 -1
  10. package/docs/Guides/Migration-Guide-V5.md +9 -0
  11. package/docs/Guides/Serverless.md +25 -7
  12. package/docs/Guides/Testing.md +2 -2
  13. package/docs/Reference/Decorators.md +4 -3
  14. package/docs/Reference/Encapsulation.md +5 -1
  15. package/docs/Reference/Logging.md +4 -0
  16. package/docs/Reference/Plugins.md +13 -4
  17. package/docs/Reference/Principles.md +2 -2
  18. package/docs/Reference/Reply.md +6 -2
  19. package/docs/Reference/Server.md +67 -4
  20. package/docs/Reference/Type-Providers.md +2 -2
  21. package/docs/Reference/TypeScript.md +2 -1
  22. package/docs/Reference/Validation-and-Serialization.md +7 -1
  23. package/docs/fastify-fastify-pr-6425-run-20528050272-Lint_Docs.log +180 -0
  24. package/docs/fastify-fastify-pr-6425-run-20528054188-PR_#6425.log +4006 -0
  25. package/docs/fastify-fastify-pr-6425-run-20528054403-Pull_Request_Labeler.log +42 -0
  26. package/docs/fastify-fastify-pr-6425-run-20528054421-Lint_Docs.log +196 -0
  27. package/docs/fastify-fastify-pr-6425-run-20528054423-Internal_Links_Check.log +1864 -0
  28. package/docs/fastify-fastify-pr-6425-run-20528054430-Test_compare.log +5 -0
  29. package/docs/fastify-fastify-pr-6425-run-20528054438-pull_request_title_check.log +41 -0
  30. package/eslint.config.js +18 -2
  31. package/examples/benchmark/webstream.js +27 -0
  32. package/fastify.d.ts +17 -22
  33. package/fastify.js +192 -177
  34. package/lib/{configValidator.js → config-validator.js} +189 -223
  35. package/lib/{contentTypeParser.js → content-type-parser.js} +2 -1
  36. package/lib/error-handler.js +3 -6
  37. package/lib/error-status.js +14 -0
  38. package/lib/{fourOhFour.js → four-oh-four.js} +6 -3
  39. package/lib/{handleRequest.js → handle-request.js} +7 -2
  40. package/lib/{headRoute.js → head-route.js} +13 -1
  41. package/lib/{initialConfigValidation.js → initial-config-validation.js} +1 -1
  42. package/lib/{pluginOverride.js → plugin-override.js} +2 -2
  43. package/lib/{pluginUtils.js → plugin-utils.js} +2 -2
  44. package/lib/reply.js +58 -6
  45. package/lib/request.js +5 -0
  46. package/lib/route.js +39 -14
  47. package/lib/schema-controller.js +2 -2
  48. package/lib/server.js +38 -5
  49. package/lib/validation.js +9 -1
  50. package/lib/{wrapThenable.js → wrap-thenable.js} +3 -0
  51. package/package.json +11 -11
  52. package/test/404s.test.js +69 -0
  53. package/test/500s.test.js +191 -0
  54. package/test/child-logger-factory.test.js +3 -3
  55. package/test/content-parser.test.js +2 -1
  56. package/test/diagnostics-channel/error-before-handler.test.js +1 -1
  57. package/test/diagnostics-channel/error-status.test.js +84 -0
  58. package/test/internals/content-type-parser.test.js +2 -2
  59. package/test/internals/handle-request.test.js +2 -2
  60. package/test/internals/initial-config.test.js +1 -1
  61. package/test/internals/plugin.test.js +2 -2
  62. package/test/internals/reply.test.js +22 -3
  63. package/test/internals/req-id-gen-factory.test.js +1 -1
  64. package/test/internals/schema-controller-perf.test.js +40 -0
  65. package/test/issue-4959.test.js +34 -9
  66. package/test/listen.1.test.js +9 -1
  67. package/test/logger/logging.test.js +38 -1
  68. package/test/promises.test.js +3 -3
  69. package/test/reply-web-stream-locked.test.js +37 -0
  70. package/test/request-error.test.js +116 -0
  71. package/test/route.6.test.js +20 -1
  72. package/test/route.7.test.js +49 -0
  73. package/test/router-options.test.js +169 -0
  74. package/test/schema-validation.test.js +27 -4
  75. package/test/server.test.js +25 -4
  76. package/test/stream.5.test.js +3 -3
  77. package/test/types/fastify.test-d.ts +94 -21
  78. package/test/types/hooks.test-d.ts +6 -1
  79. package/test/types/instance.test-d.ts +64 -36
  80. package/test/types/logger.test-d.ts +18 -6
  81. package/test/types/plugin.test-d.ts +24 -6
  82. package/test/types/register.test-d.ts +108 -33
  83. package/test/types/reply.test-d.ts +78 -10
  84. package/test/types/request.test-d.ts +25 -6
  85. package/test/types/route.test-d.ts +10 -1
  86. package/test/types/type-provider.test-d.ts +6 -6
  87. package/test/validation-error-handling.test.js +68 -1
  88. package/test/web-api.test.js +136 -0
  89. package/test/wrap-thenable.test.js +1 -1
  90. package/types/instance.d.ts +3 -3
  91. package/types/reply.d.ts +2 -2
  92. package/types/type-provider.d.ts +16 -0
  93. package/.vscode/settings.json +0 -22
  94. package/test/check.test.js +0 -219
  95. package/test/decorator-namespace.test._js_ +0 -30
  96. /package/lib/{reqIdGenFactory.js → req-id-gen-factory.js} +0 -0
  97. /package/types/{serverFactory.d.ts → server-factory.d.ts} +0 -0
@@ -16,7 +16,7 @@ t.test('logging', { timeout: 60000 }, async (t) => {
16
16
  let localhost
17
17
  let localhostForURL
18
18
 
19
- t.plan(13)
19
+ t.plan(14)
20
20
 
21
21
  t.before(async function () {
22
22
  [localhost, localhostForURL] = await helper.getLoopbackHost()
@@ -282,6 +282,43 @@ t.test('logging', { timeout: 60000 }, async (t) => {
282
282
  t.assert.strictEqual(stream.readableLength, 0)
283
283
  })
284
284
 
285
+ await t.test('should log incoming request and outgoing response based on disableRequestLogging function', async (t) => {
286
+ const lines = [
287
+ 'incoming request',
288
+ 'request completed'
289
+ ]
290
+ t.plan(lines.length)
291
+
292
+ const stream = split(JSON.parse)
293
+ const loggerInstance = pino(stream)
294
+
295
+ const fastify = Fastify({
296
+ disableRequestLogging: (request) => {
297
+ return request.url !== '/not-logged'
298
+ },
299
+ loggerInstance
300
+ })
301
+ t.after(() => fastify.close())
302
+
303
+ fastify.get('/logged', (req, reply) => {
304
+ return reply.code(200).send({})
305
+ })
306
+
307
+ fastify.get('/not-logged', (req, reply) => {
308
+ return reply.code(200).send({})
309
+ })
310
+
311
+ await fastify.ready()
312
+
313
+ await fastify.inject({ method: 'GET', url: '/not-logged' })
314
+ await fastify.inject({ method: 'GET', url: '/logged' })
315
+
316
+ for await (const [line] of on(stream, 'data')) {
317
+ t.assert.strictEqual(line.msg, lines.shift())
318
+ if (lines.length === 0) break
319
+ }
320
+ })
321
+
285
322
  await t.test('defaults to info level', async (t) => {
286
323
  const lines = [
287
324
  { req: { method: 'GET' }, msg: 'incoming request' },
@@ -64,7 +64,7 @@ fastify.get('/return-reply', opts, function (req, reply) {
64
64
  fastify.listen({ port: 0 }, (err, fastifyServer) => {
65
65
  assert.ifError(err)
66
66
 
67
- test('shorthand - sget return promise es6 get', async t => {
67
+ test('shorthand - fetch return promise es6 get', async t => {
68
68
  t.plan(4)
69
69
 
70
70
  const result = await fetch(`${fastifyServer}/return`)
@@ -75,7 +75,7 @@ fastify.listen({ port: 0 }, (err, fastifyServer) => {
75
75
  t.assert.deepStrictEqual(JSON.parse(body), { hello: 'world' })
76
76
  })
77
77
 
78
- test('shorthand - sget promise es6 get return error', async t => {
78
+ test('shorthand - fetch promise es6 get return error', async t => {
79
79
  t.plan(2)
80
80
 
81
81
  const result = await fetch(`${fastifyServer}/return-error`)
@@ -83,7 +83,7 @@ fastify.listen({ port: 0 }, (err, fastifyServer) => {
83
83
  t.assert.strictEqual(result.status, 500)
84
84
  })
85
85
 
86
- test('sget promise double send', async t => {
86
+ test('fetch promise double send', async t => {
87
87
  t.plan(3)
88
88
 
89
89
  const result = await fetch(`${fastifyServer}/double`)
@@ -0,0 +1,37 @@
1
+ 'use strict'
2
+
3
+ const { ReadableStream } = require('node:stream/web')
4
+ const { test, after } = require('node:test')
5
+ const Fastify = require('..')
6
+
7
+ test('reply.send(web ReadableStream) throws if locked', async t => {
8
+ t.plan(3)
9
+
10
+ const app = Fastify()
11
+ after(() => app.close())
12
+
13
+ app.get('/', (req, reply) => {
14
+ const rs = new ReadableStream({
15
+ start (controller) { controller.enqueue(new TextEncoder().encode('hi')); controller.close() }
16
+ })
17
+ // lock the stream
18
+ const reader = rs.getReader()
19
+ t.assert.strictEqual(rs.locked, true, 'stream is locked')
20
+
21
+ // sending a locked stream should trigger the Fastify error
22
+ reply.send(rs)
23
+ reader.releaseLock()
24
+ })
25
+
26
+ const res = await app.inject({ method: 'GET', url: '/' })
27
+ t.assert.strictEqual(res.statusCode, 500)
28
+ t.assert.deepStrictEqual(
29
+ JSON.parse(res.body),
30
+ {
31
+ statusCode: 500,
32
+ code: 'FST_ERR_REP_READABLE_STREAM_LOCKED',
33
+ error: 'Internal Server Error',
34
+ message: 'ReadableStream was locked. You should call releaseLock() method on reader before sending.'
35
+ }
36
+ )
37
+ })
@@ -4,6 +4,9 @@ const { connect } = require('node:net')
4
4
  const { test } = require('node:test')
5
5
  const Fastify = require('..')
6
6
  const { kRequest } = require('../lib/symbols.js')
7
+ const split = require('split2')
8
+ const { Readable } = require('node:stream')
9
+ const { getServerUrl } = require('./helper')
7
10
 
8
11
  test('default 400 on request error', (t, done) => {
9
12
  t.plan(4)
@@ -465,3 +468,116 @@ test('test request.routeOptions.version', async t => {
465
468
  t.assert.ok(result2.ok)
466
469
  t.assert.strictEqual(result2.status, 200)
467
470
  })
471
+
472
+ test('customErrorHandler should throw for json err and stream response', async (t) => {
473
+ t.plan(5)
474
+
475
+ const logStream = split(JSON.parse)
476
+ const fastify = Fastify({
477
+ logger: {
478
+ stream: logStream,
479
+ level: 'error'
480
+ }
481
+ })
482
+ t.after(() => fastify.close())
483
+
484
+ fastify.get('/', async (req, reply) => {
485
+ const stream = new Readable({
486
+ read () {
487
+ this.push('hello')
488
+ }
489
+ })
490
+ process.nextTick(() => stream.destroy(new Error('stream error')))
491
+
492
+ reply.type('application/text')
493
+ await reply.send(stream)
494
+ })
495
+
496
+ fastify.setErrorHandler((err, req, reply) => {
497
+ t.assert.strictEqual(err.message, 'stream error')
498
+ reply.code(400)
499
+ reply.send({ error: err.message })
500
+ })
501
+
502
+ logStream.once('data', line => {
503
+ t.assert.strictEqual(line.msg, 'Attempted to send payload of invalid type \'object\'. Expected a string or Buffer.')
504
+ t.assert.strictEqual(line.level, 50)
505
+ })
506
+
507
+ await fastify.listen({ port: 0 })
508
+
509
+ const response = await fetch(getServerUrl(fastify) + '/')
510
+
511
+ t.assert.strictEqual(response.status, 500)
512
+ t.assert.deepStrictEqual(await response.json(), { statusCode: 500, code: 'FST_ERR_REP_INVALID_PAYLOAD_TYPE', error: 'Internal Server Error', message: "Attempted to send payload of invalid type 'object'. Expected a string or Buffer." })
513
+ })
514
+
515
+ test('customErrorHandler should not throw for json err and stream response with content-type defined', async (t) => {
516
+ t.plan(4)
517
+
518
+ const logStream = split(JSON.parse)
519
+ const fastify = Fastify({
520
+ logger: {
521
+ stream: logStream,
522
+ level: 'error'
523
+ }
524
+ })
525
+
526
+ t.after(() => fastify.close())
527
+
528
+ fastify.get('/', async (req, reply) => {
529
+ const stream = new Readable({
530
+ read () {
531
+ this.push('hello')
532
+ }
533
+ })
534
+ process.nextTick(() => stream.destroy(new Error('stream error')))
535
+
536
+ reply.type('application/text')
537
+ await reply.send(stream)
538
+ })
539
+
540
+ fastify.setErrorHandler((err, req, reply) => {
541
+ t.assert.strictEqual(err.message, 'stream error')
542
+ reply
543
+ .code(400)
544
+ .type('application/json')
545
+ .send({ error: err.message })
546
+ })
547
+
548
+ await fastify.listen({ port: 0 })
549
+
550
+ const response = await fetch(getServerUrl(fastify) + '/')
551
+
552
+ t.assert.strictEqual(response.status, 400)
553
+ t.assert.strictEqual(response.headers.get('content-type'), 'application/json; charset=utf-8')
554
+ t.assert.deepStrictEqual(await response.json(), { error: 'stream error' })
555
+ })
556
+
557
+ test('customErrorHandler should not call handler for in-stream error', async (t) => {
558
+ t.plan(1)
559
+
560
+ const fastify = Fastify()
561
+ t.after(() => fastify.close())
562
+
563
+ fastify.get('/', async (req, reply) => {
564
+ const stream = new Readable({
565
+ read () {
566
+ this.push('hello')
567
+ stream.destroy(new Error('stream error'))
568
+ }
569
+ })
570
+
571
+ reply.type('application/text')
572
+ await reply.send(stream)
573
+ })
574
+
575
+ fastify.setErrorHandler(() => {
576
+ t.assert.fail('must not be called')
577
+ })
578
+ await fastify.listen({ port: 0 })
579
+
580
+ await t.assert.rejects(fetch(getServerUrl(fastify) + '/'), {
581
+ message: 'fetch failed'
582
+ })
583
+ })
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const stream = require('node:stream')
4
+ const { ReadableStream } = require('node:stream/web')
4
5
  const { test } = require('node:test')
5
6
  const Fastify = require('..')
6
7
 
@@ -90,13 +91,14 @@ test('Will not create a HEAD route that is not GET', async t => {
90
91
  })
91
92
 
92
93
  test('HEAD route should handle properly each response type', async t => {
93
- t.plan(20)
94
+ t.plan(24)
94
95
 
95
96
  const fastify = Fastify({ exposeHeadRoutes: true })
96
97
  const resString = 'Found me!'
97
98
  const resJSON = { here: 'is Johnny' }
98
99
  const resBuffer = Buffer.from('I am a buffer!')
99
100
  const resStream = stream.Readable.from('I am a stream!')
101
+ const resWebStream = ReadableStream.from('I am a web stream!')
100
102
 
101
103
  fastify.route({
102
104
  method: 'GET',
@@ -139,6 +141,14 @@ test('HEAD route should handle properly each response type', async t => {
139
141
  }
140
142
  })
141
143
 
144
+ fastify.route({
145
+ method: 'GET',
146
+ path: '/web-stream',
147
+ handler: (req, reply) => {
148
+ return resWebStream
149
+ }
150
+ })
151
+
142
152
  let res = await fastify.inject({
143
153
  method: 'HEAD',
144
154
  url: '/json'
@@ -183,6 +193,15 @@ test('HEAD route should handle properly each response type', async t => {
183
193
  t.assert.strictEqual(res.headers['content-type'], undefined)
184
194
  t.assert.strictEqual(res.headers['content-length'], undefined)
185
195
  t.assert.strictEqual(res.body, '')
196
+
197
+ res = await fastify.inject({
198
+ method: 'HEAD',
199
+ url: '/web-stream'
200
+ })
201
+ t.assert.strictEqual(res.statusCode, 200)
202
+ t.assert.strictEqual(res.headers['content-type'], undefined)
203
+ t.assert.strictEqual(res.headers['content-length'], undefined)
204
+ t.assert.strictEqual(res.body, '')
186
205
  })
187
206
 
188
207
  test('HEAD route should respect custom onSend handlers', async t => {
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const stream = require('node:stream')
4
+ const { ReadableStream } = require('node:stream/web')
4
5
  const split = require('split2')
5
6
  const { test } = require('node:test')
6
7
  const Fastify = require('..')
@@ -47,6 +48,54 @@ test("HEAD route should handle stream.on('error')", (t, done) => {
47
48
  })
48
49
  })
49
50
 
51
+ test('HEAD route should handle ReadableStream.cancel() error', (t, done) => {
52
+ t.plan(7)
53
+
54
+ const logStream = split(JSON.parse)
55
+ const expectedError = new Error('Cancel error!')
56
+ const fastify = Fastify({
57
+ logger: {
58
+ stream: logStream,
59
+ level: 'error'
60
+ }
61
+ })
62
+
63
+ fastify.route({
64
+ method: 'GET',
65
+ path: '/web-stream',
66
+ exposeHeadRoute: true,
67
+ handler: (req, reply) => {
68
+ const webStream = new ReadableStream({
69
+ start (controller) {
70
+ controller.enqueue('Hello from web stream!')
71
+ },
72
+ cancel (reason) {
73
+ t.assert.strictEqual(reason, 'Stream cancelled by HEAD route')
74
+ throw expectedError
75
+ }
76
+ })
77
+ return webStream
78
+ }
79
+ })
80
+
81
+ logStream.once('data', line => {
82
+ const { message, stack } = expectedError
83
+ t.assert.deepStrictEqual(line.err, { type: 'Error', message, stack })
84
+ t.assert.strictEqual(line.msg, 'Error on Stream found for HEAD route')
85
+ t.assert.strictEqual(line.level, 50)
86
+ })
87
+
88
+ fastify.inject({
89
+ method: 'HEAD',
90
+ url: '/web-stream'
91
+ }, (error, res) => {
92
+ t.assert.ifError(error)
93
+ t.assert.strictEqual(res.statusCode, 200)
94
+ t.assert.strictEqual(res.headers['content-type'], undefined)
95
+ done()
96
+ })
97
+ })
98
+
50
99
  test('HEAD route should be exposed by default', async t => {
51
100
  t.plan(5)
52
101
 
@@ -447,6 +447,157 @@ test('Should honor disableRequestLogging option in frameworkErrors wrapper - FST
447
447
  )
448
448
  })
449
449
 
450
+ test('Should honor disableRequestLogging function in frameworkErrors wrapper - FST_ERR_BAD_URL', (t, done) => {
451
+ t.plan(4)
452
+
453
+ let logCallCount = 0
454
+ const logStream = split(JSON.parse)
455
+
456
+ const fastify = Fastify({
457
+ disableRequestLogging: (req) => {
458
+ // Disable logging for URLs containing 'silent'
459
+ return req.url.includes('silent')
460
+ },
461
+ frameworkErrors: function (err, req, res) {
462
+ res.send(`${err.message} - ${err.code}`)
463
+ },
464
+ logger: {
465
+ stream: logStream,
466
+ level: 'info'
467
+ }
468
+ })
469
+
470
+ fastify.get('/test/:id', (req, res) => {
471
+ res.send('{ hello: \'world\' }')
472
+ })
473
+
474
+ logStream.on('data', (json) => {
475
+ if (json.msg === 'incoming request') {
476
+ logCallCount++
477
+ }
478
+ })
479
+
480
+ // First request: URL does not contain 'silent', so logging should happen
481
+ fastify.inject(
482
+ {
483
+ method: 'GET',
484
+ url: '/test/%world'
485
+ },
486
+ (err, res) => {
487
+ t.assert.ifError(err)
488
+ t.assert.strictEqual(res.body, '\'/test/%world\' is not a valid url component - FST_ERR_BAD_URL')
489
+
490
+ // Second request: URL contains 'silent', so logging should be disabled
491
+ fastify.inject(
492
+ {
493
+ method: 'GET',
494
+ url: '/silent/%world'
495
+ },
496
+ (err2, res2) => {
497
+ t.assert.ifError(err2)
498
+ // Give time for any potential log events
499
+ setImmediate(() => {
500
+ // Only the first request should have logged
501
+ t.assert.strictEqual(logCallCount, 1)
502
+ done()
503
+ })
504
+ }
505
+ )
506
+ }
507
+ )
508
+ })
509
+
510
+ test('Should honor disableRequestLogging function in frameworkErrors wrapper - FST_ERR_ASYNC_CONSTRAINT', (t, done) => {
511
+ t.plan(4)
512
+
513
+ let logCallCount = 0
514
+
515
+ const constraint = {
516
+ name: 'secret',
517
+ storage: function () {
518
+ const secrets = {}
519
+ return {
520
+ get: (secret) => { return secrets[secret] || null },
521
+ set: (secret, store) => { secrets[secret] = store }
522
+ }
523
+ },
524
+ deriveConstraint: (req, ctx, done) => {
525
+ done(Error('kaboom'))
526
+ },
527
+ validate () { return true }
528
+ }
529
+
530
+ const logStream = split(JSON.parse)
531
+
532
+ const fastify = Fastify({
533
+ constraints: { secret: constraint },
534
+ disableRequestLogging: (req) => {
535
+ // Disable logging for URLs containing 'silent'
536
+ return req.url.includes('silent')
537
+ },
538
+ frameworkErrors: function (err, req, res) {
539
+ res.send(`${err.message} - ${err.code}`)
540
+ },
541
+ logger: {
542
+ stream: logStream,
543
+ level: 'info'
544
+ }
545
+ })
546
+
547
+ fastify.route({
548
+ method: 'GET',
549
+ url: '/',
550
+ constraints: { secret: 'alpha' },
551
+ handler: (req, reply) => {
552
+ reply.send({ hello: 'from alpha' })
553
+ }
554
+ })
555
+
556
+ fastify.route({
557
+ method: 'GET',
558
+ url: '/silent',
559
+ constraints: { secret: 'alpha' },
560
+ handler: (req, reply) => {
561
+ reply.send({ hello: 'from alpha' })
562
+ }
563
+ })
564
+
565
+ logStream.on('data', (json) => {
566
+ if (json.msg === 'incoming request') {
567
+ logCallCount++
568
+ }
569
+ })
570
+
571
+ // First request: URL does not contain 'silent', so logging should happen
572
+ fastify.inject(
573
+ {
574
+ method: 'GET',
575
+ url: '/'
576
+ },
577
+ (err, res) => {
578
+ t.assert.ifError(err)
579
+ t.assert.strictEqual(res.body, 'Unexpected error from async constraint - FST_ERR_ASYNC_CONSTRAINT')
580
+
581
+ // Second request: URL contains 'silent', so logging should be disabled
582
+ fastify.inject(
583
+ {
584
+ method: 'GET',
585
+ url: '/silent'
586
+ },
587
+ (err2, res2) => {
588
+ t.assert.ifError(err2)
589
+ // Give time for any potential log events
590
+ setImmediate(() => {
591
+ // Only the first request should have logged
592
+ t.assert.strictEqual(logCallCount, 1)
593
+ done()
594
+ })
595
+ }
596
+ )
597
+ }
598
+ )
599
+ })
600
+
450
601
  test('Should honor routerOptions.defaultRoute', async t => {
451
602
  t.plan(3)
452
603
  const fastify = Fastify({
@@ -895,3 +1046,21 @@ test('Should honor routerOptions.useSemicolonDelimiter over useSemicolonDelimite
895
1046
  })
896
1047
  t.assert.strictEqual(res.statusCode, 200)
897
1048
  })
1049
+
1050
+ test('Should support extra find-my-way options', async t => {
1051
+ t.plan(1)
1052
+ // Use a real upstream option from find-my-way
1053
+ const fastify = Fastify({
1054
+ routerOptions: {
1055
+ buildPrettyMeta: (route) => {
1056
+ const cleanMeta = Object.assign({}, route.store)
1057
+ return cleanMeta
1058
+ }
1059
+ }
1060
+ })
1061
+
1062
+ await fastify.ready()
1063
+
1064
+ // Ensure the option is preserved after validation
1065
+ t.assert.strictEqual(typeof fastify.initialConfig.routerOptions.buildPrettyMeta, 'function')
1066
+ })
@@ -1241,7 +1241,13 @@ test('Custom validator builder override by custom validator compiler', async t =
1241
1241
  }
1242
1242
  const ajv1 = new AJV(ajvDefaults).addKeyword({ keyword: 'extended_one', type: 'object', validator: () => true })
1243
1243
  const ajv2 = new AJV(ajvDefaults).addKeyword({ keyword: 'extended_two', type: 'object', validator: () => true })
1244
- const fastify = Fastify({ schemaController: { compilersFactory: { buildValidator: () => (routeSchemaDef) => ajv1.compile(routeSchemaDef.schema) } } })
1244
+ const fastify = Fastify({
1245
+ schemaController: {
1246
+ compilersFactory: {
1247
+ buildValidator: () => (routeSchemaDef) => ajv1.compile(routeSchemaDef.schema)
1248
+ }
1249
+ }
1250
+ })
1245
1251
 
1246
1252
  fastify.setValidatorCompiler((routeSchemaDef) => ajv2.compile(routeSchemaDef.schema))
1247
1253
 
@@ -1281,7 +1287,13 @@ test('Custom validator builder override by custom validator compiler in child in
1281
1287
  }
1282
1288
  const ajv1 = new AJV(ajvDefaults).addKeyword({ keyword: 'extended_one', type: 'object', validator: () => true })
1283
1289
  const ajv2 = new AJV(ajvDefaults).addKeyword({ keyword: 'extended_two', type: 'object', validator: () => true })
1284
- const fastify = Fastify({ schemaController: { compilersFactory: { buildValidator: () => (routeSchemaDef) => ajv1.compile(routeSchemaDef.schema) } } })
1290
+ const fastify = Fastify({
1291
+ schemaController: {
1292
+ compilersFactory: {
1293
+ buildValidator: () => (routeSchemaDef) => ajv1.compile(routeSchemaDef.schema)
1294
+ }
1295
+ }
1296
+ })
1285
1297
 
1286
1298
  fastify.register((embedded, _opts, done) => {
1287
1299
  embedded.setValidatorCompiler((routeSchemaDef) => ajv2.compile(routeSchemaDef.schema))
@@ -1426,6 +1438,17 @@ test('Schema validation will not be bypass by different content type', async t =
1426
1438
  t.assert.strictEqual(correct2.status, 200)
1427
1439
  await correct2.bytes()
1428
1440
 
1441
+ const correct3 = await fetch(address, {
1442
+ method: 'POST',
1443
+ url: '/',
1444
+ headers: {
1445
+ 'content-type': 'application/json\t; charset=utf-8'
1446
+ },
1447
+ body: JSON.stringify({ foo: 'string' })
1448
+ })
1449
+ t.assert.strictEqual(correct2.status, 200)
1450
+ await correct3.bytes()
1451
+
1429
1452
  const invalid1 = await fetch(address, {
1430
1453
  method: 'POST',
1431
1454
  url: '/',
@@ -1489,8 +1512,8 @@ test('Schema validation will not be bypass by different content type', async t =
1489
1512
  },
1490
1513
  body: JSON.stringify({ invalid: 'string' })
1491
1514
  })
1492
- t.assert.strictEqual(invalid6.status, 415)
1493
- t.assert.strictEqual((await invalid6.json()).code, 'FST_ERR_CTP_INVALID_MEDIA_TYPE')
1515
+ t.assert.strictEqual(invalid6.status, 400)
1516
+ t.assert.strictEqual((await invalid6.json()).code, 'FST_ERR_VALIDATION')
1494
1517
 
1495
1518
  const invalid7 = await fetch(address, {
1496
1519
  method: 'POST',
@@ -1,11 +1,14 @@
1
1
  'use strict'
2
2
 
3
3
  const dns = require('node:dns')
4
+ const { networkInterfaces } = require('node:os')
4
5
  const { test } = require('node:test')
5
6
  const Fastify = require('..')
6
7
  const undici = require('undici')
7
8
  const proxyquire = require('proxyquire')
8
9
 
10
+ const isIPv6Missing = !Object.values(networkInterfaces()).flat().some(({ family }) => family === 'IPv6')
11
+
9
12
  test('listen should accept null port', async t => {
10
13
  const fastify = Fastify()
11
14
  t.after(() => fastify.close())
@@ -67,13 +70,13 @@ test('listen should reject string port', async (t) => {
67
70
  })
68
71
 
69
72
  test('Test for hostname and port', async (t) => {
73
+ t.plan(3)
70
74
  const app = Fastify()
71
75
  t.after(() => app.close())
72
76
  app.get('/host', (req, res) => {
73
- const host = 'localhost:8000'
74
- t.assert.strictEqual(req.host, host)
75
- t.assert.strictEqual(req.hostname, req.host.split(':')[0])
76
- t.assert.strictEqual(req.port, Number(req.host.split(':')[1]))
77
+ t.assert.strictEqual(req.host, 'localhost:8000')
78
+ t.assert.strictEqual(req.hostname, 'localhost')
79
+ t.assert.strictEqual(req.port, 8000)
77
80
  res.send('ok')
78
81
  })
79
82
 
@@ -81,6 +84,24 @@ test('Test for hostname and port', async (t) => {
81
84
  await fetch('http://localhost:8000/host')
82
85
  })
83
86
 
87
+ test('Test for IPV6 port', { skip: isIPv6Missing }, async (t) => {
88
+ t.plan(3)
89
+ const app = Fastify()
90
+ t.after(() => app.close())
91
+ app.get('/host', (req, res) => {
92
+ t.assert.strictEqual(req.host, '[::1]:3040')
93
+ t.assert.strictEqual(req.hostname, '[::1]')
94
+ t.assert.strictEqual(req.port, 3040)
95
+ res.send('ok')
96
+ })
97
+
98
+ await app.listen({
99
+ port: 3040,
100
+ host: '::1'
101
+ })
102
+ await fetch('http://[::1]:3040/host')
103
+ })
104
+
84
105
  test('abort signal', async t => {
85
106
  await t.test('should close server when aborted after', (t, end) => {
86
107
  t.plan(2)
@@ -35,8 +35,8 @@ test('should mark reply as sent before pumping the payload stream into response
35
35
  t.plan(2)
36
36
  t.after(() => fastify.close())
37
37
 
38
- const handleRequest = proxyquire('../lib/handleRequest', {
39
- './wrapThenable': (thenable, reply) => {
38
+ const handleRequest = proxyquire('../lib/handle-request', {
39
+ './wrap-thenable': (thenable, reply) => {
40
40
  thenable.then(function (payload) {
41
41
  t.assert.strictEqual(reply.sent, true)
42
42
  })
@@ -44,7 +44,7 @@ test('should mark reply as sent before pumping the payload stream into response
44
44
  })
45
45
 
46
46
  const route = proxyquire('../lib/route', {
47
- './handleRequest': handleRequest
47
+ './handle-request': handleRequest
48
48
  })
49
49
 
50
50
  const Fastify = proxyquire('..', {