fastify 5.10.0 → 5.11.1
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/.pi/bash-confirm-whitelist.json +180 -0
- package/SPONSORS.md +1 -0
- package/docs/Guides/Ecosystem.md +2 -2
- package/docs/Guides/Getting-Started.md +8 -8
- package/docs/Reference/Errors.md +4 -0
- package/docs/Reference/Reply.md +2 -2
- package/docs/Reference/Request.md +11 -11
- package/docs/Reference/Server.md +34 -17
- package/docs/Reference/TypeScript.md +5 -7
- package/docs/Reference/Validation-and-Serialization.md +13 -2
- package/docs/Reference/Warnings.md +3 -2
- package/eslint.config.js +1 -1
- package/fastify.d.ts +57 -13
- package/fastify.js +17 -5
- package/lib/content-type.js +36 -18
- package/lib/errors.js +10 -0
- package/lib/handle-request.js +35 -7
- package/lib/hooks.js +5 -1
- package/lib/route.js +1 -1
- package/lib/schemas.js +3 -3
- package/lib/validation.js +1 -1
- package/lib/warnings.js +11 -2
- package/package.json +2 -2
- package/test/close.test.js +39 -0
- package/test/content-type.test.js +31 -4
- package/test/find-route.test.js +35 -0
- package/test/fix-6411.test.js +65 -0
- package/test/http-methods/custom-http-methods.test.js +61 -0
- package/test/internals/all.test.js +2 -1
- package/test/internals/errors.test.js +21 -1
- package/test/internals/reply.test.js +22 -0
- package/test/reply-error.test.js +35 -0
- package/test/rfc-10008.test.js +147 -0
- package/test/route-shorthand.test.js +14 -4
- package/test/schema-serialization.test.js +33 -0
- package/test/types/errors.tst.ts +2 -0
- package/test/types/fastify.tst.ts +11 -0
- package/test/types/instance.tst.ts +2 -0
- package/test/types/route.tst.ts +3 -3
- package/test/validation-error-handling.test.js +34 -0
- package/types/content-type-parser.d.ts +26 -6
- package/types/errors.d.ts +2 -0
- package/types/hooks.d.ts +137 -76
- package/types/instance.d.ts +153 -47
- package/types/logger.d.ts +36 -3
- package/types/plugin.d.ts +7 -3
- package/types/register.d.ts +53 -10
- package/types/reply.d.ts +62 -14
- package/types/route.d.ts +68 -34
- package/types/type-provider.d.ts +29 -8
- package/types/utils.d.ts +2 -2
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { test } = require('node:test')
|
|
4
|
+
const Fastify = require('..')
|
|
5
|
+
|
|
6
|
+
test('custom validatorCompiler returning falsy values should update request param', async (t) => {
|
|
7
|
+
const fastify = Fastify()
|
|
8
|
+
|
|
9
|
+
fastify.setValidatorCompiler(() => {
|
|
10
|
+
return (data) => {
|
|
11
|
+
if (typeof data === 'object' && data !== null && 'coerceTo' in data) {
|
|
12
|
+
return { value: data.coerceTo }
|
|
13
|
+
}
|
|
14
|
+
return true
|
|
15
|
+
}
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
fastify.post('/test', { schema: { body: { type: 'object' } } }, (request, reply) => {
|
|
19
|
+
reply.send({ body: request.body })
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
// value: 0 (falsy)
|
|
23
|
+
{
|
|
24
|
+
const res = await fastify.inject({
|
|
25
|
+
method: 'POST',
|
|
26
|
+
url: '/test',
|
|
27
|
+
payload: { coerceTo: 0 }
|
|
28
|
+
})
|
|
29
|
+
t.assert.strictEqual(res.statusCode, 200)
|
|
30
|
+
t.assert.strictEqual(JSON.parse(res.payload).body, 0, 'should update body to 0')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// value: "" (falsy)
|
|
34
|
+
{
|
|
35
|
+
const res = await fastify.inject({
|
|
36
|
+
method: 'POST',
|
|
37
|
+
url: '/test',
|
|
38
|
+
payload: { coerceTo: '' }
|
|
39
|
+
})
|
|
40
|
+
t.assert.strictEqual(res.statusCode, 200)
|
|
41
|
+
t.assert.strictEqual(JSON.parse(res.payload).body, '', 'should update body to empty string')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// value: false (falsy)
|
|
45
|
+
{
|
|
46
|
+
const res = await fastify.inject({
|
|
47
|
+
method: 'POST',
|
|
48
|
+
url: '/test',
|
|
49
|
+
payload: { coerceTo: false }
|
|
50
|
+
})
|
|
51
|
+
t.assert.strictEqual(res.statusCode, 200)
|
|
52
|
+
t.assert.strictEqual(JSON.parse(res.payload).body, false, 'should update body to false')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// value: null (falsy)
|
|
56
|
+
{
|
|
57
|
+
const res = await fastify.inject({
|
|
58
|
+
method: 'POST',
|
|
59
|
+
url: '/test',
|
|
60
|
+
payload: { coerceTo: null }
|
|
61
|
+
})
|
|
62
|
+
t.assert.strictEqual(res.statusCode, 200)
|
|
63
|
+
t.assert.strictEqual(JSON.parse(res.payload).body, null, 'should update body to null')
|
|
64
|
+
}
|
|
65
|
+
})
|
|
@@ -112,3 +112,64 @@ test('addHttpMethod rejects fake http method', t => {
|
|
|
112
112
|
const fastify = Fastify()
|
|
113
113
|
t.assert.throws(() => { fastify.addHttpMethod('FOOO') }, /Provided method is invalid!/)
|
|
114
114
|
})
|
|
115
|
+
|
|
116
|
+
test('addHttpMethod warns when overriding an existing method', (t, done) => {
|
|
117
|
+
const fastify = Fastify()
|
|
118
|
+
const onWarning = warning => {
|
|
119
|
+
t.assert.strictEqual(warning.name, 'FastifyDeprecation')
|
|
120
|
+
t.assert.strictEqual(warning.code, 'FSTDEP025')
|
|
121
|
+
done()
|
|
122
|
+
}
|
|
123
|
+
process.once('warning', onWarning)
|
|
124
|
+
|
|
125
|
+
t.after(() => {
|
|
126
|
+
fastify.close()
|
|
127
|
+
process.removeListener('warning', onWarning)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
fastify.addHttpMethod('GET', { hasBody: true })
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test('addHttpMethod does not warn when overriding an existing method explicitly', t => {
|
|
134
|
+
const doNotWarn = () => {
|
|
135
|
+
t.assert.fail('should not warn')
|
|
136
|
+
}
|
|
137
|
+
process.on('warning', doNotWarn)
|
|
138
|
+
|
|
139
|
+
const fastify = Fastify()
|
|
140
|
+
t.after(() => {
|
|
141
|
+
fastify.close()
|
|
142
|
+
process.removeListener('warning', doNotWarn)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
fastify.addHttpMethod('POST', { overrideExisting: true })
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test('addHttpMethod can change an existing method body behavior', async t => {
|
|
149
|
+
const fastify = Fastify()
|
|
150
|
+
|
|
151
|
+
fastify.addHttpMethod('GET', {
|
|
152
|
+
hasBody: true,
|
|
153
|
+
overrideExisting: true
|
|
154
|
+
})
|
|
155
|
+
fastify.route({
|
|
156
|
+
method: 'GET',
|
|
157
|
+
url: '/',
|
|
158
|
+
exposeHeadRoute: false,
|
|
159
|
+
schema: {
|
|
160
|
+
body: {
|
|
161
|
+
type: 'object'
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
handler: async request => request.body
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
const response = await fastify.inject({
|
|
168
|
+
method: 'GET',
|
|
169
|
+
url: '/',
|
|
170
|
+
payload: { hello: 'world' }
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
t.assert.strictEqual(response.statusCode, 200)
|
|
174
|
+
t.assert.deepStrictEqual(response.json(), { hello: 'world' })
|
|
175
|
+
})
|
|
@@ -5,7 +5,7 @@ const errors = require('../../lib/errors')
|
|
|
5
5
|
const { readFileSync } = require('node:fs')
|
|
6
6
|
const { resolve } = require('node:path')
|
|
7
7
|
|
|
8
|
-
const expectedErrors =
|
|
8
|
+
const expectedErrors = 94
|
|
9
9
|
|
|
10
10
|
test(`should expose ${expectedErrors} errors`, t => {
|
|
11
11
|
t.plan(1)
|
|
@@ -770,6 +770,26 @@ test('FST_ERR_ROUTE_REWRITE_NOT_STR', t => {
|
|
|
770
770
|
t.assert.ok(error instanceof TypeError)
|
|
771
771
|
})
|
|
772
772
|
|
|
773
|
+
test('FST_ERR_ROUTE_MISSING_CONTENT_TYPE', t => {
|
|
774
|
+
t.plan(5)
|
|
775
|
+
const error = new errors.FST_ERR_ROUTE_MISSING_CONTENT_TYPE()
|
|
776
|
+
t.assert.strictEqual(error.name, 'FastifyError')
|
|
777
|
+
t.assert.strictEqual(error.code, 'FST_ERR_ROUTE_MISSING_CONTENT_TYPE')
|
|
778
|
+
t.assert.strictEqual(error.message, "Method '%s' must provide a 'Content-Type' header.")
|
|
779
|
+
t.assert.strictEqual(error.statusCode, 400)
|
|
780
|
+
t.assert.ok(error instanceof Error)
|
|
781
|
+
})
|
|
782
|
+
|
|
783
|
+
test('FST_ERR_ROUTE_MISSING_CONTENT', t => {
|
|
784
|
+
t.plan(5)
|
|
785
|
+
const error = new errors.FST_ERR_ROUTE_MISSING_CONTENT()
|
|
786
|
+
t.assert.strictEqual(error.name, 'FastifyError')
|
|
787
|
+
t.assert.strictEqual(error.code, 'FST_ERR_ROUTE_MISSING_CONTENT')
|
|
788
|
+
t.assert.strictEqual(error.message, "Method '%s' must provide a request body.")
|
|
789
|
+
t.assert.strictEqual(error.statusCode, 400)
|
|
790
|
+
t.assert.ok(error instanceof Error)
|
|
791
|
+
})
|
|
792
|
+
|
|
773
793
|
test('FST_ERR_REOPENED_CLOSE_SERVER', t => {
|
|
774
794
|
t.plan(5)
|
|
775
795
|
const error = new errors.FST_ERR_REOPENED_CLOSE_SERVER()
|
|
@@ -1949,3 +1949,25 @@ test('Uint8Array view of ArrayBuffer returns correct byteLength', (t, done) => {
|
|
|
1949
1949
|
})
|
|
1950
1950
|
})
|
|
1951
1951
|
})
|
|
1952
|
+
|
|
1953
|
+
test('reply.send should not treat charset= inside a quoted parameter value as an already-declared charset', async t => {
|
|
1954
|
+
t.plan(2)
|
|
1955
|
+
|
|
1956
|
+
const fastify = Fastify()
|
|
1957
|
+
t.after(() => fastify.close())
|
|
1958
|
+
|
|
1959
|
+
fastify.get('/', function (req, reply) {
|
|
1960
|
+
// A quoted parameter value that happens to contain the substring
|
|
1961
|
+
// `charset=` is opaque content of that value, not a real charset
|
|
1962
|
+
// parameter, so reply.send must still append `; charset=utf-8`.
|
|
1963
|
+
reply.header('content-type', 'application/json; name="a=b;charset=fake"')
|
|
1964
|
+
reply.send({ hello: 'world' })
|
|
1965
|
+
})
|
|
1966
|
+
|
|
1967
|
+
const res = await fastify.inject({ method: 'GET', url: '/' })
|
|
1968
|
+
t.assert.strictEqual(res.statusCode, 200)
|
|
1969
|
+
t.assert.strictEqual(
|
|
1970
|
+
res.headers['content-type'],
|
|
1971
|
+
'application/json; name="a=b;charset=fake"; charset=utf-8'
|
|
1972
|
+
)
|
|
1973
|
+
})
|
package/test/reply-error.test.js
CHANGED
|
@@ -813,3 +813,38 @@ test('pipe stream inside error handler should not cause error', (t, testDone) =>
|
|
|
813
813
|
testDone()
|
|
814
814
|
})
|
|
815
815
|
})
|
|
816
|
+
|
|
817
|
+
test('should catch error when setting invalid header on async preSerializeation hook', async (t) => {
|
|
818
|
+
t.plan(2)
|
|
819
|
+
|
|
820
|
+
const app = Fastify()
|
|
821
|
+
app.addHook('preSerialization', async (_, reply) => {
|
|
822
|
+
reply.header('X-Invalid', '\n')
|
|
823
|
+
})
|
|
824
|
+
app.addHook('onError', function (request, reply, err, done) {
|
|
825
|
+
// onError will run twice, since it execute preSerialization hook twice
|
|
826
|
+
// requires to fallback to root error handler
|
|
827
|
+
t.assert.ok(err)
|
|
828
|
+
done()
|
|
829
|
+
})
|
|
830
|
+
app.get('/', async () => ({ ok: true }))
|
|
831
|
+
await app.inject({ method: 'GET', path: '/' })
|
|
832
|
+
})
|
|
833
|
+
|
|
834
|
+
test('should catch error when setting invalid header on async onSend hook', async (t) => {
|
|
835
|
+
t.plan(2)
|
|
836
|
+
|
|
837
|
+
const app = Fastify()
|
|
838
|
+
app.addHook('onSend', async (_, reply, payload) => {
|
|
839
|
+
reply.header('X-Invalid', '\n')
|
|
840
|
+
return payload
|
|
841
|
+
})
|
|
842
|
+
app.addHook('onError', function (request, reply, err, done) {
|
|
843
|
+
// onError will run twice, since it execute onSend hook twice
|
|
844
|
+
// requires to fallback to root error handler
|
|
845
|
+
t.assert.ok(err)
|
|
846
|
+
done()
|
|
847
|
+
})
|
|
848
|
+
app.get('/', async () => ({ ok: true }))
|
|
849
|
+
await app.inject({ method: 'GET', path: '/' })
|
|
850
|
+
})
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { describe, test } = require('node:test')
|
|
4
|
+
const Fastify = require('..')
|
|
5
|
+
|
|
6
|
+
describe('RFC 10008', () => {
|
|
7
|
+
test('support adding QUERY method though fastify.route', async (t) => {
|
|
8
|
+
t.plan(2)
|
|
9
|
+
|
|
10
|
+
const fastify = Fastify()
|
|
11
|
+
fastify.route({
|
|
12
|
+
method: 'QUERY',
|
|
13
|
+
url: '/query',
|
|
14
|
+
handler: function (request, reply) {
|
|
15
|
+
reply.send(request.body)
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
await fastify.ready()
|
|
19
|
+
|
|
20
|
+
const res = await fastify.inject({
|
|
21
|
+
method: 'QUERY',
|
|
22
|
+
url: '/query',
|
|
23
|
+
headers: { 'Content-Type': 'application/json' },
|
|
24
|
+
body: JSON.stringify({ hello: 'world' })
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
t.assert.equal(res.statusCode, 200)
|
|
28
|
+
t.assert.equal(res.json().hello, 'world')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('support adding QUERY method though fastify.query', async (t) => {
|
|
32
|
+
t.plan(2)
|
|
33
|
+
|
|
34
|
+
const fastify = Fastify()
|
|
35
|
+
fastify.query('/query', function (request, reply) {
|
|
36
|
+
reply.send(request.body)
|
|
37
|
+
})
|
|
38
|
+
await fastify.ready()
|
|
39
|
+
|
|
40
|
+
const res = await fastify.inject({
|
|
41
|
+
method: 'QUERY',
|
|
42
|
+
url: '/query',
|
|
43
|
+
headers: { 'Content-Type': 'application/json' },
|
|
44
|
+
body: JSON.stringify({ hello: 'world' })
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
t.assert.equal(res.statusCode, 200)
|
|
48
|
+
t.assert.equal(res.json().hello, 'world')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* https://datatracker.ietf.org/doc/html/rfc10008#section-2.1
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
test('should return 400 if Content-Type header is missing for QUERY method', async (t) => {
|
|
56
|
+
// If a request lacks media type information, it is
|
|
57
|
+
// incorrect by definition and needs to fail with a
|
|
58
|
+
// 4xx status code such as 400 (Client Error).
|
|
59
|
+
t.plan(2)
|
|
60
|
+
|
|
61
|
+
const fastify = Fastify()
|
|
62
|
+
fastify.query('/query', function (request, reply) {
|
|
63
|
+
reply.send(request.body)
|
|
64
|
+
})
|
|
65
|
+
await fastify.ready()
|
|
66
|
+
|
|
67
|
+
const res = await fastify.inject({
|
|
68
|
+
method: 'QUERY',
|
|
69
|
+
url: '/query',
|
|
70
|
+
body: JSON.stringify({ hello: 'world' })
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
t.assert.equal(res.statusCode, 400)
|
|
74
|
+
t.assert.equal(res.json().code, 'FST_ERR_ROUTE_MISSING_CONTENT_TYPE')
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test('should return 400 if invalid content is provided for QUERY method', async (t) => {
|
|
78
|
+
// If a media type is specified but is inconsistent
|
|
79
|
+
// with the actual request content, a 400 (Bad Request)
|
|
80
|
+
// can be returned. That is, a server is not allowed to
|
|
81
|
+
// infer a media type from the request content and then
|
|
82
|
+
// override a missing or "erroneous" value (i.e., "content sniffing").
|
|
83
|
+
t.plan(2)
|
|
84
|
+
|
|
85
|
+
const fastify = Fastify()
|
|
86
|
+
fastify.query('/query', function (request, reply) {
|
|
87
|
+
reply.send(request.body)
|
|
88
|
+
})
|
|
89
|
+
await fastify.ready()
|
|
90
|
+
|
|
91
|
+
const res = await fastify.inject({
|
|
92
|
+
method: 'QUERY',
|
|
93
|
+
url: '/query',
|
|
94
|
+
headers: { 'Content-Type': 'application/json' },
|
|
95
|
+
body: 'it is not a valid json'
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
t.assert.equal(res.statusCode, 400)
|
|
99
|
+
t.assert.equal(res.json().code, 'FST_ERR_CTP_INVALID_JSON_BODY')
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test('should return 400 if no content is provided for QUERY method', async (t) => {
|
|
103
|
+
// If a media type is specified but is inconsistent
|
|
104
|
+
// with the actual request content, a 400 (Bad Request)
|
|
105
|
+
// can be returned. That is, a server is not allowed to
|
|
106
|
+
// infer a media type from the request content and then
|
|
107
|
+
// override a missing or "erroneous" value (i.e., "content sniffing").
|
|
108
|
+
t.plan(2)
|
|
109
|
+
|
|
110
|
+
const fastify = Fastify()
|
|
111
|
+
fastify.query('/query', function (request, reply) {
|
|
112
|
+
reply.send(request.body)
|
|
113
|
+
})
|
|
114
|
+
await fastify.ready()
|
|
115
|
+
|
|
116
|
+
const res = await fastify.inject({
|
|
117
|
+
method: 'QUERY',
|
|
118
|
+
url: '/query',
|
|
119
|
+
headers: { 'Content-Type': 'application/json' }
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
t.assert.equal(res.statusCode, 400)
|
|
123
|
+
t.assert.equal(res.json().code, 'FST_ERR_ROUTE_MISSING_CONTENT')
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test('should return 415 if unsupported media type is provided for QUERY method', async (t) => {
|
|
127
|
+
// If a media type is specified but is not supported by the
|
|
128
|
+
// resource, a 415 (Unsupported Media Type) is appropriate.
|
|
129
|
+
t.plan(2)
|
|
130
|
+
|
|
131
|
+
const fastify = Fastify()
|
|
132
|
+
fastify.query('/query', function (request, reply) {
|
|
133
|
+
reply.send(request.body)
|
|
134
|
+
})
|
|
135
|
+
await fastify.ready()
|
|
136
|
+
|
|
137
|
+
const res = await fastify.inject({
|
|
138
|
+
method: 'QUERY',
|
|
139
|
+
url: '/query',
|
|
140
|
+
headers: { 'Content-Type': 'application/ld+json' },
|
|
141
|
+
body: JSON.stringify({ hello: 'world' })
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
t.assert.equal(res.statusCode, 415)
|
|
145
|
+
t.assert.equal(res.json().code, 'FST_ERR_CTP_INVALID_MEDIA_TYPE')
|
|
146
|
+
})
|
|
147
|
+
})
|
|
@@ -21,8 +21,13 @@ describe('route-shorthand', () => {
|
|
|
21
21
|
|
|
22
22
|
const instance = new Client(`http://localhost:${fastify.server.address().port}`)
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
if (method === 'QUERY') {
|
|
25
|
+
const response = await instance.request({ path: '/', method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ hello: 'world' }) })
|
|
26
|
+
t.assert.strictEqual(response.statusCode, 200)
|
|
27
|
+
} else {
|
|
28
|
+
const response = await instance.request({ path: '/', method })
|
|
29
|
+
t.assert.strictEqual(response.statusCode, 200)
|
|
30
|
+
}
|
|
26
31
|
})
|
|
27
32
|
}
|
|
28
33
|
|
|
@@ -41,8 +46,13 @@ describe('route-shorthand', () => {
|
|
|
41
46
|
currentMethod = method
|
|
42
47
|
const instance = new Client(`http://localhost:${fastify.server.address().port}`)
|
|
43
48
|
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
if (method === 'QUERY') {
|
|
50
|
+
const response = await instance.request({ path: '/', method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ hello: 'world' }) })
|
|
51
|
+
t.assert.strictEqual(response.statusCode, 200)
|
|
52
|
+
} else {
|
|
53
|
+
const response = await instance.request({ path: '/', method })
|
|
54
|
+
t.assert.strictEqual(response.statusCode, 200)
|
|
55
|
+
}
|
|
46
56
|
}
|
|
47
57
|
})
|
|
48
58
|
})
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { test } = require('node:test')
|
|
4
4
|
const Fastify = require('..')
|
|
5
|
+
const ContentType = require('../lib/content-type')
|
|
5
6
|
const { waitForCb } = require('./toolkit')
|
|
6
7
|
|
|
7
8
|
const echoBody = (req, reply) => { reply.send(req.body) }
|
|
@@ -62,6 +63,38 @@ test('custom serializer options', (t, testDone) => {
|
|
|
62
63
|
})
|
|
63
64
|
})
|
|
64
65
|
|
|
66
|
+
test('reuse parsed content type when selecting response serializer', async t => {
|
|
67
|
+
const fastify = Fastify()
|
|
68
|
+
const contentType = 'application/x-fastify-cache-test+json; version=1'
|
|
69
|
+
|
|
70
|
+
fastify.get('/', {
|
|
71
|
+
schema: {
|
|
72
|
+
response: {
|
|
73
|
+
200: {
|
|
74
|
+
content: {
|
|
75
|
+
'application/x-fastify-cache-test+json': {
|
|
76
|
+
schema: {
|
|
77
|
+
type: 'object',
|
|
78
|
+
properties: {
|
|
79
|
+
hello: { type: 'string' }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}, function (request, reply) {
|
|
88
|
+
reply.type(contentType).send({ hello: 'world', ignored: true })
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const response = await fastify.inject('/')
|
|
92
|
+
const normalizedContentType = response.headers['content-type']
|
|
93
|
+
|
|
94
|
+
t.assert.deepStrictEqual(response.json(), { hello: 'world' })
|
|
95
|
+
t.assert.ok(ContentType.cache.get(normalizedContentType))
|
|
96
|
+
})
|
|
97
|
+
|
|
65
98
|
test('Different content types', (t, testDone) => {
|
|
66
99
|
t.plan(46)
|
|
67
100
|
|
package/test/types/errors.tst.ts
CHANGED
|
@@ -77,6 +77,8 @@ expect(errorCodes.FST_ERR_ROUTE_METHOD_NOT_SUPPORTED).type.toBeAssignableTo<Fast
|
|
|
77
77
|
expect(errorCodes.FST_ERR_ROUTE_BODY_VALIDATION_SCHEMA_NOT_SUPPORTED).type.toBeAssignableTo<FastifyErrorConstructor>()
|
|
78
78
|
expect(errorCodes.FST_ERR_ROUTE_BODY_LIMIT_OPTION_NOT_INT).type.toBeAssignableTo<FastifyErrorConstructor>()
|
|
79
79
|
expect(errorCodes.FST_ERR_ROUTE_REWRITE_NOT_STR).type.toBeAssignableTo<FastifyErrorConstructor>()
|
|
80
|
+
expect(errorCodes.FST_ERR_ROUTE_MISSING_CONTENT_TYPE).type.toBeAssignableTo<FastifyErrorConstructor>()
|
|
81
|
+
expect(errorCodes.FST_ERR_ROUTE_MISSING_CONTENT).type.toBeAssignableTo<FastifyErrorConstructor>()
|
|
80
82
|
expect(errorCodes.FST_ERR_REOPENED_CLOSE_SERVER).type.toBeAssignableTo<FastifyErrorConstructor>()
|
|
81
83
|
expect(errorCodes.FST_ERR_REOPENED_SERVER).type.toBeAssignableTo<FastifyErrorConstructor>()
|
|
82
84
|
expect(errorCodes.FST_ERR_INSTANCE_ALREADY_LISTENING).type.toBeAssignableTo<FastifyErrorConstructor>()
|
|
@@ -68,6 +68,17 @@ expect(fastify({ schemaController: {} })).type.toBe<
|
|
|
68
68
|
FastifyInstance<http.Server, http.IncomingMessage, http.ServerResponse> &
|
|
69
69
|
SafePromiseLike<FastifyInstance<http.Server, http.IncomingMessage, http.ServerResponse>>
|
|
70
70
|
>()
|
|
71
|
+
|
|
72
|
+
expect(fastify({ http2: false })).type.toBe<
|
|
73
|
+
FastifyInstance<http.Server, http.IncomingMessage, http.ServerResponse> &
|
|
74
|
+
SafePromiseLike<FastifyInstance<http.Server, http.IncomingMessage, http.ServerResponse>>
|
|
75
|
+
>()
|
|
76
|
+
|
|
77
|
+
expect(fastify({ https: {}, http2: false })).type.toBe<
|
|
78
|
+
FastifyInstance<https.Server, http.IncomingMessage, http.ServerResponse> &
|
|
79
|
+
SafePromiseLike<FastifyInstance<https.Server, http.IncomingMessage, http.ServerResponse>>
|
|
80
|
+
>()
|
|
81
|
+
|
|
71
82
|
expect(
|
|
72
83
|
fastify({
|
|
73
84
|
schemaController: {
|
|
@@ -215,6 +215,8 @@ function invalidSchemaErrorFormatter (err: Error) {
|
|
|
215
215
|
server.setSchemaErrorFormatter(invalidSchemaErrorFormatter)
|
|
216
216
|
|
|
217
217
|
expect(server.addHttpMethod('SEARCH', { hasBody: true })).type.toBe<FastifyInstance>()
|
|
218
|
+
expect(server.addHttpMethod('GET', { overrideExisting: true })).type.toBe<FastifyInstance>()
|
|
219
|
+
expect(server.addHttpMethod('POST', { hasBody: true, overrideExisting: true })).type.toBe<FastifyInstance>()
|
|
218
220
|
|
|
219
221
|
// test listen opts objects
|
|
220
222
|
const options: FastifyListenOptions = {}
|
package/test/types/route.tst.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import * as http from 'node:http'
|
|
2
1
|
import { FastifyError } from '@fastify/error'
|
|
2
|
+
import * as http from 'node:http'
|
|
3
3
|
import { expect } from 'tstyche'
|
|
4
4
|
import fastify, { FastifyInstance, FastifyReply, FastifyRequest, RouteHandlerMethod } from '../../fastify.js'
|
|
5
5
|
import { RequestPayload } from '../../types/hooks.js'
|
|
@@ -75,10 +75,10 @@ fastify().get(
|
|
|
75
75
|
)
|
|
76
76
|
|
|
77
77
|
type LowerCaseHTTPMethods = 'delete' | 'get' | 'head' | 'patch' | 'post' | 'put' |
|
|
78
|
-
'options' | 'propfind' | 'proppatch' | 'mkcol' | 'copy' | 'move' | 'lock' |
|
|
78
|
+
'options' | 'query' | 'propfind' | 'proppatch' | 'mkcol' | 'copy' | 'move' | 'lock' |
|
|
79
79
|
'unlock' | 'trace' | 'search' | 'mkcalendar' | 'report'
|
|
80
80
|
|
|
81
|
-
;['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT', 'OPTIONS', 'PROPFIND',
|
|
81
|
+
;['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT', 'OPTIONS', 'QUERY', 'PROPFIND',
|
|
82
82
|
'PROPPATCH', 'MKCOL', 'COPY', 'MOVE', 'LOCK', 'UNLOCK', 'TRACE', 'SEARCH', 'MKCALENDAR', 'REPORT'
|
|
83
83
|
].forEach(method => {
|
|
84
84
|
// route method
|
|
@@ -218,6 +218,40 @@ test('should be able to attach validation to request', async (t) => {
|
|
|
218
218
|
t.assert.strictEqual(response.statusCode, 400)
|
|
219
219
|
})
|
|
220
220
|
|
|
221
|
+
test('attached validationError exposes the same message sent by the default handler', async (t) => {
|
|
222
|
+
t.plan(4)
|
|
223
|
+
|
|
224
|
+
const fastify = Fastify()
|
|
225
|
+
|
|
226
|
+
fastify.post('/attached', { schema, attachValidation: true }, function (req, reply) {
|
|
227
|
+
reply.code(400).send({
|
|
228
|
+
message: req.validationError.message,
|
|
229
|
+
code: req.validationError.code,
|
|
230
|
+
statusCode: req.validationError.statusCode,
|
|
231
|
+
validationContext: req.validationError.validationContext
|
|
232
|
+
})
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
fastify.post('/default', { schema }, echoBody)
|
|
236
|
+
|
|
237
|
+
const payload = { hello: 'michelangelo' }
|
|
238
|
+
const attached = await fastify.inject({ method: 'POST', payload, url: '/attached' })
|
|
239
|
+
const notAttached = await fastify.inject({ method: 'POST', payload, url: '/default' })
|
|
240
|
+
|
|
241
|
+
const attachedBody = attached.json()
|
|
242
|
+
|
|
243
|
+
t.assert.deepStrictEqual(attachedBody, {
|
|
244
|
+
message: "body must have required property 'name'",
|
|
245
|
+
code: 'FST_ERR_VALIDATION',
|
|
246
|
+
statusCode: 400,
|
|
247
|
+
validationContext: 'body'
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
t.assert.strictEqual(attachedBody.message, notAttached.json().message)
|
|
251
|
+
t.assert.strictEqual(attached.statusCode, 400)
|
|
252
|
+
t.assert.strictEqual(notAttached.statusCode, 400)
|
|
253
|
+
})
|
|
254
|
+
|
|
221
255
|
test('should respect when attachValidation is explicitly set to false', async (t) => {
|
|
222
256
|
t.plan(2)
|
|
223
257
|
|
|
@@ -16,8 +16,15 @@ export type FastifyBodyParser<
|
|
|
16
16
|
RouteGeneric extends RouteGenericInterface = RouteGenericInterface,
|
|
17
17
|
SchemaCompiler extends FastifySchema = FastifySchema,
|
|
18
18
|
TypeProvider extends FastifyTypeProvider = FastifyTypeProviderDefault
|
|
19
|
-
> = ((
|
|
20
|
-
|
|
19
|
+
> = ((
|
|
20
|
+
request: FastifyRequest<RouteGeneric, RawServer, RawRequest, SchemaCompiler, TypeProvider>,
|
|
21
|
+
rawBody: RawBody,
|
|
22
|
+
done: ContentTypeParserDoneFunction
|
|
23
|
+
) => void)
|
|
24
|
+
| ((
|
|
25
|
+
request: FastifyRequest<RouteGeneric, RawServer, RawRequest, SchemaCompiler, TypeProvider>,
|
|
26
|
+
rawBody: RawBody
|
|
27
|
+
) => Promise<any>)
|
|
21
28
|
|
|
22
29
|
/**
|
|
23
30
|
* Content Type Parser method that operates on request content
|
|
@@ -28,8 +35,15 @@ export type FastifyContentTypeParser<
|
|
|
28
35
|
RouteGeneric extends RouteGenericInterface = RouteGenericInterface,
|
|
29
36
|
SchemaCompiler extends FastifySchema = FastifySchema,
|
|
30
37
|
TypeProvider extends FastifyTypeProvider = FastifyTypeProviderDefault
|
|
31
|
-
> = ((
|
|
32
|
-
|
|
38
|
+
> = ((
|
|
39
|
+
request: FastifyRequest<RouteGeneric, RawServer, RawRequest, SchemaCompiler, TypeProvider>,
|
|
40
|
+
payload: RawRequest
|
|
41
|
+
) => Promise<any>)
|
|
42
|
+
| ((
|
|
43
|
+
request: FastifyRequest<RouteGeneric, RawServer, RawRequest, SchemaCompiler, TypeProvider>,
|
|
44
|
+
payload: RawRequest,
|
|
45
|
+
done: ContentTypeParserDoneFunction
|
|
46
|
+
) => void)
|
|
33
47
|
|
|
34
48
|
/**
|
|
35
49
|
* Natively, Fastify only supports 'application/json' and 'text/plain' content types. The default charset is utf-8. If you need to support different content types, you can use the addContentTypeParser API. The default JSON and/or plain text parser can be changed.
|
|
@@ -48,7 +62,10 @@ export interface AddContentTypeParser<
|
|
|
48
62
|
},
|
|
49
63
|
parser: FastifyContentTypeParser<RawServer, RawRequest, RouteGeneric, SchemaCompiler, TypeProvider>
|
|
50
64
|
): void;
|
|
51
|
-
(
|
|
65
|
+
(
|
|
66
|
+
contentType: string | string[] | RegExp,
|
|
67
|
+
parser: FastifyContentTypeParser<RawServer, RawRequest, RouteGeneric, SchemaCompiler, TypeProvider>
|
|
68
|
+
): void;
|
|
52
69
|
<parseAs extends string | Buffer>(
|
|
53
70
|
contentType: string | string[] | RegExp,
|
|
54
71
|
opts: {
|
|
@@ -68,7 +85,10 @@ export type ProtoAction = 'error' | 'remove' | 'ignore'
|
|
|
68
85
|
|
|
69
86
|
export type ConstructorAction = 'error' | 'remove' | 'ignore'
|
|
70
87
|
|
|
71
|
-
export type getDefaultJsonParser = (
|
|
88
|
+
export type getDefaultJsonParser = (
|
|
89
|
+
onProtoPoisoning: ProtoAction,
|
|
90
|
+
onConstructorPoisoning: ConstructorAction
|
|
91
|
+
) => FastifyBodyParser<string>
|
|
72
92
|
|
|
73
93
|
export type removeContentTypeParser = (contentType: string | RegExp | (string | RegExp)[]) => void
|
|
74
94
|
|
package/types/errors.d.ts
CHANGED
|
@@ -81,6 +81,8 @@ export type FastifyErrorCodes = Record<
|
|
|
81
81
|
'FST_ERR_HANDLER_TIMEOUT' |
|
|
82
82
|
'FST_ERR_ROUTE_HANDLER_TIMEOUT_OPTION_NOT_INT' |
|
|
83
83
|
'FST_ERR_ROUTE_REWRITE_NOT_STR' |
|
|
84
|
+
'FST_ERR_ROUTE_MISSING_CONTENT_TYPE' |
|
|
85
|
+
'FST_ERR_ROUTE_MISSING_CONTENT' |
|
|
84
86
|
'FST_ERR_REOPENED_CLOSE_SERVER' |
|
|
85
87
|
'FST_ERR_REOPENED_SERVER' |
|
|
86
88
|
'FST_ERR_INSTANCE_ALREADY_LISTENING' |
|