fastify 6.0.0-alpha.5 → 6.0.0-alpha.6

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 (46) hide show
  1. package/README.md +1 -0
  2. package/build/build-validation.js +2 -0
  3. package/docs/Guides/Ecosystem.md +4 -0
  4. package/docs/Guides/Migration-Guide-V5.md +2 -2
  5. package/docs/Guides/Serverless.md +33 -10
  6. package/docs/Reference/Errors.md +13 -0
  7. package/docs/Reference/Hooks.md +13 -0
  8. package/docs/Reference/Reply.md +786 -697
  9. package/docs/Reference/Request.md +303 -107
  10. package/docs/Reference/Server.md +39 -2
  11. package/docs/Reference/Validation-and-Serialization.md +15 -1
  12. package/fastify.d.ts +1 -0
  13. package/fastify.js +35 -1
  14. package/lib/config-validator.js +102 -73
  15. package/lib/content-type-parser.js +14 -2
  16. package/lib/context.js +2 -1
  17. package/lib/decorate.js +4 -3
  18. package/lib/error-handler.js +21 -4
  19. package/lib/error-status.js +4 -2
  20. package/lib/errors.js +15 -9
  21. package/lib/reply.js +12 -11
  22. package/lib/request.js +3 -2
  23. package/lib/route.js +34 -5
  24. package/lib/schemas.js +44 -32
  25. package/package.json +8 -3
  26. package/test/close-disposable.test.js +154 -0
  27. package/test/constrained-routes.test.js +4 -2
  28. package/test/content-parser.test.js +39 -0
  29. package/test/decorator.test.js +29 -0
  30. package/test/diagnostics-channel/error-status.test.js +35 -2
  31. package/test/handler-timeout.test.js +224 -0
  32. package/test/internals/all.test.js +5 -0
  33. package/test/internals/errors.test.js +18 -9
  34. package/test/internals/initial-config.test.js +2 -0
  35. package/test/internals/server.test.js +6 -4
  36. package/test/reply-error.test.js +82 -0
  37. package/test/reply-trailers.test.js +159 -1
  38. package/test/router-compile.test.js +241 -0
  39. package/test/schema-serialization.test.js +120 -7
  40. package/test/types/fastify.tst.ts +4 -0
  41. package/test/types/instance.tst.ts +1 -1
  42. package/test/types/route.tst.ts +16 -0
  43. package/test/types/using.tst.ts +20 -0
  44. package/test/validation-error-handling.test.js +21 -1
  45. package/types/instance.d.ts +5 -1
  46. package/types/route.d.ts +1 -0
