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.
Files changed (43) hide show
  1. package/SPONSORS.md +1 -0
  2. package/docs/Guides/Ecosystem.md +2 -2
  3. package/docs/Guides/Getting-Started.md +8 -8
  4. package/docs/Reference/Errors.md +4 -0
  5. package/docs/Reference/Reply.md +2 -2
  6. package/docs/Reference/Request.md +11 -11
  7. package/docs/Reference/Server.md +16 -15
  8. package/docs/Reference/TypeScript.md +5 -7
  9. package/docs/Reference/Warnings.md +1 -0
  10. package/eslint.config.js +1 -1
  11. package/fastify.d.ts +55 -13
  12. package/fastify.js +7 -2
  13. package/lib/content-type.js +36 -18
  14. package/lib/errors.js +10 -0
  15. package/lib/handle-request.js +35 -7
  16. package/lib/hooks.js +5 -1
  17. package/lib/route.js +1 -1
  18. package/lib/schemas.js +3 -3
  19. package/lib/validation.js +1 -1
  20. package/package.json +2 -2
  21. package/test/content-type.test.js +31 -4
  22. package/test/find-route.test.js +35 -0
  23. package/test/fix-6411.test.js +65 -0
  24. package/test/internals/all.test.js +2 -1
  25. package/test/internals/errors.test.js +21 -1
  26. package/test/internals/reply.test.js +22 -0
  27. package/test/reply-error.test.js +35 -0
  28. package/test/rfc-10008.test.js +147 -0
  29. package/test/route-shorthand.test.js +14 -4
  30. package/test/schema-serialization.test.js +33 -0
  31. package/test/types/errors.tst.ts +2 -0
  32. package/test/types/route.tst.ts +3 -3
  33. package/types/content-type-parser.d.ts +26 -6
  34. package/types/errors.d.ts +2 -0
  35. package/types/hooks.d.ts +137 -76
  36. package/types/instance.d.ts +150 -47
  37. package/types/logger.d.ts +36 -3
  38. package/types/plugin.d.ts +7 -3
  39. package/types/register.d.ts +53 -10
  40. package/types/reply.d.ts +62 -14
  41. package/types/route.d.ts +68 -34
  42. package/types/type-provider.d.ts +29 -8
  43. package/types/utils.d.ts +2 -2
package/lib/route.js CHANGED
@@ -179,7 +179,7 @@ function buildRouting (options) {
179
179
 
180
180
  function findRoute (options) {
181
181
  const route = router.find(
182
- options.method,
182
+ options.method?.toUpperCase() ?? '',
183
183
  options.url || '',
184
184
  options.constraints
185
185
  )
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 = new ContentType(contentType)
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 = new ContentType(contentType)
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 = new ContentType(contentType)
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.value) request[paramName] = ret.value
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.10.0",
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.22.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, 3)
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"; baz="invalid quoted string"'
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', () => {
@@ -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
+ })
@@ -9,7 +9,8 @@ test('fastify.all should add all the methods to the same url', async t => {
9
9
  const requirePayload = [
10
10
  'POST',
11
11
  'PUT',
12
- 'PATCH'
12
+ 'PATCH',
13
+ 'QUERY'
13
14
  ]
14
15
 
15
16
  const supportedMethods = fastify.supportedMethods
@@ -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 = 92
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
+ })
@@ -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
- const response = await instance.request({ path: '/', method })
25
- t.assert.strictEqual(response.statusCode, 200)
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
- const response = await instance.request({ path: '/', method })
45
- t.assert.strictEqual(response.statusCode, 200)
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
 
@@ -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>()
@@ -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
- > = ((request: FastifyRequest<RouteGeneric, RawServer, RawRequest, SchemaCompiler, TypeProvider>, rawBody: RawBody, done: ContentTypeParserDoneFunction) => void)
20
- | ((request: FastifyRequest<RouteGeneric, RawServer, RawRequest, SchemaCompiler, TypeProvider>, rawBody: RawBody) => Promise<any>)
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
- > = ((request: FastifyRequest<RouteGeneric, RawServer, RawRequest, SchemaCompiler, TypeProvider>, payload: RawRequest) => Promise<any>)
32
- | ((request: FastifyRequest<RouteGeneric, RawServer, RawRequest, SchemaCompiler, TypeProvider>, payload: RawRequest, done: ContentTypeParserDoneFunction) => void)
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
- (contentType: string | string[] | RegExp, parser: FastifyContentTypeParser<RawServer, RawRequest, RouteGeneric, SchemaCompiler, TypeProvider>): void;
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 = (onProtoPoisoning: ProtoAction, onConstructorPoisoning: ConstructorAction) => FastifyBodyParser<string>
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