fastify 5.7.0 → 5.7.2
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/.idea/fastify.iml +12 -0
- package/.idea/inspectionProfiles/Project_Default.xml +6 -0
- package/.idea/jsLibraryMappings.xml +6 -0
- package/.idea/jsLinters/eslint.xml +6 -0
- package/.idea/modules.xml +8 -0
- package/.idea/vcs.xml +6 -0
- package/LICENSE +1 -4
- package/SPONSORS.md +2 -2
- package/docs/Guides/Ecosystem.md +2 -1
- package/docs/Guides/Plugins-Guide.md +8 -8
- package/docs/Reference/Server.md +6 -2
- package/docs/Reference/Validation-and-Serialization.md +15 -6
- package/fastify.js +1 -1
- package/lib/content-type-parser.js +40 -30
- package/lib/content-type.js +152 -0
- package/lib/error-serializer.js +24 -10
- package/lib/handle-request.js +12 -4
- package/package.json +2 -2
- package/test/content-parser.test.js +21 -39
- package/test/content-type.test.js +102 -1
- package/test/custom-parser.0.test.js +3 -3
- package/test/custom-parser.1.test.js +0 -35
- package/test/custom-parser.3.test.js +1 -1
- package/test/schema-validation.test.js +66 -33
- package/.claude/settings.local.json +0 -11
- package/docs/fastify-fastify-pr-6425-run-20528050272-Lint_Docs.log +0 -180
- package/docs/fastify-fastify-pr-6425-run-20528054188-PR_#6425.log +0 -4006
- package/docs/fastify-fastify-pr-6425-run-20528054403-Pull_Request_Labeler.log +0 -42
- package/docs/fastify-fastify-pr-6425-run-20528054421-Lint_Docs.log +0 -196
- package/docs/fastify-fastify-pr-6425-run-20528054423-Internal_Links_Check.log +0 -1864
- package/docs/fastify-fastify-pr-6425-run-20528054430-Test_compare.log +0 -5
- package/docs/fastify-fastify-pr-6425-run-20528054438-pull_request_title_check.log +0 -41
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<module type="WEB_MODULE" version="4">
|
|
3
|
+
<component name="NewModuleRootManager">
|
|
4
|
+
<content url="file://$MODULE_DIR$">
|
|
5
|
+
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
|
6
|
+
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
|
7
|
+
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
|
8
|
+
</content>
|
|
9
|
+
<orderEntry type="inheritedJdk" />
|
|
10
|
+
<orderEntry type="sourceFolder" forTests="false" />
|
|
11
|
+
</component>
|
|
12
|
+
</module>
|
package/.idea/vcs.xml
ADDED
package/LICENSE
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2016-present The Fastify Team
|
|
4
|
-
|
|
5
|
-
The Fastify team members are listed at https://github.com/fastify/fastify#team
|
|
6
|
-
and in the README file.
|
|
3
|
+
Copyright (c) 2016-present The Fastify Team (members are listed in the README file)
|
|
7
4
|
|
|
8
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
9
6
|
of this software and associated documentation files (the "Software"), to deal
|
package/SPONSORS.md
CHANGED
|
@@ -9,7 +9,7 @@ or [GitHub Sponsors](https://github.com/sponsors/fastify)!
|
|
|
9
9
|
|
|
10
10
|
## Tier 4
|
|
11
11
|
|
|
12
|
-
- [SerpApi](
|
|
12
|
+
- [SerpApi](https://serpapi.com/?utm_source=fastify)
|
|
13
13
|
|
|
14
14
|
## Tier 3
|
|
15
15
|
|
|
@@ -17,7 +17,7 @@ or [GitHub Sponsors](https://github.com/sponsors/fastify)!
|
|
|
17
17
|
- [Val Town, Inc.](https://opencollective.com/valtown)
|
|
18
18
|
- [Handsontable - JavaScript Data Grid](https://handsontable.com/docs/react-data-grid/?utm_source=Fastify_GH&utm_medium=sponsorship&utm_campaign=library_sponsorship_2024)
|
|
19
19
|
- [Lokalise - A Localization and Translation Software Tool](https://lokalise.com/?utm_source=Fastify_GH&utm_medium=sponsorship)
|
|
20
|
-
- [
|
|
20
|
+
- [TestMu AI](https://www.testmu.ai/)
|
|
21
21
|
|
|
22
22
|
## Tier 2
|
|
23
23
|
|
package/docs/Guides/Ecosystem.md
CHANGED
|
@@ -427,6 +427,8 @@ section.
|
|
|
427
427
|
context to take place per API call within the Fastify lifecycle of calls.
|
|
428
428
|
- [`fastify-http-errors-enhanced`](https://github.com/ShogunPanda/fastify-http-errors-enhanced)
|
|
429
429
|
An error handling plugin for Fastify that uses enhanced HTTP errors.
|
|
430
|
+
- [`fastify-http-exceptions`](https://github.com/bhouston/fastify-http-exceptions)
|
|
431
|
+
Typed HTTP status exceptions which are automatically converted into Fastify responses.
|
|
430
432
|
- [`fastify-http2https`](https://github.com/lolo32/fastify-http2https) Redirect
|
|
431
433
|
HTTP requests to HTTPS, both using the same port number, or different response
|
|
432
434
|
on HTTP and HTTPS.
|
|
@@ -746,7 +748,6 @@ middlewares into Fastify plugins
|
|
|
746
748
|
- [`typeorm-fastify-plugin`](https://github.com/jclemens24/fastify-typeorm) A simple
|
|
747
749
|
and updated Typeorm plugin for use with Fastify.
|
|
748
750
|
|
|
749
|
-
|
|
750
751
|
#### [Community Tools](#community-tools)
|
|
751
752
|
|
|
752
753
|
- [`@fastify-userland/workflows`](https://github.com/fastify-userland/workflows)
|
|
@@ -204,12 +204,12 @@ of an *arrow function expression*.
|
|
|
204
204
|
|
|
205
205
|
You can do the same for the `request` object:
|
|
206
206
|
```js
|
|
207
|
-
fastify.decorate('
|
|
208
|
-
return req.headers[header
|
|
207
|
+
fastify.decorate('getBoolHeader', (req, name) => {
|
|
208
|
+
return req.headers[name] ?? false // We return `false` if header is missing
|
|
209
209
|
})
|
|
210
210
|
|
|
211
211
|
fastify.addHook('preHandler', (request, reply, done) => {
|
|
212
|
-
request.isHappy = fastify.
|
|
212
|
+
request.isHappy = fastify.getBoolHeader(request, 'happy')
|
|
213
213
|
done()
|
|
214
214
|
})
|
|
215
215
|
|
|
@@ -219,14 +219,14 @@ fastify.get('/happiness', (request, reply) => {
|
|
|
219
219
|
```
|
|
220
220
|
Again, it works, but it can be much better!
|
|
221
221
|
```js
|
|
222
|
-
fastify.decorateRequest('
|
|
223
|
-
this.isHappy = this.headers[
|
|
222
|
+
fastify.decorateRequest('setBoolHeader', function (name) {
|
|
223
|
+
this.isHappy = this.headers[name] ?? false
|
|
224
224
|
})
|
|
225
225
|
|
|
226
226
|
fastify.decorateRequest('isHappy', false) // This will be added to the Request object prototype, yay speed!
|
|
227
227
|
|
|
228
228
|
fastify.addHook('preHandler', (request, reply, done) => {
|
|
229
|
-
request.
|
|
229
|
+
request.setBoolHeader('happy')
|
|
230
230
|
done()
|
|
231
231
|
})
|
|
232
232
|
|
|
@@ -337,11 +337,11 @@ fastify.register((instance, opts, done) => {
|
|
|
337
337
|
}
|
|
338
338
|
})
|
|
339
339
|
|
|
340
|
-
|
|
340
|
+
instance.get('/plugin1', {config: {useUtil: true}}, (request, reply) => {
|
|
341
341
|
reply.send(request)
|
|
342
342
|
})
|
|
343
343
|
|
|
344
|
-
|
|
344
|
+
instance.get('/plugin2', (request, reply) => {
|
|
345
345
|
reply.send(request)
|
|
346
346
|
})
|
|
347
347
|
|
package/docs/Reference/Server.md
CHANGED
|
@@ -534,7 +534,9 @@ recommend using a custom parser to convert only the keys to lowercase.
|
|
|
534
534
|
```js
|
|
535
535
|
const qs = require('qs')
|
|
536
536
|
const fastify = require('fastify')({
|
|
537
|
-
|
|
537
|
+
routerOptions: {
|
|
538
|
+
querystringParser: str => qs.parse(str)
|
|
539
|
+
}
|
|
538
540
|
})
|
|
539
541
|
```
|
|
540
542
|
|
|
@@ -544,7 +546,9 @@ like the example below for case insensitive keys and values:
|
|
|
544
546
|
```js
|
|
545
547
|
const querystring = require('fast-querystring')
|
|
546
548
|
const fastify = require('fastify')({
|
|
547
|
-
|
|
549
|
+
routerOptions: {
|
|
550
|
+
querystringParser: str => querystring.parse(str.toLowerCase())
|
|
551
|
+
}
|
|
548
552
|
})
|
|
549
553
|
```
|
|
550
554
|
|
|
@@ -675,9 +675,12 @@ const schema = {
|
|
|
675
675
|
content: {
|
|
676
676
|
'application/json': {
|
|
677
677
|
schema: {
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
678
|
+
type: 'object',
|
|
679
|
+
properties: {
|
|
680
|
+
name: { type: 'string' },
|
|
681
|
+
image: { type: 'string' },
|
|
682
|
+
address: { type: 'string' }
|
|
683
|
+
}
|
|
681
684
|
}
|
|
682
685
|
},
|
|
683
686
|
'application/vnd.v1+json': {
|
|
@@ -692,8 +695,11 @@ const schema = {
|
|
|
692
695
|
content: {
|
|
693
696
|
'application/vnd.v2+json': {
|
|
694
697
|
schema: {
|
|
695
|
-
|
|
696
|
-
|
|
698
|
+
type: 'object',
|
|
699
|
+
properties: {
|
|
700
|
+
fullName: { type: 'string' },
|
|
701
|
+
phone: { type: 'string' }
|
|
702
|
+
}
|
|
697
703
|
}
|
|
698
704
|
}
|
|
699
705
|
}
|
|
@@ -703,7 +709,10 @@ const schema = {
|
|
|
703
709
|
// */* is match-all content-type
|
|
704
710
|
'*/*': {
|
|
705
711
|
schema: {
|
|
706
|
-
|
|
712
|
+
type: 'object',
|
|
713
|
+
properties: {
|
|
714
|
+
desc: { type: 'string' }
|
|
715
|
+
}
|
|
707
716
|
}
|
|
708
717
|
}
|
|
709
718
|
}
|
package/fastify.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const { AsyncResource } = require('node:async_hooks')
|
|
4
4
|
const { FifoMap: Fifo } = require('toad-cache')
|
|
5
5
|
const { parse: secureJsonParse } = require('secure-json-parse')
|
|
6
|
+
const ContentType = require('./content-type')
|
|
6
7
|
const {
|
|
7
8
|
kDefaultJsonParse,
|
|
8
9
|
kContentTypeParser,
|
|
@@ -75,8 +76,13 @@ ContentTypeParser.prototype.add = function (contentType, opts, parserFn) {
|
|
|
75
76
|
this.customParsers.set('', parser)
|
|
76
77
|
} else {
|
|
77
78
|
if (contentTypeIsString) {
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
const ct = new ContentType(contentType)
|
|
80
|
+
if (ct.isValid === false) {
|
|
81
|
+
throw new FST_ERR_CTP_INVALID_TYPE()
|
|
82
|
+
}
|
|
83
|
+
const normalizedContentType = ct.toString()
|
|
84
|
+
this.parserList.unshift(normalizedContentType)
|
|
85
|
+
this.customParsers.set(normalizedContentType, parser)
|
|
80
86
|
} else {
|
|
81
87
|
validateRegExp(contentType)
|
|
82
88
|
this.parserRegExpList.unshift(contentType)
|
|
@@ -87,7 +93,7 @@ ContentTypeParser.prototype.add = function (contentType, opts, parserFn) {
|
|
|
87
93
|
|
|
88
94
|
ContentTypeParser.prototype.hasParser = function (contentType) {
|
|
89
95
|
if (typeof contentType === 'string') {
|
|
90
|
-
contentType = contentType
|
|
96
|
+
contentType = new ContentType(contentType).toString()
|
|
91
97
|
} else {
|
|
92
98
|
if (!(contentType instanceof RegExp)) throw new FST_ERR_CTP_INVALID_TYPE()
|
|
93
99
|
contentType = contentType.toString()
|
|
@@ -97,45 +103,49 @@ ContentTypeParser.prototype.hasParser = function (contentType) {
|
|
|
97
103
|
}
|
|
98
104
|
|
|
99
105
|
ContentTypeParser.prototype.existingParser = function (contentType) {
|
|
100
|
-
if (contentType === '
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
106
|
+
if (typeof contentType === 'string') {
|
|
107
|
+
const ct = new ContentType(contentType).toString()
|
|
108
|
+
if (contentType === 'application/json' && this.customParsers.has(contentType)) {
|
|
109
|
+
return this.customParsers.get(ct).fn !== this[kDefaultJsonParse]
|
|
110
|
+
}
|
|
111
|
+
if (contentType === 'text/plain' && this.customParsers.has(contentType)) {
|
|
112
|
+
return this.customParsers.get(ct).fn !== defaultPlainTextParser
|
|
113
|
+
}
|
|
105
114
|
}
|
|
106
115
|
|
|
107
116
|
return this.hasParser(contentType)
|
|
108
117
|
}
|
|
109
118
|
|
|
110
119
|
ContentTypeParser.prototype.getParser = function (contentType) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
120
|
+
if (typeof contentType === 'string') {
|
|
121
|
+
contentType = new ContentType(contentType)
|
|
122
|
+
}
|
|
123
|
+
const ct = contentType.toString()
|
|
124
|
+
|
|
125
|
+
let parser = this.cache.get(ct)
|
|
114
126
|
if (parser !== undefined) return parser
|
|
127
|
+
parser = this.customParsers.get(ct)
|
|
128
|
+
if (parser !== undefined) {
|
|
129
|
+
this.cache.set(ct, parser)
|
|
130
|
+
return parser
|
|
131
|
+
}
|
|
115
132
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
caseInsensitiveContentType.charCodeAt(parserListItem.length) === 9 /* `\t` */
|
|
126
|
-
)
|
|
127
|
-
) {
|
|
128
|
-
parser = this.customParsers.get(parserListItem)
|
|
129
|
-
this.cache.set(contentType, parser)
|
|
130
|
-
return parser
|
|
131
|
-
}
|
|
133
|
+
// We have conflicting desires across our test suite. In some cases, we
|
|
134
|
+
// expect to get a parser by just passing the media-type. In others, we expect
|
|
135
|
+
// to get a parser registered under the media-type while also providing
|
|
136
|
+
// parameters. And in yet others, we expect to register a parser under the
|
|
137
|
+
// media-type and have it apply to any request with a header that starts
|
|
138
|
+
// with that type.
|
|
139
|
+
parser = this.customParsers.get(contentType.mediaType)
|
|
140
|
+
if (parser !== undefined) {
|
|
141
|
+
return parser
|
|
132
142
|
}
|
|
133
143
|
|
|
134
144
|
for (let j = 0; j !== this.parserRegExpList.length; ++j) {
|
|
135
145
|
const parserRegExp = this.parserRegExpList[j]
|
|
136
|
-
if (parserRegExp.test(
|
|
146
|
+
if (parserRegExp.test(ct)) {
|
|
137
147
|
parser = this.customParsers.get(parserRegExp.toString())
|
|
138
|
-
this.cache.set(
|
|
148
|
+
this.cache.set(ct, parser)
|
|
139
149
|
return parser
|
|
140
150
|
}
|
|
141
151
|
}
|
|
@@ -154,7 +164,7 @@ ContentTypeParser.prototype.remove = function (contentType) {
|
|
|
154
164
|
let parsers
|
|
155
165
|
|
|
156
166
|
if (typeof contentType === 'string') {
|
|
157
|
-
contentType = contentType
|
|
167
|
+
contentType = new ContentType(contentType).toString()
|
|
158
168
|
parsers = this.parserList
|
|
159
169
|
} else {
|
|
160
170
|
if (!(contentType instanceof RegExp)) throw new FST_ERR_CTP_INVALID_TYPE()
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* keyValuePairsReg is used to split the parameters list into associated
|
|
5
|
+
* key value pairings.
|
|
6
|
+
*
|
|
7
|
+
* @see https://httpwg.org/specs/rfc9110.html#parameter
|
|
8
|
+
* @type {RegExp}
|
|
9
|
+
*/
|
|
10
|
+
const keyValuePairsReg = /([\w!#$%&'*+.^`|~-]+)=([^;]*)/gm
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* typeNameReg is used to validate that the first part of the media-type
|
|
14
|
+
* does not use disallowed characters.
|
|
15
|
+
*
|
|
16
|
+
* @see https://httpwg.org/specs/rfc9110.html#rule.token.separators
|
|
17
|
+
* @type {RegExp}
|
|
18
|
+
*/
|
|
19
|
+
const typeNameReg = /^[\w!#$%&'*+.^`|~-]+$/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* subtypeNameReg is used to validate that the second part of the media-type
|
|
23
|
+
* does not use disallowed characters.
|
|
24
|
+
*
|
|
25
|
+
* @see https://httpwg.org/specs/rfc9110.html#rule.token.separators
|
|
26
|
+
* @type {RegExp}
|
|
27
|
+
*/
|
|
28
|
+
const subtypeNameReg = /^[\w!#$%&'*+.^`|~-]+\s*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* ContentType parses and represents the value of the content-type header.
|
|
32
|
+
*
|
|
33
|
+
* @see https://httpwg.org/specs/rfc9110.html#media.type
|
|
34
|
+
* @see https://httpwg.org/specs/rfc9110.html#parameter
|
|
35
|
+
*/
|
|
36
|
+
class ContentType {
|
|
37
|
+
#valid = false
|
|
38
|
+
#empty = true
|
|
39
|
+
#type = ''
|
|
40
|
+
#subtype = ''
|
|
41
|
+
#parameters = new Map()
|
|
42
|
+
#string
|
|
43
|
+
|
|
44
|
+
constructor (headerValue) {
|
|
45
|
+
if (headerValue == null || headerValue === '' || headerValue === 'undefined') {
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let sepIdx = headerValue.indexOf(';')
|
|
50
|
+
if (sepIdx === -1) {
|
|
51
|
+
// The value is the simplest `type/subtype` variant.
|
|
52
|
+
sepIdx = headerValue.indexOf('/')
|
|
53
|
+
if (sepIdx === -1) {
|
|
54
|
+
// Got a string without the correct `type/subtype` format.
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const type = headerValue.slice(0, sepIdx).trimStart().toLowerCase()
|
|
59
|
+
const subtype = headerValue.slice(sepIdx + 1).trimEnd().toLowerCase()
|
|
60
|
+
|
|
61
|
+
if (
|
|
62
|
+
typeNameReg.test(type) === true &&
|
|
63
|
+
subtypeNameReg.test(subtype) === true
|
|
64
|
+
) {
|
|
65
|
+
this.#valid = true
|
|
66
|
+
this.#empty = false
|
|
67
|
+
this.#type = type
|
|
68
|
+
this.#subtype = subtype
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// We have a `type/subtype; params=list...` header value.
|
|
75
|
+
const mediaType = headerValue.slice(0, sepIdx).toLowerCase()
|
|
76
|
+
const paramsList = headerValue.slice(sepIdx + 1).trim()
|
|
77
|
+
|
|
78
|
+
sepIdx = mediaType.indexOf('/')
|
|
79
|
+
if (sepIdx === -1) {
|
|
80
|
+
// We got an invalid string like `something; params=list...`.
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
const type = mediaType.slice(0, sepIdx).trimStart()
|
|
84
|
+
const subtype = mediaType.slice(sepIdx + 1).trimEnd()
|
|
85
|
+
|
|
86
|
+
if (
|
|
87
|
+
typeNameReg.test(type) === false ||
|
|
88
|
+
subtypeNameReg.test(subtype) === false
|
|
89
|
+
) {
|
|
90
|
+
// Some portion of the media-type is using invalid characters. Therefore,
|
|
91
|
+
// the content-type header is invalid.
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
this.#type = type
|
|
95
|
+
this.#subtype = subtype
|
|
96
|
+
this.#valid = true
|
|
97
|
+
this.#empty = false
|
|
98
|
+
|
|
99
|
+
let matches = keyValuePairsReg.exec(paramsList)
|
|
100
|
+
while (matches) {
|
|
101
|
+
const key = matches[1]
|
|
102
|
+
const value = matches[2]
|
|
103
|
+
if (value[0] === '"') {
|
|
104
|
+
if (value.at(-1) !== '"') {
|
|
105
|
+
this.#parameters.set(key, 'invalid quoted string')
|
|
106
|
+
matches = keyValuePairsReg.exec(paramsList)
|
|
107
|
+
continue
|
|
108
|
+
}
|
|
109
|
+
// We should probably verify the value matches a quoted string
|
|
110
|
+
// (https://httpwg.org/specs/rfc9110.html#rule.quoted-string) value.
|
|
111
|
+
// But we are not really doing much with the parameter values, so we
|
|
112
|
+
// are omitting that at this time.
|
|
113
|
+
this.#parameters.set(key, value.slice(1, value.length - 1))
|
|
114
|
+
} else {
|
|
115
|
+
this.#parameters.set(key, value)
|
|
116
|
+
}
|
|
117
|
+
matches = keyValuePairsReg.exec(paramsList)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
get [Symbol.toStringTag] () { return 'ContentType' }
|
|
122
|
+
|
|
123
|
+
get isEmpty () { return this.#empty }
|
|
124
|
+
|
|
125
|
+
get isValid () { return this.#valid }
|
|
126
|
+
|
|
127
|
+
get mediaType () { return `${this.#type}/${this.#subtype}` }
|
|
128
|
+
|
|
129
|
+
get type () { return this.#type }
|
|
130
|
+
|
|
131
|
+
get subtype () { return this.#subtype }
|
|
132
|
+
|
|
133
|
+
get parameters () { return this.#parameters }
|
|
134
|
+
|
|
135
|
+
toString () {
|
|
136
|
+
/* c8 ignore next: we don't need to verify the cache */
|
|
137
|
+
if (this.#string) return this.#string
|
|
138
|
+
const parameters = []
|
|
139
|
+
for (const [key, value] of this.#parameters.entries()) {
|
|
140
|
+
parameters.push(`${key}="${value}"`)
|
|
141
|
+
}
|
|
142
|
+
const result = [this.#type, '/', this.#subtype]
|
|
143
|
+
if (parameters.length > 0) {
|
|
144
|
+
result.push('; ')
|
|
145
|
+
result.push(parameters.join('; '))
|
|
146
|
+
}
|
|
147
|
+
this.#string = result.join('')
|
|
148
|
+
return this.#string
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
module.exports = ContentType
|
package/lib/error-serializer.js
CHANGED
|
@@ -13,6 +13,20 @@
|
|
|
13
13
|
module.exports = function anonymous(validator,serializer
|
|
14
14
|
) {
|
|
15
15
|
|
|
16
|
+
|
|
17
|
+
const {
|
|
18
|
+
asString,
|
|
19
|
+
asNumber,
|
|
20
|
+
asBoolean,
|
|
21
|
+
asDateTime,
|
|
22
|
+
asDate,
|
|
23
|
+
asTime,
|
|
24
|
+
asUnsafeString
|
|
25
|
+
} = serializer
|
|
26
|
+
|
|
27
|
+
const asInteger = serializer.asInteger.bind(serializer)
|
|
28
|
+
|
|
29
|
+
|
|
16
30
|
const JSON_STR_BEGIN_OBJECT = '{'
|
|
17
31
|
const JSON_STR_END_OBJECT = '}'
|
|
18
32
|
const JSON_STR_BEGIN_ARRAY = '['
|
|
@@ -43,7 +57,7 @@ let addComma = false
|
|
|
43
57
|
if (value !== undefined) {
|
|
44
58
|
!addComma && (addComma = true) || (json += JSON_STR_COMMA)
|
|
45
59
|
json += "\"statusCode\":"
|
|
46
|
-
json +=
|
|
60
|
+
json += asNumber(value)
|
|
47
61
|
}
|
|
48
62
|
|
|
49
63
|
value = obj["code"]
|
|
@@ -57,12 +71,12 @@ let addComma = false
|
|
|
57
71
|
} else if (value instanceof Date) {
|
|
58
72
|
json += JSON_STR_QUOTE + value.toISOString() + JSON_STR_QUOTE
|
|
59
73
|
} else if (value instanceof RegExp) {
|
|
60
|
-
json +=
|
|
74
|
+
json += asString(value.source)
|
|
61
75
|
} else {
|
|
62
|
-
json +=
|
|
76
|
+
json += asString(value.toString())
|
|
63
77
|
}
|
|
64
78
|
} else {
|
|
65
|
-
json +=
|
|
79
|
+
json += asString(value)
|
|
66
80
|
}
|
|
67
81
|
|
|
68
82
|
}
|
|
@@ -78,12 +92,12 @@ let addComma = false
|
|
|
78
92
|
} else if (value instanceof Date) {
|
|
79
93
|
json += JSON_STR_QUOTE + value.toISOString() + JSON_STR_QUOTE
|
|
80
94
|
} else if (value instanceof RegExp) {
|
|
81
|
-
json +=
|
|
95
|
+
json += asString(value.source)
|
|
82
96
|
} else {
|
|
83
|
-
json +=
|
|
97
|
+
json += asString(value.toString())
|
|
84
98
|
}
|
|
85
99
|
} else {
|
|
86
|
-
json +=
|
|
100
|
+
json += asString(value)
|
|
87
101
|
}
|
|
88
102
|
|
|
89
103
|
}
|
|
@@ -99,12 +113,12 @@ let addComma = false
|
|
|
99
113
|
} else if (value instanceof Date) {
|
|
100
114
|
json += JSON_STR_QUOTE + value.toISOString() + JSON_STR_QUOTE
|
|
101
115
|
} else if (value instanceof RegExp) {
|
|
102
|
-
json +=
|
|
116
|
+
json += asString(value.source)
|
|
103
117
|
} else {
|
|
104
|
-
json +=
|
|
118
|
+
json += asString(value.toString())
|
|
105
119
|
}
|
|
106
120
|
} else {
|
|
107
|
-
json +=
|
|
121
|
+
json += asString(value)
|
|
108
122
|
}
|
|
109
123
|
|
|
110
124
|
}
|
package/lib/handle-request.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const diagnostics = require('node:diagnostics_channel')
|
|
4
|
+
const ContentType = require('./content-type')
|
|
5
|
+
const wrapThenable = require('./wrap-thenable')
|
|
4
6
|
const { validate: validateSchema } = require('./validation')
|
|
5
7
|
const { preValidationHookRunner, preHandlerHookRunner } = require('./hooks')
|
|
6
|
-
const
|
|
8
|
+
const { FST_ERR_CTP_INVALID_MEDIA_TYPE } = require('./errors')
|
|
7
9
|
const { setErrorStatusCode } = require('./error-status')
|
|
8
10
|
const {
|
|
9
11
|
kReplyIsError,
|
|
@@ -31,9 +33,9 @@ function handleRequest (err, request, reply) {
|
|
|
31
33
|
|
|
32
34
|
if (this[kSupportedHTTPMethods].bodywith.has(method)) {
|
|
33
35
|
const headers = request.headers
|
|
34
|
-
const
|
|
36
|
+
const ctHeader = headers['content-type']
|
|
35
37
|
|
|
36
|
-
if (
|
|
38
|
+
if (ctHeader === undefined) {
|
|
37
39
|
const contentLength = headers['content-length']
|
|
38
40
|
const transferEncoding = headers['transfer-encoding']
|
|
39
41
|
const isEmptyBody = transferEncoding === undefined &&
|
|
@@ -49,7 +51,13 @@ function handleRequest (err, request, reply) {
|
|
|
49
51
|
return
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
|
|
54
|
+
const contentType = new ContentType(ctHeader)
|
|
55
|
+
if (contentType.isValid === false) {
|
|
56
|
+
reply[kReplyIsError] = true
|
|
57
|
+
reply.status(415).send(new FST_ERR_CTP_INVALID_MEDIA_TYPE())
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
request[kRouteContext].contentTypeParser.run(contentType.toString(), handler, request, reply)
|
|
53
61
|
return
|
|
54
62
|
}
|
|
55
63
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fastify",
|
|
3
|
-
"version": "5.7.
|
|
3
|
+
"version": "5.7.2",
|
|
4
4
|
"description": "Fast and low overhead web framework, for Node.js",
|
|
5
5
|
"main": "fastify.js",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"benchmark:parser:error": "concurrently -k -s first \"node ./examples/benchmark/parser.js\" \"autocannon -c 100 -d 30 -p 10 -i ./examples/benchmark/body.json -H \"content-type:application/jsoff\" -H \"content-length:123\" -m POST localhost:3000/\"",
|
|
13
13
|
"build:validation": "node build/build-error-serializer.js && node build/build-validation.js",
|
|
14
14
|
"build:sync-version": "node build/sync-version.js",
|
|
15
|
-
"coverage": "c8 --reporter html borp --reporter=@jsumners/line-reporter
|
|
15
|
+
"coverage": "c8 --reporter html borp --reporter=@jsumners/line-reporter",
|
|
16
16
|
"coverage:ci-check-coverage": "borp --reporter=@jsumners/line-reporter --coverage --check-coverage --lines 100",
|
|
17
17
|
"lint": "npm run lint:eslint",
|
|
18
18
|
"lint:fix": "eslint --fix",
|