@@ -0,0 +1,241 @@
1
+ 'use strict'
2
+
3
+ const { test } = require('node:test')
4
+ const FindMyWay = require('find-my-way')
5
+ const Fastify = require('..')
6
+ const { FST_ERR_INIT_OPTS_INVALID } = require('../lib/errors')
7
+
8
+ const RouterPrototype = Object.getPrototypeOf(FindMyWay())
9
+
10
+ function buildApp (opts) {
11
+ const fastify = Fastify(opts)
12
+
13
+ fastify.get('/', () => ({ route: 'root' }))
14
+ fastify.get('/users/:id', (req) => ({ route: 'user', id: req.params.id }))
15
+ fastify.post('/users', (req) => ({ route: 'create', body: req.body }))
16
+ fastify.get('/files/*', (req) => ({ route: 'files', path: req.params['*'] }))
17
+ fastify.get('/search', (req) => ({ route: 'search', query: req.query }))
18
+ fastify.get('/versioned', { constraints: { version: '1.2.0' } }, () => ({ route: 'v1' }))
19
+ fastify.get('/versioned', { constraints: { version: '2.0.0' } }, () => ({ route: 'v2' }))
20
+
21
+ return fastify
22
+ }
23
+
24
+ async function assertRoutes (t, fastify) {
25
+ let res = await fastify.inject('/')
26
+ t.assert.deepStrictEqual(res.json(), { route: 'root' })
27
+
28
+ res = await fastify.inject('/users/42')
29
+ t.assert.deepStrictEqual(res.json(), { route: 'user', id: '42' })
30
+
31
+ res = await fastify.inject('/users/caf%C3%A9')
32
+ t.assert.deepStrictEqual(res.json(), { route: 'user', id: 'café' })
33
+
34
+ res = await fastify.inject({ method: 'POST', url: '/users', payload: { name: 'foo' } })
35
+ t.assert.deepStrictEqual(res.json(), { route: 'create', body: { name: 'foo' } })
36
+
37
+ res = await fastify.inject('/files/a/b/c.txt')
38
+ t.assert.deepStrictEqual(res.json(), { route: 'files', path: 'a/b/c.txt' })
39
+
40
+ res = await fastify.inject('/search?q=fastify')
41
+ t.assert.deepStrictEqual(res.json(), { route: 'search', query: { q: 'fastify' } })
42
+
43
+ res = await fastify.inject({ url: '/versioned', headers: { 'accept-version': '2.x' } })
44
+ t.assert.deepStrictEqual(res.json(), { route: 'v2' })
45
+
46
+ res = await fastify.inject({ method: 'HEAD', url: '/users/42' })
47
+ t.assert.strictEqual(res.statusCode, 200)
48
+ t.assert.strictEqual(res.body, '')
49
+
50
+ res = await fastify.inject('/not-found')
51
+ t.assert.strictEqual(res.statusCode, 404)
52
+
53
+ res = await fastify.inject({ method: 'DELETE', url: '/users/42' })
54
+ t.assert.strictEqual(res.statusCode, 404)
55
+ }
56
+
57
+ test('routerOptions.compile defaults to false', async t => {
58
+ const compile = t.mock.method(RouterPrototype, 'compile')
59
+ const fastify = buildApp()
60
+ t.after(() => fastify.close())
61
+
62
+ t.assert.strictEqual(fastify.initialConfig.routerOptions.compile, false)
63
+ await assertRoutes(t, fastify)
64
+ t.assert.strictEqual(compile.mock.callCount(), 0)
65
+ })
66
+
67
+ test('routerOptions.compile: true compiles the routes on ready', async t => {
68
+ const compile = t.mock.method(RouterPrototype, 'compile')
69
+ const fastify = buildApp({ routerOptions: { compile: true } })
70
+ t.after(() => fastify.close())
71
+
72
+ t.assert.strictEqual(fastify.initialConfig.routerOptions.compile, true)
73
+ await fastify.ready()
74
+ // the main router and the 404 router
75
+ t.assert.strictEqual(compile.mock.callCount(), 2)
76
+ await assertRoutes(t, fastify)
77
+ t.assert.strictEqual(compile.mock.callCount(), 2)
78
+ })
79
+
80
+ test('routerOptions.compile: true works with real http requests', async t => {
81
+ const fastify = buildApp({ routerOptions: { compile: true } })
82
+ t.after(() => fastify.close())
83
+
84
+ const address = await fastify.listen({ port: 0 })
85
+
86
+ let res = await fetch(address + '/users/42')
87
+ t.assert.deepStrictEqual(await res.json(), { route: 'user', id: '42' })
88
+
89
+ res = await fetch(address + '/files/a/b')
90
+ t.assert.deepStrictEqual(await res.json(), { route: 'files', path: 'a/b' })
91
+
92
+ res = await fetch(address + '/nope')
93
+ t.assert.strictEqual(res.status, 404)
94
+ })
95
+
96
+ test('routerOptions.compile: true honors the other router options', async t => {
97
+ const fastify = Fastify({
98
+ routerOptions: {
99
+ compile: true,
100
+ ignoreTrailingSlash: true,
101
+ ignoreDuplicateSlashes: true,
102
+ caseSensitive: false,
103
+ maxParamLength: 5
104
+ }
105
+ })
106
+ t.after(() => fastify.close())
107
+
108
+ fastify.get('/foo/:id', (req) => ({ id: req.params.id }))
109
+
110
+ let res = await fastify.inject('/foo/abc/')
111
+ t.assert.deepStrictEqual(res.json(), { id: 'abc' })
112
+
113
+ res = await fastify.inject('//FOO//abc')
114
+ t.assert.deepStrictEqual(res.json(), { id: 'abc' })
115
+
116
+ res = await fastify.inject('/foo/toolongparam')
117
+ t.assert.strictEqual(res.statusCode, 414)
118
+ })
119
+
120
+ test('routerOptions.compile: true supports rewriteUrl', async t => {
121
+ const fastify = Fastify({
122
+ routerOptions: { compile: true },
123
+ rewriteUrl: (req) => req.url === '/old' ? '/new' : req.url
124
+ })
125
+ t.after(() => fastify.close())
126
+
127
+ fastify.get('/new', () => ({ route: 'new' }))
128
+
129
+ const res = await fastify.inject('/old')
130
+ t.assert.deepStrictEqual(res.json(), { route: 'new' })
131
+ })
132
+
133
+ test('routerOptions.compile: true supports asynchronous constraints', async t => {
134
+ const fastify = Fastify({
135
+ routerOptions: {
136
+ compile: true,
137
+ constraints: {
138
+ secret: {
139
+ name: 'secret',
140
+ storage () {
141
+ const secrets = {}
142
+ return {
143
+ get: (secret) => secrets[secret] ?? null,
144
+ set: (secret, store) => { secrets[secret] = store }
145
+ }
146
+ },
147
+ deriveConstraint (req, ctx, done) {
148
+ setImmediate(() => done(null, req.headers['x-secret']))
149
+ },
150
+ validate () { return true }
151
+ }
152
+ }
153
+ }
154
+ })
155
+ t.after(() => fastify.close())
156
+
157
+ fastify.get('/', { constraints: { secret: 'alpha' } }, () => ({ secret: 'alpha' }))
158
+ fastify.get('/', { constraints: { secret: 'beta' } }, () => ({ secret: 'beta' }))
159
+
160
+ let res = await fastify.inject({ url: '/', headers: { 'x-secret': 'beta' } })
161
+ t.assert.deepStrictEqual(res.json(), { secret: 'beta' })
162
+
163
+ res = await fastify.inject({ url: '/', headers: { 'x-secret': 'gamma' } })
164
+ t.assert.strictEqual(res.statusCode, 404)
165
+ })
166
+
167
+ test('routerOptions.compile: true supports encapsulated 404 handlers', async t => {
168
+ const fastify = Fastify({ routerOptions: { compile: true } })
169
+ t.after(() => fastify.close())
170
+
171
+ fastify.register(async (instance) => {
172
+ instance.setNotFoundHandler((req, reply) => {
173
+ reply.code(404).send({ notFound: 'api' })
174
+ })
175
+ instance.get('/ok', () => ({ ok: true }))
176
+ }, { prefix: '/api' })
177
+
178
+ let res = await fastify.inject('/api/ok')
179
+ t.assert.deepStrictEqual(res.json(), { ok: true })
180
+
181
+ res = await fastify.inject('/api/missing')
182
+ t.assert.deepStrictEqual(res.json(), { notFound: 'api' })
183
+ })
184
+
185
+ test('routerOptions.compile: true compiles the 404 router', async t => {
186
+ const fastify = Fastify({ routerOptions: { compile: true } })
187
+ t.after(() => fastify.close())
188
+
189
+ fastify.setNotFoundHandler((req, reply) => {
190
+ reply.code(404).send({ notFound: 'root', method: req.method })
191
+ })
192
+
193
+ fastify.register(async (instance) => {
194
+ instance.setNotFoundHandler((req, reply) => {
195
+ reply.code(404).send({ notFound: 'api', method: req.method })
196
+ })
197
+
198
+ instance.register(async (instance) => {
199
+ instance.setNotFoundHandler((req, reply) => {
200
+ reply.code(404).send({ notFound: 'v1', method: req.method })
201
+ })
202
+ }, { prefix: '/v1' })
203
+ }, { prefix: '/api' })
204
+
205
+ for (const method of ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']) {
206
+ let res = await fastify.inject({ method, url: '/missing' })
207
+ t.assert.deepStrictEqual(res.json(), { notFound: 'root', method })
208
+
209
+ res = await fastify.inject({ method, url: '/api/missing?x=1' })
210
+ t.assert.deepStrictEqual(res.json(), { notFound: 'api', method })
211
+
212
+ res = await fastify.inject({ method, url: '/api/v1/a/b/c' })
213
+ t.assert.deepStrictEqual(res.json(), { notFound: 'v1', method })
214
+ }
215
+
216
+ let res = await fastify.inject('/api')
217
+ t.assert.deepStrictEqual(res.json(), { notFound: 'api', method: 'GET' })
218
+
219
+ res = await fastify.inject('/api/v1/caf%C3%A9')
220
+ t.assert.deepStrictEqual(res.json(), { notFound: 'v1', method: 'GET' })
221
+ })
222
+
223
+ test('routerOptions.compile: true keeps findRoute and hasRoute working', async t => {
224
+ const fastify = Fastify({ routerOptions: { compile: true } })
225
+ t.after(() => fastify.close())
226
+
227
+ fastify.get('/users/:id', () => ({}))
228
+ await fastify.ready()
229
+
230
+ t.assert.ok(fastify.hasRoute({ method: 'GET', url: '/users/:id' }))
231
+ const route = fastify.findRoute({ method: 'GET', url: '/users/42' })
232
+ t.assert.deepStrictEqual({ ...route.params }, { id: '42' })
233
+ t.assert.strictEqual(fastify.findRoute({ method: 'GET', url: '/nope' }), null)
234
+ })
235
+
236
+ test('routerOptions.compile must be a boolean', t => {
237
+ t.assert.throws(
238
+ () => Fastify({ routerOptions: { compile: 'yes' } }),
239
+ FST_ERR_INIT_OPTS_INVALID
240
+ )
241
+ })
@@ -243,7 +243,7 @@ test('Different content types', (t, testDone) => {
243
243
  reply.send({ details: 'validation error' })
244
244
  break
245
245
  default:
246
- // to test if schema not found
246
+ // to test failing closed on an undeclared content-type
247
247
  reply.header('Content-Type', 'application/vnd.v3+json')
248
248
  reply.code(200)
249
249
  reply.send([{ type: 'student', grade: 6 }, { type: 'student', grade: 9 }])
@@ -320,8 +320,8 @@ test('Different content types', (t, testDone) => {
320
320
  })
321
321
  fastify.inject({ method: 'GET', url: '/' }, (err, res) => {
322
322
  t.assert.ifError(err)
323
- t.assert.strictEqual(res.payload, JSON.stringify([{ type: 'student', grade: 6 }, { type: 'student', grade: 9 }]))
324
- t.assert.strictEqual(res.statusCode, 200)
323
+ t.assert.strictEqual(res.statusCode, 500)
324
+ t.assert.strictEqual(res.payload, '{}')
325
325
  completion.stepIn()
326
326
  })
327
327
  fastify.inject({ method: 'GET', url: '/', headers: { Accept: 'application/vnd.v2+json' } }, (err, res) => {
@@ -332,8 +332,8 @@ test('Different content types', (t, testDone) => {
332
332
  })
333
333
  fastify.inject({ method: 'GET', url: '/', headers: { Accept: 'application/vnd.v3+json' } }, (err, res) => {
334
334
  t.assert.ifError(err)
335
- t.assert.strictEqual(res.payload, JSON.stringify({ firstName: 'New', lastName: 'Hoo', country: 'eg', city: 'node' }))
336
- t.assert.strictEqual(res.statusCode, 300)
335
+ t.assert.strictEqual(res.statusCode, 500)
336
+ t.assert.strictEqual(res.payload, '{}')
337
337
  completion.stepIn()
338
338
  })
339
339
  fastify.inject({ method: 'GET', url: '/', headers: { Accept: 'application/vnd.v4+json' } }, (err, res) => {
@@ -386,8 +386,8 @@ test('Different content types', (t, testDone) => {
386
386
  })
387
387
  fastify.inject({ method: 'GET', url: '/test', headers: { Accept: 'application/vnd.v1+json' } }, (err, res) => {
388
388
  t.assert.ifError(err)
389
- t.assert.strictEqual(res.payload, JSON.stringify({ created: true }))
390
- t.assert.strictEqual(res.statusCode, 201)
389
+ t.assert.strictEqual(res.statusCode, 500)
390
+ t.assert.strictEqual(res.payload, '{}')
391
391
  completion.stepIn()
392
392
  })
393
393
 
@@ -1310,3 +1310,116 @@ test('response schema is applied when Content-Type has trailing semicolon only',
1310
1310
  testDone()
1311
1311
  })
1312
1312
  })
1313
+
1314
+ test('fails closed when the response content-type is not declared in the content map', (t, testDone) => {
1315
+ t.plan(3)
1316
+
1317
+ const fastify = Fastify()
1318
+ fastify.get('/', {
1319
+ schema: {
1320
+ response: {
1321
+ 200: {
1322
+ content: {
1323
+ 'application/json': {
1324
+ schema: {
1325
+ type: 'object',
1326
+ properties: {
1327
+ name: { type: 'string' }
1328
+ }
1329
+ }
1330
+ }
1331
+ }
1332
+ }
1333
+ }
1334
+ }
1335
+ }, function (req, reply) {
1336
+ reply.header('Content-Type', 'application/hal+json')
1337
+ reply.send({ name: 'alice', password: 'SHOULD_NOT_LEAK' })
1338
+ })
1339
+
1340
+ fastify.inject('/', (err, res) => {
1341
+ t.assert.ifError(err)
1342
+ t.assert.strictEqual(res.statusCode, 500)
1343
+ t.assert.strictEqual(
1344
+ JSON.parse(res.payload).code,
1345
+ 'FST_ERR_MISSING_CONTENTTYPE_SERIALIZATION_FN'
1346
+ )
1347
+ testDone()
1348
+ })
1349
+ })
1350
+
1351
+ test('response content-type matching */* still serialises when no exact content-type is declared', (t, testDone) => {
1352
+ t.plan(3)
1353
+
1354
+ const fastify = Fastify()
1355
+ fastify.get('/', {
1356
+ schema: {
1357
+ response: {
1358
+ 200: {
1359
+ content: {
1360
+ '*/*': {
1361
+ schema: {
1362
+ type: 'object',
1363
+ required: ['name'],
1364
+ additionalProperties: false,
1365
+ properties: {
1366
+ name: { type: 'string' }
1367
+ }
1368
+ }
1369
+ }
1370
+ }
1371
+ }
1372
+ }
1373
+ }
1374
+ }, function (req, reply) {
1375
+ reply.header('Content-Type', 'application/hal+json')
1376
+ reply.send({ name: 'alice', password: 'SHOULD_BE_STRIPPED' })
1377
+ })
1378
+
1379
+ fastify.inject('/', (err, res) => {
1380
+ t.assert.ifError(err)
1381
+ t.assert.strictEqual(res.statusCode, 200)
1382
+ t.assert.deepStrictEqual(res.json(), { name: 'alice' })
1383
+ testDone()
1384
+ })
1385
+ })
1386
+
1387
+ test('error responses fall back to the default error serializer when the content map does not declare the error content type', (t, testDone) => {
1388
+ t.plan(3)
1389
+
1390
+ const fastify = Fastify()
1391
+ // The content map does not declare `application/json` and has no `*/*`
1392
+ // catch-all, so the error handler must fall back to the default serializer
1393
+ // instead of failing closed.
1394
+ fastify.get('/', {
1395
+ schema: {
1396
+ response: {
1397
+ default: {
1398
+ content: {
1399
+ 'application/problem+json': {
1400
+ schema: {
1401
+ type: 'object',
1402
+ properties: {
1403
+ title: { type: 'string' }
1404
+ }
1405
+ }
1406
+ }
1407
+ }
1408
+ }
1409
+ }
1410
+ }
1411
+ }, function (req, reply) {
1412
+ throw new Error('boom')
1413
+ })
1414
+
1415
+ fastify.inject('/', (err, res) => {
1416
+ t.assert.ifError(err)
1417
+ t.assert.strictEqual(res.statusCode, 500)
1418
+ t.assert.deepStrictEqual(res.json(), {
1419
+ statusCode: 500,
1420
+ error: 'Internal Server Error',
1421
+ message: 'boom'
1422
+ })
1423
+ testDone()
1424
+ })
1425
+ })
@@ -361,5 +361,9 @@ fastify({ routerOptions: { allowUnsafeRegex: true } })
361
361
  fastify({ routerOptions: { allowUnsafeRegex: false } })
362
362
  expect(fastify).type.not.toBeCallableWith({ routerOptions: { allowUnsafeRegex: 'invalid' } })
363
363
 
364
+ fastify({ routerOptions: { compile: true } })
365
+ fastify({ routerOptions: { compile: false } })
366
+ expect(fastify).type.not.toBeCallableWith({ routerOptions: { compile: 'invalid' } })
367
+
364
368
  expect(fastify({ allowErrorHandlerOverride: true })).type.toBeAssignableTo<FastifyInstance>()
365
369
  expect(fastify({ allowErrorHandlerOverride: false })).type.toBeAssignableTo<FastifyInstance>()
@@ -29,7 +29,7 @@ expect(server.addSchema({ schemas: [] })).type.toBe<FastifyInstance>()
29
29
  expect(server.pluginName).type.toBe<string>()
30
30
 
31
31
  expect(server.getSchemas()).type.toBe<Record<string, unknown>>()
32
- expect(server.addresses()).type.toBe<AddressInfo[]>()
32
+ expect(server.addresses()).type.toBe<Array<AddressInfo | string>>()
33
33
  expect(server.getSchema('SchemaId')).type.toBe<unknown>()
34
34
  expect(server.printRoutes()).type.toBe<string>()
35
35
  expect(server.printPlugins()).type.toBe<string>()
@@ -83,6 +83,22 @@ fastify().get(
83
83
  }
84
84
  )
85
85
 
86
+ fastify().get(
87
+ '/',
88
+ { logLevel: 'info', logSerializers: { test: (value: number) => 'X' + value } },
89
+ routeHandler
90
+ )
91
+
92
+ fastify().route({
93
+ method: 'GET',
94
+ url: '/',
95
+ logSerializers: {
96
+ // serializers may return objects as well as strings
97
+ req: (req: FastifyRequest) => ({ url: req.url })
98
+ },
99
+ handler: routeHandler
100
+ })
101
+
86
102
  type LowerCaseHTTPMethods = 'delete' | 'get' | 'head' | 'patch' | 'post' | 'put' |
87
103
  'options' | 'query' | 'propfind' | 'proppatch' | 'mkcol' | 'copy' | 'move' | 'lock' |
88
104
  'unlock' | 'trace' | 'search' | 'mkcalendar' | 'report'
@@ -12,3 +12,23 @@ test("has 'Symbol.dispose'", async () => {
12
12
  expect(app).type.toBeAssignableTo<FastifyInstance>()
13
13
  expect(app[Symbol.asyncDispose]).type.toBe<() => Promise<undefined>>()
14
14
  })
15
+
16
+ test("addHook('onClose') accepts AsyncDisposable", () => {
17
+ const app = fastify()
18
+
19
+ const asyncDisposable: AsyncDisposable = {
20
+ async [Symbol.asyncDispose] () {}
21
+ }
22
+
23
+ expect(app.addHook('onClose', asyncDisposable)).type.toBeAssignableTo<FastifyInstance>()
24
+ })
25
+
26
+ test("addHook('onClose') accepts Disposable", () => {
27
+ const app = fastify()
28
+
29
+ const syncDisposable: Disposable = {
30
+ [Symbol.dispose] () {}
31
+ }
32
+
33
+ expect(app.addHook('onClose', syncDisposable)).type.toBeAssignableTo<FastifyInstance>()
34
+ })
@@ -252,6 +252,26 @@ test('attached validationError exposes the same message sent by the default hand
252
252
  t.assert.strictEqual(notAttached.statusCode, 400)
253
253
  })
254
254
 
255
+ test('default validation errors are FST_ERR_VALIDATION errors without a stack trace', async (t) => {
256
+ t.plan(5)
257
+
258
+ const fastify = Fastify()
259
+
260
+ let validationError
261
+ fastify.post('/', { schema, attachValidation: true }, function (req, reply) {
262
+ validationError = req.validationError
263
+ reply.code(400).send()
264
+ })
265
+
266
+ await fastify.inject({ method: 'POST', payload: { hello: 'michelangelo' }, url: '/' })
267
+
268
+ t.assert.ok(validationError instanceof Fastify.errorCodes.FST_ERR_VALIDATION)
269
+ t.assert.ok(validationError instanceof Error)
270
+ t.assert.strictEqual(validationError.name, 'FastifyError')
271
+ t.assert.strictEqual(validationError.message, "body must have required property 'name'")
272
+ t.assert.strictEqual(validationError.stack, undefined)
273
+ })
274
+
255
275
  test('should respect when attachValidation is explicitly set to false', async (t) => {
256
276
  t.plan(2)
257
277
 
@@ -303,7 +323,7 @@ test('Attached validation error should take precedence over setErrorHandler', as
303
323
  url: '/'
304
324
  })
305
325
 
306
- t.assert.deepStrictEqual(response.payload, "Attached: Error: body must have required property 'name'")
326
+ t.assert.deepStrictEqual(response.payload, "Attached: FastifyError [FST_ERR_VALIDATION]: body must have required property 'name'")
307
327
  t.assert.strictEqual(response.statusCode, 400)
308
328
  })
