fastify 5.10.0 → 5.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +16 -15
- package/docs/Reference/TypeScript.md +5 -7
- package/docs/Reference/Warnings.md +1 -0
- package/eslint.config.js +1 -1
- package/fastify.d.ts +55 -13
- package/fastify.js +7 -2
- 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/package.json +2 -2
- 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/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/route.tst.ts +3 -3
- 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 +150 -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
package/lib/route.js
CHANGED
package/lib/schemas.js
CHANGED
|
@@ -149,7 +149,7 @@ function getSchemaSerializer (context, statusCode, contentType) {
|
|
|
149
149
|
}
|
|
150
150
|
if (responseSchemaDef[statusCode]) {
|
|
151
151
|
if (responseSchemaDef[statusCode].constructor === Object) {
|
|
152
|
-
const ct =
|
|
152
|
+
const ct = ContentType.from(contentType)
|
|
153
153
|
if (ct.isValid) {
|
|
154
154
|
if (responseSchemaDef[statusCode][ct.mediaType]) {
|
|
155
155
|
return responseSchemaDef[statusCode][ct.mediaType]
|
|
@@ -168,7 +168,7 @@ function getSchemaSerializer (context, statusCode, contentType) {
|
|
|
168
168
|
const fallbackStatusCode = (statusCode + '')[0] + 'xx'
|
|
169
169
|
if (responseSchemaDef[fallbackStatusCode]) {
|
|
170
170
|
if (responseSchemaDef[fallbackStatusCode].constructor === Object) {
|
|
171
|
-
const ct =
|
|
171
|
+
const ct = ContentType.from(contentType)
|
|
172
172
|
if (ct.isValid) {
|
|
173
173
|
if (responseSchemaDef[fallbackStatusCode][ct.mediaType]) {
|
|
174
174
|
return responseSchemaDef[fallbackStatusCode][ct.mediaType]
|
|
@@ -187,7 +187,7 @@ function getSchemaSerializer (context, statusCode, contentType) {
|
|
|
187
187
|
}
|
|
188
188
|
if (responseSchemaDef.default) {
|
|
189
189
|
if (responseSchemaDef.default.constructor === Object) {
|
|
190
|
-
const ct =
|
|
190
|
+
const ct = ContentType.from(contentType)
|
|
191
191
|
if (ct.isValid) {
|
|
192
192
|
if (responseSchemaDef.default[ct.mediaType]) {
|
|
193
193
|
return responseSchemaDef.default[ct.mediaType]
|
package/lib/validation.js
CHANGED
|
@@ -138,7 +138,7 @@ function validateParam (validatorFunction, request, paramName) {
|
|
|
138
138
|
function answer (ret) {
|
|
139
139
|
if (ret === false) return validatorFunction.errors
|
|
140
140
|
if (ret && ret.error) return ret.error
|
|
141
|
-
if (ret && ret
|
|
141
|
+
if (ret && typeof ret === 'object' && 'value' in ret) request[paramName] = ret.value
|
|
142
142
|
return false
|
|
143
143
|
}
|
|
144
144
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fastify",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.11.0",
|
|
4
4
|
"description": "Fast and low overhead web framework, for Node.js",
|
|
5
5
|
"main": "fastify.js",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -191,7 +191,7 @@
|
|
|
191
191
|
"joi": "^18.0.1",
|
|
192
192
|
"json-schema-to-ts": "^3.0.1",
|
|
193
193
|
"JSONStream": "^1.3.5",
|
|
194
|
-
"markdownlint-cli2": "^0.
|
|
194
|
+
"markdownlint-cli2": "^0.23.0",
|
|
195
195
|
"neostandard": "^0.12.0",
|
|
196
196
|
"node-forge": "^1.3.1",
|
|
197
197
|
"proxyquire": "^2.1.3",
|
|
@@ -161,12 +161,11 @@ describe('ContentType class', () => {
|
|
|
161
161
|
t.assert.equal(found.mediaType, 'application/json')
|
|
162
162
|
t.assert.equal(found.type, 'application')
|
|
163
163
|
t.assert.equal(found.subtype, 'json')
|
|
164
|
-
t.assert.equal(found.parameters.size,
|
|
164
|
+
t.assert.equal(found.parameters.size, 2)
|
|
165
165
|
|
|
166
166
|
const expected = [
|
|
167
167
|
['charset', 'utf-8'],
|
|
168
|
-
['foo', 'BaR']
|
|
169
|
-
['baz', 'invalid quoted string']
|
|
168
|
+
['foo', 'BaR']
|
|
170
169
|
]
|
|
171
170
|
t.assert.deepStrictEqual(
|
|
172
171
|
Array.from(found.parameters.entries()),
|
|
@@ -175,9 +174,37 @@ describe('ContentType class', () => {
|
|
|
175
174
|
|
|
176
175
|
t.assert.equal(
|
|
177
176
|
found.toString(),
|
|
178
|
-
'application/json; charset="utf-8"; foo="BaR"
|
|
177
|
+
'application/json; charset="utf-8"; foo="BaR"'
|
|
179
178
|
)
|
|
180
179
|
})
|
|
180
|
+
|
|
181
|
+
test('preserves a semicolon inside a quoted parameter value', (t) => {
|
|
182
|
+
// RFC 9110 §5.6.6: ';' is a literal qdtext octet inside a quoted-string
|
|
183
|
+
// and does not terminate the parameter value.
|
|
184
|
+
const found = new ContentType('application/json; name="foo;bar"; charset=utf-8')
|
|
185
|
+
t.assert.equal(found.isValid, true)
|
|
186
|
+
t.assert.equal(found.mediaType, 'application/json')
|
|
187
|
+
t.assert.equal(found.parameters.get('name'), 'foo;bar')
|
|
188
|
+
t.assert.equal(found.parameters.get('charset'), 'utf-8')
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
test('does not leak a fake parameter out of a quoted value', (t) => {
|
|
192
|
+
// A `key=value;` sequence inside a quoted-string is opaque content of the
|
|
193
|
+
// enclosing value, not a subsequent parameter.
|
|
194
|
+
const found = new ContentType('application/json; name="a=b;charset=fake"; boundary=xyz')
|
|
195
|
+
t.assert.equal(found.isValid, true)
|
|
196
|
+
t.assert.equal(found.parameters.get('name'), 'a=b;charset=fake')
|
|
197
|
+
t.assert.equal(found.parameters.get('boundary'), 'xyz')
|
|
198
|
+
t.assert.equal(found.parameters.has('charset'), false)
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
test('unescapes a quoted-pair inside a quoted-string', (t) => {
|
|
202
|
+
// RFC 9110 §5.6.4: a quoted-pair MUST be handled as if replaced by
|
|
203
|
+
// the octet following the backslash.
|
|
204
|
+
const found = new ContentType('application/json; name="he said \\"hi\\""')
|
|
205
|
+
t.assert.equal(found.isValid, true)
|
|
206
|
+
t.assert.equal(found.parameters.get('name'), 'he said "hi"')
|
|
207
|
+
})
|
|
181
208
|
})
|
|
182
209
|
|
|
183
210
|
describe('ContentType class cache', () => {
|
package/test/find-route.test.js
CHANGED
|
@@ -66,6 +66,22 @@ test('findRoute should return null when when url is not passed', t => {
|
|
|
66
66
|
}), null)
|
|
67
67
|
})
|
|
68
68
|
|
|
69
|
+
test('findRoute should return null when method is not passed', t => {
|
|
70
|
+
t.plan(1)
|
|
71
|
+
const fastify = Fastify()
|
|
72
|
+
|
|
73
|
+
fastify.get('/artists/:artistId', {
|
|
74
|
+
schema: {
|
|
75
|
+
params: { artistId: { type: 'integer' } }
|
|
76
|
+
},
|
|
77
|
+
handler: (req, reply) => reply.send(typeof req.params.artistId)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
t.assert.strictEqual(fastify.findRoute({
|
|
81
|
+
url: '/artists/:artistId'
|
|
82
|
+
}), null)
|
|
83
|
+
})
|
|
84
|
+
|
|
69
85
|
test('findRoute should return null when route cannot be found due to a different path', t => {
|
|
70
86
|
t.plan(1)
|
|
71
87
|
const fastify = Fastify()
|
|
@@ -103,6 +119,25 @@ test('findRoute should return the route when found', t => {
|
|
|
103
119
|
t.assert.strictEqual(route.params.artistId, ':artistId')
|
|
104
120
|
})
|
|
105
121
|
|
|
122
|
+
test('findRoute should find a route even if method is not uppercased', t => {
|
|
123
|
+
t.plan(2)
|
|
124
|
+
const fastify = Fastify()
|
|
125
|
+
|
|
126
|
+
fastify.get('/artists/:artistId', {
|
|
127
|
+
schema: {
|
|
128
|
+
params: { artistId: { type: 'integer' } }
|
|
129
|
+
},
|
|
130
|
+
handler: (req, reply) => reply.send(typeof req.params.artistId)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
const route = fastify.findRoute({
|
|
134
|
+
method: 'get',
|
|
135
|
+
url: '/artists/:artistId'
|
|
136
|
+
})
|
|
137
|
+
t.assert.notStrictEqual(route, null)
|
|
138
|
+
t.assert.strictEqual(route.params.artistId, ':artistId')
|
|
139
|
+
})
|
|
140
|
+
|
|
106
141
|
test('findRoute should work correctly when used within plugins', (t, done) => {
|
|
107
142
|
t.plan(1)
|
|
108
143
|
const fastify = Fastify()
|
|
@@ -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
|
+
})
|
|
@@ -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>()
|
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
|
|
@@ -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
|
|