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.
- package/README.md +1 -0
- package/build/build-validation.js +2 -0
- package/docs/Guides/Ecosystem.md +4 -0
- package/docs/Guides/Migration-Guide-V5.md +2 -2
- package/docs/Guides/Serverless.md +33 -10
- package/docs/Reference/Errors.md +13 -0
- package/docs/Reference/Hooks.md +13 -0
- package/docs/Reference/Reply.md +786 -697
- package/docs/Reference/Request.md +303 -107
- package/docs/Reference/Server.md +39 -2
- package/docs/Reference/Validation-and-Serialization.md +15 -1
- package/fastify.d.ts +1 -0
- package/fastify.js +35 -1
- package/lib/config-validator.js +102 -73
- package/lib/content-type-parser.js +14 -2
- package/lib/context.js +2 -1
- package/lib/decorate.js +4 -3
- package/lib/error-handler.js +21 -4
- package/lib/error-status.js +4 -2
- package/lib/errors.js +15 -9
- package/lib/reply.js +12 -11
- package/lib/request.js +3 -2
- package/lib/route.js +34 -5
- package/lib/schemas.js +44 -32
- package/package.json +8 -3
- package/test/close-disposable.test.js +154 -0
- package/test/constrained-routes.test.js +4 -2
- package/test/content-parser.test.js +39 -0
- package/test/decorator.test.js +29 -0
- package/test/diagnostics-channel/error-status.test.js +35 -2
- package/test/handler-timeout.test.js +224 -0
- package/test/internals/all.test.js +5 -0
- package/test/internals/errors.test.js +18 -9
- package/test/internals/initial-config.test.js +2 -0
- package/test/internals/server.test.js +6 -4
- package/test/reply-error.test.js +82 -0
- package/test/reply-trailers.test.js +159 -1
- package/test/router-compile.test.js +241 -0
- package/test/schema-serialization.test.js +120 -7
- package/test/types/fastify.tst.ts +4 -0
- package/test/types/instance.tst.ts +1 -1
- package/test/types/route.tst.ts +16 -0
- package/test/types/using.tst.ts +20 -0
- package/test/validation-error-handling.test.js +21 -1
- package/types/instance.d.ts +5 -1
- 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
|
|
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.
|
|
324
|
-
t.assert.strictEqual(res.
|
|
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.
|
|
336
|
-
t.assert.strictEqual(res.
|
|
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.
|
|
390
|
-
t.assert.strictEqual(res.
|
|
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>()
|
package/test/types/route.tst.ts
CHANGED
|
@@ -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'
|
package/test/types/using.tst.ts
CHANGED
|
@@ -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:
|
|
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
|
|
package/types/instance.d.ts
CHANGED
|
@@ -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';
|