fastify 6.0.0-alpha.4 → 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/SECURITY.md +6 -0
- package/build/build-validation.js +2 -0
- package/docs/Guides/Ecosystem.md +12 -53
- package/docs/Guides/Migration-Guide-V5.md +2 -2
- package/docs/Guides/Serverless.md +33 -10
- package/docs/Reference/Errors.md +13 -2
- package/docs/Reference/Hooks.md +13 -0
- package/docs/Reference/Reply.md +787 -681
- package/docs/Reference/Request.md +308 -104
- package/docs/Reference/Routes.md +6 -2
- package/docs/Reference/Server.md +41 -4
- package/docs/Reference/Validation-and-Serialization.md +48 -1
- package/docs/Reference/Warnings.md +2 -0
- package/fastify.d.ts +1 -0
- package/fastify.js +35 -1
- package/lib/config-validator.js +102 -73
- package/lib/content-type-parser.js +35 -14
- package/lib/content-type.js +4 -4
- 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 -14
- package/lib/handle-request.js +7 -20
- package/lib/reply.js +18 -13
- package/lib/request.js +12 -7
- package/lib/route.js +40 -7
- package/lib/schemas.js +44 -32
- package/lib/validation.js +67 -10
- package/lib/warnings.js +10 -1
- package/package.json +8 -3
- package/test/child-logger-factory.test.js +25 -0
- package/test/close-disposable.test.js +154 -0
- package/test/constrained-routes.test.js +4 -2
- package/test/content-parser.test.js +99 -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 +19 -20
- package/test/internals/initial-config.test.js +2 -0
- package/test/internals/reply-serialize.test.js +20 -0
- package/test/internals/reply.test.js +30 -1
- package/test/internals/request.test.js +50 -0
- package/test/internals/server.test.js +6 -4
- package/test/reply-error.test.js +82 -0
- package/test/reply-media-type.test.js +57 -0
- package/test/reply-trailers.test.js +159 -1
- package/test/rfc-10008.test.js +6 -9
- package/test/router-compile.test.js +241 -0
- package/test/schema-feature.test.js +1 -1
- package/test/schema-serialization.test.js +120 -7
- package/test/schema-validation.test.js +251 -1
- package/test/types/errors.tst.ts +0 -1
- package/test/types/fastify.tst.ts +9 -0
- package/test/types/instance.tst.ts +1 -1
- package/test/types/route.tst.ts +37 -0
- package/test/types/using.tst.ts +20 -0
- package/test/validation-error-handling.test.js +21 -1
- package/types/errors.d.ts +0 -1
- package/types/instance.d.ts +9 -5
- package/types/route.d.ts +4 -2
|
@@ -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
|
+
})
|
|
@@ -459,7 +459,7 @@ test('Should emit a warning for every route with undefined schema', (t, testDone
|
|
|
459
459
|
t.assert.ifError(error)
|
|
460
460
|
t.assert.strictEqual(res.statusCode, 200)
|
|
461
461
|
// fastify.inject run in series
|
|
462
|
-
// last callback
|
|
462
|
+
// last callback receive all warnings at once
|
|
463
463
|
// GET /undefinedParams/123
|
|
464
464
|
t.assert.deepStrictEqual(spyData.calls[0], { arguments: ['params', 'GET', '/undefinedParams/:id'], result: true })
|
|
465
465
|
t.assert.deepStrictEqual(spyData.calls[1], { arguments: ['params', 'HEAD', '/undefinedParams/:id'], result: true })
|
|
@@ -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
|
+
})
|
|
@@ -8,7 +8,7 @@ const Schema = require('fluent-json-schema')
|
|
|
8
8
|
const { waitForCb } = require('./helper')
|
|
9
9
|
const { kRequestContentType } = require('../lib/symbols')
|
|
10
10
|
const { spyWarning } = require('process-warning')
|
|
11
|
-
const { FSTSEC002 } = require('../lib/warnings')
|
|
11
|
+
const { FSTSEC002, FSTSEC003 } = require('../lib/warnings')
|
|
12
12
|
|
|
13
13
|
const customSchemaCompilers = {
|
|
14
14
|
body: new AJV({
|
|
@@ -1921,6 +1921,52 @@ test('inline and local $ref header schemas do not emit FSTSEC002', async t => {
|
|
|
1921
1921
|
t.assert.strictEqual(spyData.callCount(), 0)
|
|
1922
1922
|
})
|
|
1923
1923
|
|
|
1924
|
+
test('header schema with a $ref inside propertyNames emits FSTSEC003 (case-normalization does not reach it)', async t => {
|
|
1925
|
+
const spyData = spyWarning(FSTSEC003)
|
|
1926
|
+
t.after(spyData.restore)
|
|
1927
|
+
|
|
1928
|
+
const fastify = Fastify()
|
|
1929
|
+
fastify.post('/', {
|
|
1930
|
+
schema: {
|
|
1931
|
+
headers: {
|
|
1932
|
+
type: 'object',
|
|
1933
|
+
propertyNames: { $ref: '#/definitions/denied' },
|
|
1934
|
+
definitions: {
|
|
1935
|
+
denied: { not: { enum: ['X-Debug-Override'] } }
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
}, async () => ({ ok: true }))
|
|
1940
|
+
|
|
1941
|
+
await fastify.ready()
|
|
1942
|
+
|
|
1943
|
+
t.assert.strictEqual(spyData.callCount(), 1)
|
|
1944
|
+
t.assert.deepStrictEqual(spyData.calls[0].arguments, ['POST', '/', '#/definitions/denied'])
|
|
1945
|
+
})
|
|
1946
|
+
|
|
1947
|
+
test('header schema detects a $ref inside propertyNames nested in an array', async t => {
|
|
1948
|
+
const spyData = spyWarning(FSTSEC003)
|
|
1949
|
+
t.after(spyData.restore)
|
|
1950
|
+
|
|
1951
|
+
const fastify = Fastify()
|
|
1952
|
+
fastify.post('/', {
|
|
1953
|
+
schema: {
|
|
1954
|
+
headers: {
|
|
1955
|
+
type: 'object',
|
|
1956
|
+
propertyNames: { not: { anyOf: [{ $ref: '#/definitions/denied' }] } },
|
|
1957
|
+
definitions: {
|
|
1958
|
+
denied: { enum: ['X-Debug-Override'] }
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
}, async () => ({ ok: true }))
|
|
1963
|
+
|
|
1964
|
+
await fastify.ready()
|
|
1965
|
+
|
|
1966
|
+
t.assert.strictEqual(spyData.callCount(), 1)
|
|
1967
|
+
t.assert.deepStrictEqual(spyData.calls[0].arguments, ['POST', '/', '#/definitions/denied'])
|
|
1968
|
+
})
|
|
1969
|
+
|
|
1924
1970
|
test('header schema normalization traverses dependent schema keywords', async t => {
|
|
1925
1971
|
const fastify = Fastify({
|
|
1926
1972
|
ajv: { customOptions: { strictSchema: false } }
|
|
@@ -1993,3 +2039,207 @@ test('header schema detects an external $ref nested in an array', async t => {
|
|
|
1993
2039
|
'http://example.com/nested-admin-headers#'
|
|
1994
2040
|
])
|
|
1995
2041
|
})
|
|
2042
|
+
|
|
2043
|
+
test('header schema propertyNames: not-enum deny-list is lowercased', async t => {
|
|
2044
|
+
const fastify = Fastify()
|
|
2045
|
+
|
|
2046
|
+
// A `propertyNames` subschema validates each header name itself, so strings in
|
|
2047
|
+
// its `enum` are header names. They must be lowercased the same way as
|
|
2048
|
+
// `required` entries, otherwise a deny-list written in the usual header case
|
|
2049
|
+
// never matches the lowercased request object.
|
|
2050
|
+
fastify.get('/', {
|
|
2051
|
+
schema: {
|
|
2052
|
+
headers: {
|
|
2053
|
+
type: 'object',
|
|
2054
|
+
propertyNames: { not: { enum: ['X-Debug-Override'] } }
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
}, async () => ({ ok: true }))
|
|
2058
|
+
|
|
2059
|
+
await fastify.ready()
|
|
2060
|
+
|
|
2061
|
+
const blocked = await fastify.inject({
|
|
2062
|
+
method: 'GET',
|
|
2063
|
+
url: '/',
|
|
2064
|
+
headers: { 'X-Debug-Override': '1' }
|
|
2065
|
+
})
|
|
2066
|
+
t.assert.strictEqual(blocked.statusCode, 400)
|
|
2067
|
+
t.assert.strictEqual(blocked.json().code, 'FST_ERR_VALIDATION')
|
|
2068
|
+
|
|
2069
|
+
const ok = await fastify.inject({
|
|
2070
|
+
method: 'GET',
|
|
2071
|
+
url: '/',
|
|
2072
|
+
headers: { 'X-Other': '1' }
|
|
2073
|
+
})
|
|
2074
|
+
t.assert.strictEqual(ok.statusCode, 200)
|
|
2075
|
+
})
|
|
2076
|
+
|
|
2077
|
+
test('header schema propertyNames: non-string enum items are left untouched', async t => {
|
|
2078
|
+
const fastify = Fastify({
|
|
2079
|
+
ajv: { customOptions: { strictSchema: false } }
|
|
2080
|
+
})
|
|
2081
|
+
|
|
2082
|
+
fastify.get('/', {
|
|
2083
|
+
schema: {
|
|
2084
|
+
headers: {
|
|
2085
|
+
type: 'object',
|
|
2086
|
+
propertyNames: { not: { enum: ['X-Debug-Override', 42, null] } }
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
}, async () => ({ ok: true }))
|
|
2090
|
+
|
|
2091
|
+
await fastify.ready()
|
|
2092
|
+
|
|
2093
|
+
const blocked = await fastify.inject({
|
|
2094
|
+
method: 'GET',
|
|
2095
|
+
url: '/',
|
|
2096
|
+
headers: { 'X-Debug-Override': '1' }
|
|
2097
|
+
})
|
|
2098
|
+
t.assert.strictEqual(blocked.statusCode, 400)
|
|
2099
|
+
|
|
2100
|
+
const ok = await fastify.inject({
|
|
2101
|
+
method: 'GET',
|
|
2102
|
+
url: '/',
|
|
2103
|
+
headers: { 'X-Other': '1' }
|
|
2104
|
+
})
|
|
2105
|
+
t.assert.strictEqual(ok.statusCode, 200)
|
|
2106
|
+
})
|
|
2107
|
+
|
|
2108
|
+
test('header schema propertyNames: not-const deny-list is lowercased', async t => {
|
|
2109
|
+
const fastify = Fastify()
|
|
2110
|
+
|
|
2111
|
+
fastify.get('/', {
|
|
2112
|
+
schema: {
|
|
2113
|
+
headers: {
|
|
2114
|
+
type: 'object',
|
|
2115
|
+
propertyNames: { not: { const: 'X-Debug-Override' } }
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
}, async () => ({ ok: true }))
|
|
2119
|
+
|
|
2120
|
+
await fastify.ready()
|
|
2121
|
+
|
|
2122
|
+
const blocked = await fastify.inject({
|
|
2123
|
+
method: 'GET',
|
|
2124
|
+
url: '/',
|
|
2125
|
+
headers: { 'X-Debug-Override': '1' }
|
|
2126
|
+
})
|
|
2127
|
+
t.assert.strictEqual(blocked.statusCode, 400)
|
|
2128
|
+
t.assert.strictEqual(blocked.json().code, 'FST_ERR_VALIDATION')
|
|
2129
|
+
})
|
|
2130
|
+
|
|
2131
|
+
test('header schema propertyNames: enum/const are lowercased inside combinators', async t => {
|
|
2132
|
+
const fastify = Fastify()
|
|
2133
|
+
|
|
2134
|
+
// The header-name `enum`/`const` can be nested under combinators inside
|
|
2135
|
+
// `propertyNames`; the lowercasing context must propagate through them.
|
|
2136
|
+
fastify.get('/anyOf', {
|
|
2137
|
+
schema: {
|
|
2138
|
+
headers: {
|
|
2139
|
+
type: 'object',
|
|
2140
|
+
propertyNames: { not: { anyOf: [{ enum: ['X-Debug-Override'] }] } }
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
}, async () => ({ ok: true }))
|
|
2144
|
+
|
|
2145
|
+
fastify.get('/oneOf', {
|
|
2146
|
+
schema: {
|
|
2147
|
+
headers: {
|
|
2148
|
+
type: 'object',
|
|
2149
|
+
propertyNames: { not: { oneOf: [{ const: 'X-Debug-Override' }] } }
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
}, async () => ({ ok: true }))
|
|
2153
|
+
|
|
2154
|
+
await fastify.ready()
|
|
2155
|
+
|
|
2156
|
+
for (const url of ['/anyOf', '/oneOf']) {
|
|
2157
|
+
const blocked = await fastify.inject({
|
|
2158
|
+
method: 'GET',
|
|
2159
|
+
url,
|
|
2160
|
+
headers: { 'X-Debug-Override': '1' }
|
|
2161
|
+
})
|
|
2162
|
+
t.assert.strictEqual(blocked.statusCode, 400, `${url} should block the header`)
|
|
2163
|
+
t.assert.strictEqual(blocked.json().code, 'FST_ERR_VALIDATION')
|
|
2164
|
+
}
|
|
2165
|
+
})
|
|
2166
|
+
|
|
2167
|
+
test('header schema propertyNames: lowercase-equivalent schema behaves identically', async t => {
|
|
2168
|
+
const fastify = Fastify()
|
|
2169
|
+
|
|
2170
|
+
fastify.get('/', {
|
|
2171
|
+
schema: {
|
|
2172
|
+
headers: {
|
|
2173
|
+
type: 'object',
|
|
2174
|
+
propertyNames: { not: { enum: ['x-debug-override'] } }
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
}, async () => ({ ok: true }))
|
|
2178
|
+
|
|
2179
|
+
await fastify.ready()
|
|
2180
|
+
|
|
2181
|
+
const blocked = await fastify.inject({
|
|
2182
|
+
method: 'GET',
|
|
2183
|
+
url: '/',
|
|
2184
|
+
headers: { 'X-Debug-Override': '1' }
|
|
2185
|
+
})
|
|
2186
|
+
t.assert.strictEqual(blocked.statusCode, 400)
|
|
2187
|
+
t.assert.strictEqual(blocked.json().code, 'FST_ERR_VALIDATION')
|
|
2188
|
+
})
|
|
2189
|
+
|
|
2190
|
+
test('header schema propertyNames: enum/const values in header value schemas are not lowercased', async t => {
|
|
2191
|
+
const fastify = Fastify()
|
|
2192
|
+
|
|
2193
|
+
// `enum`/`const` inside a property value subschema hold header *values*, not
|
|
2194
|
+
// names, and must be preserved with their original case.
|
|
2195
|
+
fastify.get('/enum', {
|
|
2196
|
+
schema: {
|
|
2197
|
+
headers: {
|
|
2198
|
+
type: 'object',
|
|
2199
|
+
properties: { 'X-Mode': { type: 'string', enum: ['MIXED', 'lowercase'] } }
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
}, async () => ({ ok: true }))
|
|
2203
|
+
|
|
2204
|
+
fastify.get('/const', {
|
|
2205
|
+
schema: {
|
|
2206
|
+
headers: {
|
|
2207
|
+
type: 'object',
|
|
2208
|
+
properties: { 'X-Mode': { type: 'string', const: 'MIXED' } }
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
}, async () => ({ ok: true }))
|
|
2212
|
+
|
|
2213
|
+
await fastify.ready()
|
|
2214
|
+
|
|
2215
|
+
const enumMixed = await fastify.inject({
|
|
2216
|
+
method: 'GET',
|
|
2217
|
+
url: '/enum',
|
|
2218
|
+
headers: { 'X-Mode': 'MIXED' }
|
|
2219
|
+
})
|
|
2220
|
+
t.assert.strictEqual(enumMixed.statusCode, 200)
|
|
2221
|
+
|
|
2222
|
+
const constMixed = await fastify.inject({
|
|
2223
|
+
method: 'GET',
|
|
2224
|
+
url: '/const',
|
|
2225
|
+
headers: { 'X-Mode': 'MIXED' }
|
|
2226
|
+
})
|
|
2227
|
+
t.assert.strictEqual(constMixed.statusCode, 200)
|
|
2228
|
+
})
|
|
2229
|
+
|
|
2230
|
+
test('header schema propertyNames lowercasing does not mutate the input schema', async t => {
|
|
2231
|
+
const headers = {
|
|
2232
|
+
type: 'object',
|
|
2233
|
+
propertyNames: { not: { enum: ['X-Debug-Override'], const: 'X-Other' } }
|
|
2234
|
+
}
|
|
2235
|
+
const schema = { headers }
|
|
2236
|
+
const schemaBefore = JSON.stringify(schema)
|
|
2237
|
+
const headersBefore = JSON.stringify(headers)
|
|
2238
|
+
|
|
2239
|
+
const fastify = Fastify()
|
|
2240
|
+
fastify.get('/', { schema }, async () => ({ ok: true }))
|
|
2241
|
+
await fastify.ready()
|
|
2242
|
+
|
|
2243
|
+
t.assert.strictEqual(JSON.stringify(schema), schemaBefore)
|
|
2244
|
+
t.assert.strictEqual(JSON.stringify(headers), headersBefore)
|
|
2245
|
+
})
|
package/test/types/errors.tst.ts
CHANGED
|
@@ -79,7 +79,6 @@ expect(errorCodes.FST_ERR_ROUTE_BODY_VALIDATION_SCHEMA_NOT_SUPPORTED).type.toBeA
|
|
|
79
79
|
expect(errorCodes.FST_ERR_ROUTE_BODY_LIMIT_OPTION_NOT_INT).type.toBeAssignableTo<FastifyErrorConstructor>()
|
|
80
80
|
expect(errorCodes.FST_ERR_ROUTE_REWRITE_NOT_STR).type.toBeAssignableTo<FastifyErrorConstructor>()
|
|
81
81
|
expect(errorCodes.FST_ERR_ROUTE_MISSING_CONTENT_TYPE).type.toBeAssignableTo<FastifyErrorConstructor>()
|
|
82
|
-
expect(errorCodes.FST_ERR_ROUTE_MISSING_CONTENT).type.toBeAssignableTo<FastifyErrorConstructor>()
|
|
83
82
|
expect(errorCodes.FST_ERR_REOPENED_CLOSE_SERVER).type.toBeAssignableTo<FastifyErrorConstructor>()
|
|
84
83
|
expect(errorCodes.FST_ERR_REOPENED_SERVER).type.toBeAssignableTo<FastifyErrorConstructor>()
|
|
85
84
|
expect(errorCodes.FST_ERR_INSTANCE_ALREADY_STARTED).type.toBeAssignableTo<FastifyErrorConstructor>()
|