309
329
 
@@ -130,7 +130,7 @@ export interface FastifyInstance<
130
130
  version: string;
131
131
  log: Logger;
132
132
  listeningOrigin: string;
133
- addresses(): AddressInfo[]
133
+ addresses(): Array<AddressInfo | string>
134
134
  withTypeProvider<Provider extends FastifyTypeProvider>(): FastifyInstance<RawServer, RawRequest, RawReply, Logger,
135
135
  Provider>;
136
136
 
@@ -485,6 +485,10 @@ export interface FastifyInstance<
485
485
  /**
486
486
  * Triggered when fastify.close() is invoked to stop the server. It is useful when plugins need a "shutdown" event, for example to close an open connection to a database.
487
487
  */
488
+ addHook(
489
+ name: 'onClose',
490
+ hook: AsyncDisposable | Disposable,
491
+ ): FastifyInstance<RawServer, RawRequest, RawReply, Logger, TypeProvider>;
488
492
  addHook<
489
493
  Fn extends onCloseHookHandler | onCloseAsyncHookHandler = onCloseHookHandler
490
494
  >(
package/types/route.d.ts CHANGED
@@ -68,6 +68,7 @@ export interface RouteShorthandOptions<
68
68
  bodyLimit?: number;
69
69
  handlerTimeout?: number;
70
70
  logLevel?: LogLevel;
71
+ logSerializers?: Record<string, (value: any) => any>;
71
72
  config?: FastifyContextConfig & ContextConfig;
72
73
  constraints?: RouteConstraint,
73
74
  prefixTrailingSlash?: 'slash' | 'no-slash' | 'both';