fastify 4.0.2 → 4.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/docs/Guides/Ecosystem.md +3 -0
- package/docs/Guides/Migration-Guide-V4.md +1 -1
- package/docs/Reference/LTS.md +2 -2
- package/docs/Reference/Type-Providers.md +2 -1
- package/fastify.js +1 -1
- package/lib/error-serializer.js +175 -7
- package/lib/server.js +9 -1
- package/lib/wrapThenable.js +8 -3
- package/package.json +4 -4
- package/test/listen.test.js +16 -2
- package/test/stream.test.js +73 -0
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ developer experience with the least overhead and a powerful plugin architecture.
|
|
|
56
56
|
It is inspired by Hapi and Express and as far as we know, it is one of the
|
|
57
57
|
fastest web frameworks in town.
|
|
58
58
|
|
|
59
|
-
This branch refers to the
|
|
59
|
+
This branch refers to the Fastify v4 release. Check out the
|
|
60
60
|
[v3.x](https://github.com/fastify/fastify/tree/v3.x) branch for v3.
|
|
61
61
|
|
|
62
62
|
### Quick start
|
|
@@ -104,11 +104,11 @@ project as a dependency:
|
|
|
104
104
|
|
|
105
105
|
Install with npm:
|
|
106
106
|
```sh
|
|
107
|
-
npm i fastify
|
|
107
|
+
npm i fastify
|
|
108
108
|
```
|
|
109
109
|
Install with yarn:
|
|
110
110
|
```sh
|
|
111
|
-
yarn add fastify
|
|
111
|
+
yarn add fastify
|
|
112
112
|
```
|
|
113
113
|
|
|
114
114
|
### Example
|
package/docs/Guides/Ecosystem.md
CHANGED
|
@@ -346,6 +346,9 @@ section.
|
|
|
346
346
|
minification and transformation of responses.
|
|
347
347
|
- [`fastify-mongo-memory`](https://github.com/chapuletta/fastify-mongo-memory)
|
|
348
348
|
Fastify MongoDB in Memory Plugin for testing support.
|
|
349
|
+
- [`fastify-mongodb-sanitizer`](https://github.com/KlemenKozelj/fastify-mongodb-sanitizer)
|
|
350
|
+
Fastify plugin that sanitizes client input to prevent
|
|
351
|
+
potential MongoDB query injection attacks.
|
|
349
352
|
- [`fastify-mongoose-api`](https://github.com/jeka-kiselyov/fastify-mongoose-api)
|
|
350
353
|
Fastify plugin to create REST API methods based on Mongoose MongoDB models.
|
|
351
354
|
- [`fastify-mongoose-driver`](https://github.com/alex-ppg/fastify-mongoose)
|
|
@@ -21,7 +21,7 @@ there and maintained. Use Fastify's [hooks](../Reference/Hooks.md) instead.
|
|
|
21
21
|
### Change of schema for multiple types
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
Since
|
|
24
|
+
Since Fastify v4 has upgraded to Ajv v8. The "type" keywords with multiple types
|
|
25
25
|
(other than with "null") are prohibited. Read more
|
|
26
26
|
['here'](https://ajv.js.org/strict-mode.html#strict-types)
|
|
27
27
|
|
package/docs/Reference/LTS.md
CHANGED
|
@@ -47,8 +47,8 @@ A "month" is defined as 30 consecutive days.
|
|
|
47
47
|
| :------ | :----------- | :-------------- | :------------------- |
|
|
48
48
|
| 1.0.0 | 2018-03-06 | 2019-09-01 | 6, 8, 9, 10, 11 |
|
|
49
49
|
| 2.0.0 | 2019-02-25 | 2021-01-31 | 6, 8, 10, 12, 14 |
|
|
50
|
-
| 3.0.0 | 2020-07-07 |
|
|
51
|
-
| 4.0.0 |
|
|
50
|
+
| 3.0.0 | 2020-07-07 | 2023-06-30 | 10, 12, 14, 16, 18 |
|
|
51
|
+
| 4.0.0 | 2022-06-08 | TBD | 14, 16, 18 |
|
|
52
52
|
|
|
53
53
|
### CI tested operating systems
|
|
54
54
|
<a id="supported-os"></a>
|
|
@@ -65,7 +65,8 @@ $ npm i @fastify/type-provider-typebox
|
|
|
65
65
|
```
|
|
66
66
|
|
|
67
67
|
```typescript
|
|
68
|
-
import { TypeBoxTypeProvider
|
|
68
|
+
import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox'
|
|
69
|
+
import { Type } from '@sinclair/typebox'
|
|
69
70
|
|
|
70
71
|
import fastify from 'fastify'
|
|
71
72
|
|
package/fastify.js
CHANGED
package/lib/error-serializer.js
CHANGED
|
@@ -1,15 +1,183 @@
|
|
|
1
1
|
// This file is autogenerated by build/build-error-serializer.js, do not edit
|
|
2
2
|
/* istanbul ignore file */
|
|
3
3
|
|
|
4
|
-
'use strict'
|
|
4
|
+
'use strict'
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Serializer {
|
|
9
|
+
constructor (options = {}) {
|
|
10
|
+
switch (options.rounding) {
|
|
11
|
+
case 'floor':
|
|
12
|
+
this.parseInteger = Math.floor
|
|
13
|
+
break
|
|
14
|
+
case 'ceil':
|
|
15
|
+
this.parseInteger = Math.ceil
|
|
16
|
+
break
|
|
17
|
+
case 'round':
|
|
18
|
+
this.parseInteger = Math.round
|
|
19
|
+
break
|
|
20
|
+
default:
|
|
21
|
+
this.parseInteger = Math.trunc
|
|
22
|
+
break
|
|
23
|
+
}
|
|
24
|
+
}
|
|
8
25
|
|
|
9
|
-
|
|
10
|
-
|
|
26
|
+
asAny (i) {
|
|
27
|
+
return JSON.stringify(i)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
asNull () {
|
|
31
|
+
return 'null'
|
|
32
|
+
}
|
|
11
33
|
|
|
34
|
+
asInteger (i) {
|
|
35
|
+
if (typeof i === 'bigint') {
|
|
36
|
+
return i.toString()
|
|
37
|
+
} else if (Number.isInteger(i)) {
|
|
38
|
+
return '' + i
|
|
39
|
+
} else {
|
|
40
|
+
/* eslint no-undef: "off" */
|
|
41
|
+
const integer = this.parseInteger(i)
|
|
42
|
+
if (Number.isNaN(integer)) {
|
|
43
|
+
throw new Error(`The value "${i}" cannot be converted to an integer.`)
|
|
44
|
+
} else {
|
|
45
|
+
return '' + integer
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
asIntegerNullable (i) {
|
|
51
|
+
return i === null ? 'null' : this.asInteger(i)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
asNumber (i) {
|
|
55
|
+
const num = Number(i)
|
|
56
|
+
if (Number.isNaN(num)) {
|
|
57
|
+
throw new Error(`The value "${i}" cannot be converted to a number.`)
|
|
58
|
+
} else {
|
|
59
|
+
return '' + num
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
asNumberNullable (i) {
|
|
64
|
+
return i === null ? 'null' : this.asNumber(i)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
asBoolean (bool) {
|
|
68
|
+
return bool && 'true' || 'false' // eslint-disable-line
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
asBooleanNullable (bool) {
|
|
72
|
+
return bool === null ? 'null' : this.asBoolean(bool)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
asDatetime (date, skipQuotes) {
|
|
76
|
+
const quotes = skipQuotes === true ? '' : '"'
|
|
77
|
+
if (date instanceof Date) {
|
|
78
|
+
return quotes + date.toISOString() + quotes
|
|
79
|
+
}
|
|
80
|
+
return this.asString(date, skipQuotes)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
asDatetimeNullable (date, skipQuotes) {
|
|
84
|
+
return date === null ? 'null' : this.asDatetime(date, skipQuotes)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
asDate (date, skipQuotes) {
|
|
88
|
+
const quotes = skipQuotes === true ? '' : '"'
|
|
89
|
+
if (date instanceof Date) {
|
|
90
|
+
return quotes + new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(0, 10) + quotes
|
|
91
|
+
}
|
|
92
|
+
return this.asString(date, skipQuotes)
|
|
93
|
+
}
|
|
12
94
|
|
|
95
|
+
asDateNullable (date, skipQuotes) {
|
|
96
|
+
return date === null ? 'null' : this.asDate(date, skipQuotes)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
asTime (date, skipQuotes) {
|
|
100
|
+
const quotes = skipQuotes === true ? '' : '"'
|
|
101
|
+
if (date instanceof Date) {
|
|
102
|
+
return quotes + new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(11, 19) + quotes
|
|
103
|
+
}
|
|
104
|
+
return this.asString(date, skipQuotes)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
asTimeNullable (date, skipQuotes) {
|
|
108
|
+
return date === null ? 'null' : this.asTime(date, skipQuotes)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
asString (str, skipQuotes) {
|
|
112
|
+
const quotes = skipQuotes === true ? '' : '"'
|
|
113
|
+
if (str instanceof Date) {
|
|
114
|
+
return quotes + str.toISOString() + quotes
|
|
115
|
+
} else if (str === null) {
|
|
116
|
+
return quotes + quotes
|
|
117
|
+
} else if (str instanceof RegExp) {
|
|
118
|
+
str = str.source
|
|
119
|
+
} else if (typeof str !== 'string') {
|
|
120
|
+
str = str.toString()
|
|
121
|
+
}
|
|
122
|
+
// If we skipQuotes it means that we are using it as test
|
|
123
|
+
// no need to test the string length for the render
|
|
124
|
+
if (skipQuotes) {
|
|
125
|
+
return str
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (str.length < 42) {
|
|
129
|
+
return this.asStringSmall(str)
|
|
130
|
+
} else {
|
|
131
|
+
return JSON.stringify(str)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
asStringNullable (str) {
|
|
136
|
+
return str === null ? 'null' : this.asString(str)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// magically escape strings for json
|
|
140
|
+
// relying on their charCodeAt
|
|
141
|
+
// everything below 32 needs JSON.stringify()
|
|
142
|
+
// every string that contain surrogate needs JSON.stringify()
|
|
143
|
+
// 34 and 92 happens all the time, so we
|
|
144
|
+
// have a fast case for them
|
|
145
|
+
asStringSmall (str) {
|
|
146
|
+
const l = str.length
|
|
147
|
+
let result = ''
|
|
148
|
+
let last = 0
|
|
149
|
+
let found = false
|
|
150
|
+
let surrogateFound = false
|
|
151
|
+
let point = 255
|
|
152
|
+
// eslint-disable-next-line
|
|
153
|
+
for (var i = 0; i < l && point >= 32; i++) {
|
|
154
|
+
point = str.charCodeAt(i)
|
|
155
|
+
if (point >= 0xD800 && point <= 0xDFFF) {
|
|
156
|
+
// The current character is a surrogate.
|
|
157
|
+
surrogateFound = true
|
|
158
|
+
}
|
|
159
|
+
if (point === 34 || point === 92) {
|
|
160
|
+
result += str.slice(last, i) + '\\'
|
|
161
|
+
last = i
|
|
162
|
+
found = true
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!found) {
|
|
167
|
+
result = str
|
|
168
|
+
} else {
|
|
169
|
+
result += str.slice(last)
|
|
170
|
+
}
|
|
171
|
+
return ((point < 32) || (surrogateFound === true)) ? JSON.stringify(str) : '"' + result + '"'
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
const serializer = new Serializer({"mode":"standalone"})
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
|
|
13
181
|
function main (input) {
|
|
14
182
|
let json = ''
|
|
15
183
|
json += anonymous0(input)
|
|
@@ -81,5 +249,5 @@ const ajv = buildAjv({})
|
|
|
81
249
|
|
|
82
250
|
|
|
83
251
|
|
|
84
|
-
module.exports = main
|
|
85
|
-
|
|
252
|
+
module.exports = main
|
|
253
|
+
|
package/lib/server.js
CHANGED
|
@@ -41,7 +41,15 @@ function createServer (options, httpHandler) {
|
|
|
41
41
|
listenOptions.cb = cb
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
// If we have a path specified, don't default host to 'localhost' so we don't end up listening
|
|
45
|
+
// on both path and host
|
|
46
|
+
// See https://github.com/fastify/fastify/issues/4007
|
|
47
|
+
let host
|
|
48
|
+
if (listenOptions.path == null) {
|
|
49
|
+
host = listenOptions.host ?? 'localhost'
|
|
50
|
+
} else {
|
|
51
|
+
host = listenOptions.host
|
|
52
|
+
}
|
|
45
53
|
if (Object.prototype.hasOwnProperty.call(listenOptions, 'host') === false) {
|
|
46
54
|
listenOptions.host = host
|
|
47
55
|
}
|
package/lib/wrapThenable.js
CHANGED
|
@@ -11,9 +11,14 @@ function wrapThenable (thenable, reply) {
|
|
|
11
11
|
return
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
// this is for async functions that
|
|
15
|
-
//
|
|
16
|
-
|
|
14
|
+
// this is for async functions that are using reply.send directly
|
|
15
|
+
//
|
|
16
|
+
// since wrap-thenable will be called when using reply.send directly
|
|
17
|
+
// without actual return. the response can be sent already or
|
|
18
|
+
// the request may be terminated during the reply. in this situation,
|
|
19
|
+
// it require an extra checking of request.aborted to see whether
|
|
20
|
+
// the request is killed by client.
|
|
21
|
+
if (payload !== undefined || (reply.sent === false && reply.raw.headersSent === false && reply.request.raw.aborted === false)) {
|
|
17
22
|
// we use a try-catch internally to avoid adding a catch to another
|
|
18
23
|
// promise, increase promise perf by 10%
|
|
19
24
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fastify",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.3",
|
|
4
4
|
"description": "Fast and low overhead web framework, for Node.js",
|
|
5
5
|
"main": "fastify.js",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -146,6 +146,7 @@
|
|
|
146
146
|
"eslint-plugin-n": "^15.2.0",
|
|
147
147
|
"eslint-plugin-promise": "^6.0.0",
|
|
148
148
|
"fast-json-body": "^1.1.0",
|
|
149
|
+
"fast-json-stringify": "^4.2.0",
|
|
149
150
|
"fastify-plugin": "^3.0.1",
|
|
150
151
|
"fluent-json-schema": "^3.1.0",
|
|
151
152
|
"form-data": "^4.0.0",
|
|
@@ -169,7 +170,7 @@
|
|
|
169
170
|
"split2": "^4.1.0",
|
|
170
171
|
"standard": "^17.0.0-2",
|
|
171
172
|
"tap": "^16.2.0",
|
|
172
|
-
"tsd": "^0.
|
|
173
|
+
"tsd": "^0.21.0",
|
|
173
174
|
"typescript": "^4.7.2",
|
|
174
175
|
"undici": "^5.4.0",
|
|
175
176
|
"x-xss-protection": "^2.0.0",
|
|
@@ -178,10 +179,9 @@
|
|
|
178
179
|
"dependencies": {
|
|
179
180
|
"@fastify/ajv-compiler": "^3.1.0",
|
|
180
181
|
"@fastify/error": "^3.0.0",
|
|
181
|
-
"@fastify/fast-json-stringify-compiler": "^3.0.
|
|
182
|
+
"@fastify/fast-json-stringify-compiler": "^3.0.1",
|
|
182
183
|
"abstract-logging": "^2.0.1",
|
|
183
184
|
"avvio": "^8.1.3",
|
|
184
|
-
"fast-json-stringify": "^4.1.0",
|
|
185
185
|
"find-my-way": "^6.3.0",
|
|
186
186
|
"light-my-request": "^5.0.0",
|
|
187
187
|
"pino": "^8.0.0",
|
package/test/listen.test.js
CHANGED
|
@@ -196,14 +196,28 @@ if (os.platform() !== 'win32') {
|
|
|
196
196
|
const fastify = Fastify()
|
|
197
197
|
t.teardown(fastify.close.bind(fastify))
|
|
198
198
|
|
|
199
|
-
const sockFile = path.join(os.tmpdir(), `${(Math.random().toString(16) + '0000000').
|
|
199
|
+
const sockFile = path.join(os.tmpdir(), `${(Math.random().toString(16) + '0000000').slice(2, 10)}-server.sock`)
|
|
200
200
|
try {
|
|
201
201
|
fs.unlinkSync(sockFile)
|
|
202
202
|
} catch (e) { }
|
|
203
203
|
|
|
204
204
|
fastify.listen({ path: sockFile }, (err, address) => {
|
|
205
205
|
t.error(err)
|
|
206
|
-
t.
|
|
206
|
+
t.strictSame(fastify.addresses(), [sockFile])
|
|
207
|
+
t.equal(address, sockFile)
|
|
208
|
+
})
|
|
209
|
+
})
|
|
210
|
+
} else {
|
|
211
|
+
test('listen on socket', t => {
|
|
212
|
+
t.plan(3)
|
|
213
|
+
const fastify = Fastify()
|
|
214
|
+
t.teardown(fastify.close.bind(fastify))
|
|
215
|
+
|
|
216
|
+
const sockFile = `\\\\.\\pipe\\${(Math.random().toString(16) + '0000000').slice(2, 10)}-server-sock`
|
|
217
|
+
|
|
218
|
+
fastify.listen({ path: sockFile }, (err, address) => {
|
|
219
|
+
t.error(err)
|
|
220
|
+
t.strictSame(fastify.addresses(), [sockFile])
|
|
207
221
|
t.equal(address, sockFile)
|
|
208
222
|
})
|
|
209
223
|
})
|
package/test/stream.test.js
CHANGED
|
@@ -741,3 +741,76 @@ test('reply.send handles aborted requests', t => {
|
|
|
741
741
|
}, 1)
|
|
742
742
|
})
|
|
743
743
|
})
|
|
744
|
+
|
|
745
|
+
test('request terminated should not crash fastify', t => {
|
|
746
|
+
t.plan(10)
|
|
747
|
+
|
|
748
|
+
const spyLogger = {
|
|
749
|
+
level: 'error',
|
|
750
|
+
fatal: () => { },
|
|
751
|
+
error: () => {
|
|
752
|
+
t.fail('should not log an error')
|
|
753
|
+
},
|
|
754
|
+
warn: () => { },
|
|
755
|
+
info: () => { },
|
|
756
|
+
debug: () => { },
|
|
757
|
+
trace: () => { },
|
|
758
|
+
child: () => { return spyLogger }
|
|
759
|
+
}
|
|
760
|
+
const fastify = Fastify({
|
|
761
|
+
logger: spyLogger
|
|
762
|
+
})
|
|
763
|
+
|
|
764
|
+
fastify.get('/', async (req, reply) => {
|
|
765
|
+
const stream = new Readable()
|
|
766
|
+
stream._read = () => {}
|
|
767
|
+
reply.header('content-type', 'text/html; charset=utf-8')
|
|
768
|
+
reply.header('transfer-encoding', 'chunked')
|
|
769
|
+
stream.push('<h1>HTML</h1>')
|
|
770
|
+
|
|
771
|
+
reply.send(stream)
|
|
772
|
+
|
|
773
|
+
await new Promise((resolve) => { setTimeout(resolve, 6).unref() })
|
|
774
|
+
|
|
775
|
+
stream.push('<h1>should disply on second stream</h1>')
|
|
776
|
+
stream.push(null)
|
|
777
|
+
return reply
|
|
778
|
+
})
|
|
779
|
+
|
|
780
|
+
fastify.listen({ port: 0 }, err => {
|
|
781
|
+
t.error(err)
|
|
782
|
+
t.teardown(() => { fastify.close() })
|
|
783
|
+
|
|
784
|
+
const port = fastify.server.address().port
|
|
785
|
+
const http = require('http')
|
|
786
|
+
const req = http.get(`http://localhost:${port}`, function (res) {
|
|
787
|
+
const { statusCode, headers } = res
|
|
788
|
+
t.equal(statusCode, 200)
|
|
789
|
+
t.equal(headers['content-type'], 'text/html; charset=utf-8')
|
|
790
|
+
t.equal(headers['transfer-encoding'], 'chunked')
|
|
791
|
+
res.on('data', function (chunk) {
|
|
792
|
+
t.equal(chunk.toString(), '<h1>HTML</h1>')
|
|
793
|
+
})
|
|
794
|
+
|
|
795
|
+
setTimeout(() => {
|
|
796
|
+
req.destroy()
|
|
797
|
+
|
|
798
|
+
// the server is not crash, we can connect it
|
|
799
|
+
http.get(`http://localhost:${port}`, function (res) {
|
|
800
|
+
const { statusCode, headers } = res
|
|
801
|
+
t.equal(statusCode, 200)
|
|
802
|
+
t.equal(headers['content-type'], 'text/html; charset=utf-8')
|
|
803
|
+
t.equal(headers['transfer-encoding'], 'chunked')
|
|
804
|
+
let payload = ''
|
|
805
|
+
res.on('data', function (chunk) {
|
|
806
|
+
payload += chunk.toString()
|
|
807
|
+
})
|
|
808
|
+
res.on('end', function () {
|
|
809
|
+
t.equal(payload, '<h1>HTML</h1><h1>should disply on second stream</h1>')
|
|
810
|
+
t.pass('should end properly')
|
|
811
|
+
})
|
|
812
|
+
})
|
|
813
|
+
}, 1)
|
|
814
|
+
})
|
|
815
|
+
})
|
|
816
|
+
})
|