fastify 5.8.5 → 5.10.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/PROJECT_CHARTER.md +1 -1
- package/README.md +7 -10
- package/SECURITY.md +1 -1
- package/SPONSORS.md +6 -4
- package/build/build-validation.js +2 -2
- package/docs/Guides/Database.md +0 -28
- package/docs/Guides/Delay-Accepting-Requests.md +1 -1
- package/docs/Guides/Ecosystem.md +23 -9
- package/docs/Guides/Getting-Started.md +2 -2
- package/docs/Guides/Migration-Guide-V4.md +2 -2
- package/docs/Guides/Migration-Guide-V5.md +1 -1
- package/docs/Guides/Plugins-Guide.md +1 -1
- package/docs/Guides/Prototype-Poisoning.md +3 -3
- package/docs/Guides/Serverless.md +8 -15
- package/docs/Guides/Style-Guide.md +1 -1
- package/docs/Guides/Write-Plugin.md +1 -1
- package/docs/Reference/Encapsulation.md +27 -26
- package/docs/Reference/Errors.md +12 -4
- package/docs/Reference/HTTP2.md +10 -10
- package/docs/Reference/Hooks.md +5 -5
- package/docs/Reference/Index.md +14 -16
- package/docs/Reference/LTS.md +12 -13
- package/docs/Reference/Lifecycle.md +9 -8
- package/docs/Reference/Logging.md +47 -39
- package/docs/Reference/Middleware.md +21 -25
- package/docs/Reference/Principles.md +2 -2
- package/docs/Reference/Reply.md +6 -1
- package/docs/Reference/Request.md +29 -18
- package/docs/Reference/Routes.md +5 -2
- package/docs/Reference/Server.md +138 -11
- package/docs/Reference/Type-Providers.md +53 -9
- package/docs/Reference/TypeScript.md +3 -3
- package/docs/Reference/Validation-and-Serialization.md +15 -2
- package/docs/Reference/Warnings.md +11 -6
- package/eslint.config.js +7 -2
- package/fastify.d.ts +13 -3
- package/fastify.js +60 -31
- package/lib/content-type-parser.js +2 -0
- package/lib/content-type.js +34 -1
- package/lib/context.js +0 -3
- package/lib/decorate.js +11 -3
- package/lib/error-handler.js +10 -28
- package/lib/error-serializer.js +59 -59
- package/lib/errors.js +23 -1
- package/lib/four-oh-four.js +22 -19
- package/lib/handle-request.js +12 -5
- package/lib/log-controller.js +169 -0
- package/lib/logger-factory.js +25 -4
- package/lib/plugin-override.js +2 -1
- package/lib/plugin-utils.js +5 -5
- package/lib/reply.js +96 -50
- package/lib/req-id-gen-factory.js +4 -1
- package/lib/request.js +16 -6
- package/lib/route.js +47 -41
- package/lib/schema-controller.js +1 -1
- package/lib/schemas.js +37 -30
- package/lib/symbols.js +4 -2
- package/lib/validation.js +10 -13
- package/lib/warnings.js +22 -4
- package/package.json +15 -17
- package/scripts/validate-ecosystem-links.js +1 -0
- package/test/bundler/esbuild/package.json +1 -1
- package/test/close-pipelining.test.js +1 -2
- package/test/content-type.test.js +20 -0
- package/test/custom-http-server.test.js +38 -0
- package/test/decorator-instance-properties.test.js +63 -0
- package/test/diagnostics-channel/async-error-handler.test.js +74 -0
- package/test/genReqId.test.js +24 -0
- package/test/hooks.test.js +94 -0
- package/test/http-methods/get.test.js +1 -1
- package/test/http2/plain.test.js +135 -0
- package/test/http2/secure-with-fallback.test.js +1 -1
- package/test/https/https.test.js +1 -2
- package/test/internals/errors.test.js +31 -1
- package/test/internals/logger.test.js +322 -0
- package/test/internals/plugin.test.js +3 -1
- package/test/internals/reply.test.js +35 -4
- package/test/internals/request.test.js +37 -10
- package/test/internals/schema-controller-perf.test.js +33 -0
- package/test/logger/logging.test.js +57 -1
- package/test/logger/options.test.js +38 -1
- package/test/reply-error.test.js +1 -1
- package/test/reply-trailers.test.js +70 -0
- package/test/request-media-type.test.js +105 -0
- package/test/route-prefix.test.js +34 -0
- package/test/router-options.test.js +222 -11
- package/test/schema-serialization.test.js +108 -0
- package/test/schema-validation.test.js +24 -0
- package/test/scripts/validate-ecosystem-links.test.js +40 -57
- package/test/stream.4.test.js +2 -2
- package/test/throw.test.js +14 -0
- package/test/trust-proxy.test.js +70 -30
- package/test/types/content-type-parser.tst.ts +70 -0
- package/test/types/{decorate-request-reply.test-d.ts → decorate-request-reply.tst.ts} +4 -4
- package/test/types/{dummy-plugin.ts → dummy-plugin.mts} +1 -1
- package/test/types/errors.tst.ts +91 -0
- package/test/types/fastify.tst.ts +351 -0
- package/test/types/hooks.tst.ts +578 -0
- package/test/types/instance.tst.ts +597 -0
- package/test/types/{logger.test-d.ts → logger.tst.ts} +61 -62
- package/test/types/plugin.tst.ts +96 -0
- package/test/types/register.tst.ts +245 -0
- package/test/types/reply.tst.ts +297 -0
- package/test/types/request.tst.ts +199 -0
- package/test/types/route.tst.ts +576 -0
- package/test/types/{schema.test-d.ts → schema.tst.ts} +22 -22
- package/test/types/{serverFactory.test-d.ts → serverFactory.tst.ts} +4 -4
- package/test/types/tsconfig.json +9 -0
- package/test/types/{type-provider.test-d.ts → type-provider.tst.ts} +256 -250
- package/test/types/using.tst.ts +14 -0
- package/types/errors.d.ts +4 -0
- package/types/instance.d.ts +2 -0
- package/types/logger.d.ts +22 -0
- package/types/request.d.ts +23 -2
- package/test/types/content-type-parser.test-d.ts +0 -72
- package/test/types/errors.test-d.ts +0 -90
- package/test/types/fastify.test-d.ts +0 -352
- package/test/types/hooks.test-d.ts +0 -550
- package/test/types/import.ts +0 -2
- package/test/types/instance.test-d.ts +0 -588
- package/test/types/plugin.test-d.ts +0 -97
- package/test/types/register.test-d.ts +0 -237
- package/test/types/reply.test-d.ts +0 -254
- package/test/types/request.test-d.ts +0 -188
- package/test/types/route.test-d.ts +0 -553
- package/test/types/using.test-d.ts +0 -17
package/lib/symbols.js
CHANGED
|
@@ -13,7 +13,6 @@ const keys = {
|
|
|
13
13
|
kContentTypeParser: Symbol('fastify.contentTypeParser'),
|
|
14
14
|
kState: Symbol('fastify.state'),
|
|
15
15
|
kOptions: Symbol('fastify.options'),
|
|
16
|
-
kDisableRequestLogging: Symbol('fastify.disableRequestLogging'),
|
|
17
16
|
kPluginNameChain: Symbol('fastify.pluginNameChain'),
|
|
18
17
|
kRouteContext: Symbol('fastify.context'),
|
|
19
18
|
kGenReqId: Symbol('fastify.genReqId'),
|
|
@@ -32,6 +31,7 @@ const keys = {
|
|
|
32
31
|
kRequestPayloadStream: Symbol('fastify.RequestPayloadStream'),
|
|
33
32
|
kRequestAcceptVersion: Symbol('fastify.RequestAcceptVersion'),
|
|
34
33
|
kRequestCacheValidateFns: Symbol('fastify.request.cache.validateFns'),
|
|
34
|
+
kRequestContentType: Symbol('fastify.request.contentType'),
|
|
35
35
|
kRequestOriginalUrl: Symbol('fastify.request.originalUrl'),
|
|
36
36
|
kRequestSignal: Symbol('fastify.request.signal'),
|
|
37
37
|
kHandlerTimeout: Symbol('fastify.handlerTimeout'),
|
|
@@ -65,7 +65,9 @@ const keys = {
|
|
|
65
65
|
kChildLoggerFactory: Symbol('fastify.childLoggerFactory'),
|
|
66
66
|
kHasBeenDecorated: Symbol('fastify.hasBeenDecorated'),
|
|
67
67
|
kKeepAliveConnections: Symbol('fastify.keepAliveConnections'),
|
|
68
|
-
kRouteByFastify: Symbol('fastify.routeByFastify')
|
|
68
|
+
kRouteByFastify: Symbol('fastify.routeByFastify'),
|
|
69
|
+
kLogController: Symbol('fastify.logController'),
|
|
70
|
+
kDiagnosticsStore: Symbol('fastify.diagnosticsStore')
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
module.exports = keys
|
package/lib/validation.js
CHANGED
|
@@ -146,6 +146,15 @@ function validateParam (validatorFunction, request, paramName) {
|
|
|
146
146
|
function validate (context, request, execution) {
|
|
147
147
|
const runExecution = execution === undefined
|
|
148
148
|
|
|
149
|
+
if (runExecution &&
|
|
150
|
+
context[paramsSchema] === undefined &&
|
|
151
|
+
context[bodySchema] === undefined &&
|
|
152
|
+
context[querystringSchema] === undefined &&
|
|
153
|
+
context[headersSchema] === undefined) {
|
|
154
|
+
// the route has no validation schemas
|
|
155
|
+
return false
|
|
156
|
+
}
|
|
157
|
+
|
|
149
158
|
if (runExecution || !execution.skipParams) {
|
|
150
159
|
const params = validateParam(context[paramsSchema], request, 'params')
|
|
151
160
|
if (params) {
|
|
@@ -162,9 +171,7 @@ function validate (context, request, execution) {
|
|
|
162
171
|
if (typeof context[bodySchema] === 'function') {
|
|
163
172
|
validatorFunction = context[bodySchema]
|
|
164
173
|
} else if (context[bodySchema]) {
|
|
165
|
-
|
|
166
|
-
const contentType = getEssenceMediaType(request.headers['content-type'])
|
|
167
|
-
const contentSchema = context[bodySchema][contentType]
|
|
174
|
+
const contentSchema = context[bodySchema][request.mediaType]
|
|
168
175
|
if (contentSchema) {
|
|
169
176
|
validatorFunction = contentSchema
|
|
170
177
|
}
|
|
@@ -262,16 +269,6 @@ function wrapValidationError (result, dataVar, schemaErrorFormatter) {
|
|
|
262
269
|
return error
|
|
263
270
|
}
|
|
264
271
|
|
|
265
|
-
/**
|
|
266
|
-
* simple function to retrieve the essence media type
|
|
267
|
-
* @param {string} header
|
|
268
|
-
* @returns {string} Mimetype string.
|
|
269
|
-
*/
|
|
270
|
-
function getEssenceMediaType (header) {
|
|
271
|
-
if (!header) return ''
|
|
272
|
-
return header.trimStart().split(/[ ;]/, 1)[0].trim().toLowerCase()
|
|
273
|
-
}
|
|
274
|
-
|
|
275
272
|
module.exports = {
|
|
276
273
|
symbols: { bodySchema, querystringSchema, responseSchema, paramsSchema, headersSchema },
|
|
277
274
|
compileSchemasForValidation,
|
package/lib/warnings.js
CHANGED
|
@@ -7,10 +7,12 @@ const { createWarning } = require('process-warning')
|
|
|
7
7
|
* - FSTWRN001
|
|
8
8
|
* - FSTSEC001
|
|
9
9
|
* - FSTDEP022
|
|
10
|
+
* - FSTDEP023
|
|
11
|
+
* - FSTDEP024
|
|
10
12
|
*
|
|
11
|
-
* Deprecation Codes FSTDEP001 - FSTDEP021 were used by v4 and MUST NOT
|
|
13
|
+
* Deprecation Codes FSTDEP001 - FSTDEP021 were used by v4 and MUST NOT be reused.
|
|
12
14
|
* - FSTDEP022 is used by v5 and MUST NOT be reused.
|
|
13
|
-
* Warning Codes FSTWRN001 - FSTWRN002 were used by v4 and MUST NOT
|
|
15
|
+
* Warning Codes FSTWRN001 - FSTWRN002 were used by v4 and MUST NOT be reused.
|
|
14
16
|
*/
|
|
15
17
|
|
|
16
18
|
const FSTWRN001 = createWarning({
|
|
@@ -30,7 +32,7 @@ const FSTWRN003 = createWarning({
|
|
|
30
32
|
const FSTWRN004 = createWarning({
|
|
31
33
|
name: 'FastifyWarning',
|
|
32
34
|
code: 'FSTWRN004',
|
|
33
|
-
message: 'It seems that you are overriding an errorHandler in the same scope, which can lead to subtle bugs.',
|
|
35
|
+
message: 'It seems that you are overriding an errorHandler in the same scope, which can lead to subtle bugs. To disable this behavior, set \'allowErrorHandlerOverride\' to false. For more information, visit: https://fastify.dev/docs/latest/Reference/Server/#allowerrorhandleroverride',
|
|
34
36
|
unlimited: true
|
|
35
37
|
})
|
|
36
38
|
|
|
@@ -48,10 +50,26 @@ const FSTDEP022 = createWarning({
|
|
|
48
50
|
unlimited: true
|
|
49
51
|
})
|
|
50
52
|
|
|
53
|
+
const FSTDEP023 = createWarning({
|
|
54
|
+
name: 'FastifyDeprecation',
|
|
55
|
+
code: 'FSTDEP023',
|
|
56
|
+
message: 'disableRequestLogging option is deprecated. Use the logController option with disableRequestLogging or isLogDisabled override instead. The disableRequestLogging top-level option will be removed in `fastify@6`.',
|
|
57
|
+
unlimited: true
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
const FSTDEP024 = createWarning({
|
|
61
|
+
name: 'FastifyDeprecation',
|
|
62
|
+
code: 'FSTDEP024',
|
|
63
|
+
message: 'requestIdLogLabel option is deprecated. Use the logController option with requestIdLogLabel instead. The requestIdLogLabel top-level option will be removed in `fastify@6`.',
|
|
64
|
+
unlimited: true
|
|
65
|
+
})
|
|
66
|
+
|
|
51
67
|
module.exports = {
|
|
52
68
|
FSTWRN001,
|
|
53
69
|
FSTWRN003,
|
|
54
70
|
FSTWRN004,
|
|
55
71
|
FSTSEC001,
|
|
56
|
-
FSTDEP022
|
|
72
|
+
FSTDEP022,
|
|
73
|
+
FSTDEP023,
|
|
74
|
+
FSTDEP024
|
|
57
75
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fastify",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.10.0",
|
|
4
4
|
"description": "Fast and low overhead web framework, for Node.js",
|
|
5
5
|
"main": "fastify.js",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -19,11 +19,11 @@
|
|
|
19
19
|
"lint:markdown": "markdownlint-cli2",
|
|
20
20
|
"lint:eslint": "eslint",
|
|
21
21
|
"prepublishOnly": "cross-env PREPUBLISH=true borp --reporter=@jsumners/line-reporter && npm run test:validator:integrity && npm run build:sync-version",
|
|
22
|
-
"test": "npm run lint && npm run unit && npm run test:
|
|
23
|
-
"test:ci": "npm run unit && npm run test:
|
|
24
|
-
"test:report": "npm run lint && npm run unit:report && npm run test:
|
|
22
|
+
"test": "npm run lint && npm run unit && npm run test:types",
|
|
23
|
+
"test:ci": "npm run unit && npm run test:types",
|
|
24
|
+
"test:report": "npm run lint && npm run unit:report && npm run test:types",
|
|
25
25
|
"test:validator:integrity": "npm run build:validation && git diff --quiet --ignore-all-space --ignore-blank-lines --ignore-cr-at-eol lib/error-serializer.js && git diff --quiet --ignore-all-space --ignore-blank-lines --ignore-cr-at-eol lib/config-validator.js",
|
|
26
|
-
"test:
|
|
26
|
+
"test:types": "tstyche",
|
|
27
27
|
"test:watch": "npm run unit -- --watch --coverage-report=none --reporter=terse",
|
|
28
28
|
"unit": "borp",
|
|
29
29
|
"unit:report": "c8 --reporter html borp --reporter=@jsumners/line-reporter",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"contributors": [
|
|
46
46
|
{
|
|
47
47
|
"name": "Tomas Della Vedova",
|
|
48
|
-
"url": "
|
|
48
|
+
"url": "https://delvedor.dev",
|
|
49
49
|
"author": true
|
|
50
50
|
},
|
|
51
51
|
{
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
},
|
|
60
60
|
{
|
|
61
61
|
"name": "Dustin Deus",
|
|
62
|
-
"url": "
|
|
62
|
+
"url": "https://starptech.com",
|
|
63
63
|
"email": "deusdustin@gmail.com"
|
|
64
64
|
},
|
|
65
65
|
{
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
},
|
|
75
75
|
{
|
|
76
76
|
"name": "Trivikram Kamat",
|
|
77
|
-
"url": "
|
|
77
|
+
"url": "https://trivikr.github.io",
|
|
78
78
|
"email": "trivikr.dev@gmail.com"
|
|
79
79
|
},
|
|
80
80
|
{
|
|
@@ -170,7 +170,7 @@
|
|
|
170
170
|
"@sinonjs/fake-timers": "^11.2.2",
|
|
171
171
|
"@stylistic/eslint-plugin": "^5.1.0",
|
|
172
172
|
"@stylistic/eslint-plugin-js": "^4.1.0",
|
|
173
|
-
"@types/node": "^
|
|
173
|
+
"@types/node": "^26.0.1",
|
|
174
174
|
"ajv": "^8.12.0",
|
|
175
175
|
"ajv-errors": "^3.0.0",
|
|
176
176
|
"ajv-formats": "^3.0.1",
|
|
@@ -179,11 +179,12 @@
|
|
|
179
179
|
"autocannon": "^8.0.0",
|
|
180
180
|
"borp": "^1.0.0",
|
|
181
181
|
"branch-comparer": "^1.1.0",
|
|
182
|
-
"concurrently": "^
|
|
182
|
+
"concurrently": "^10.0.0",
|
|
183
183
|
"cross-env": "^10.0.0",
|
|
184
184
|
"eslint": "^9.0.0",
|
|
185
185
|
"fast-json-body": "^1.1.0",
|
|
186
|
-
"fastify-plugin": "^
|
|
186
|
+
"fastify-plugin": "^6.0.0",
|
|
187
|
+
"fastify-tsconfig": "^3.0.0",
|
|
187
188
|
"fluent-json-schema": "^6.0.0",
|
|
188
189
|
"h2url": "^0.2.0",
|
|
189
190
|
"http-errors": "^2.0.0",
|
|
@@ -195,7 +196,7 @@
|
|
|
195
196
|
"node-forge": "^1.3.1",
|
|
196
197
|
"proxyquire": "^2.1.3",
|
|
197
198
|
"split2": "^4.2.0",
|
|
198
|
-
"
|
|
199
|
+
"tstyche": "^7.0.0",
|
|
199
200
|
"typebox": "^1.0.81",
|
|
200
201
|
"typescript": "~6.0.2",
|
|
201
202
|
"undici": "^7.11.0",
|
|
@@ -209,8 +210,8 @@
|
|
|
209
210
|
"@fastify/proxy-addr": "^5.0.0",
|
|
210
211
|
"abstract-logging": "^2.0.1",
|
|
211
212
|
"avvio": "^9.0.0",
|
|
212
|
-
"fast-json-stringify": "^
|
|
213
|
-
"find-my-way": "^9.
|
|
213
|
+
"fast-json-stringify": "^7.0.0",
|
|
214
|
+
"find-my-way": "^9.6.0",
|
|
214
215
|
"light-my-request": "^6.0.0",
|
|
215
216
|
"pino": "^9.14.0 || ^10.1.0",
|
|
216
217
|
"process-warning": "^5.0.0",
|
|
@@ -218,8 +219,5 @@
|
|
|
218
219
|
"secure-json-parse": "^4.0.0",
|
|
219
220
|
"semver": "^7.6.0",
|
|
220
221
|
"toad-cache": "^3.7.0"
|
|
221
|
-
},
|
|
222
|
-
"tsd": {
|
|
223
|
-
"directory": "test/types"
|
|
224
222
|
}
|
|
225
223
|
}
|
|
@@ -41,8 +41,7 @@ test('Should return 503 while closing - pipelining', async t => {
|
|
|
41
41
|
})
|
|
42
42
|
|
|
43
43
|
test('Should close the socket abruptly - pipelining - return503OnClosing: false', async t => {
|
|
44
|
-
//
|
|
45
|
-
// therefore our socket will be closed
|
|
44
|
+
// Node.js will always invoke server.closeIdleConnections() therefore our socket will be closed
|
|
46
45
|
const fastify = Fastify({
|
|
47
46
|
return503OnClosing: false,
|
|
48
47
|
forceCloseConnections: false
|
|
@@ -179,3 +179,23 @@ describe('ContentType class', () => {
|
|
|
179
179
|
)
|
|
180
180
|
})
|
|
181
181
|
})
|
|
182
|
+
|
|
183
|
+
describe('ContentType class cache', () => {
|
|
184
|
+
test('allow access cache', (t) => {
|
|
185
|
+
const contentType1 = ContentType.from('application/json')
|
|
186
|
+
const contentType2 = ContentType.cache.get('application/json')
|
|
187
|
+
t.assert.equal(contentType1, contentType2)
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
test('returns same instance for the same content type string', (t) => {
|
|
191
|
+
const contentType1 = ContentType.from('application/json')
|
|
192
|
+
const contentType2 = ContentType.from('application/json')
|
|
193
|
+
t.assert.equal(contentType1, contentType2)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
test('returns different instances for different content type strings', (t) => {
|
|
197
|
+
const contentType1 = ContentType.from('application/json')
|
|
198
|
+
const contentType2 = ContentType.from('text/plain')
|
|
199
|
+
t.assert.notEqual(contentType1, contentType2)
|
|
200
|
+
})
|
|
201
|
+
})
|
|
@@ -63,6 +63,44 @@ async function setup () {
|
|
|
63
63
|
)
|
|
64
64
|
})
|
|
65
65
|
|
|
66
|
+
test('Should not make an extra closeIdleConnections call for native servers', async t => {
|
|
67
|
+
const fastify = Fastify({
|
|
68
|
+
forceCloseConnections: 'idle'
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
await fastify.listen({ port: 0 })
|
|
72
|
+
|
|
73
|
+
let called = 0
|
|
74
|
+
fastify.server.closeIdleConnections = function () {
|
|
75
|
+
called++
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
await fastify.close()
|
|
79
|
+
|
|
80
|
+
t.assert.strictEqual(called, 1)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('Should preserve the extra closeIdleConnections call for custom servers', async t => {
|
|
84
|
+
let called = 0
|
|
85
|
+
const fastify = Fastify({
|
|
86
|
+
forceCloseConnections: 'idle',
|
|
87
|
+
serverFactory (handler) {
|
|
88
|
+
const server = http.createServer(handler)
|
|
89
|
+
const originalCloseIdleConnections = server.closeIdleConnections.bind(server)
|
|
90
|
+
server.closeIdleConnections = function () {
|
|
91
|
+
called++
|
|
92
|
+
return originalCloseIdleConnections()
|
|
93
|
+
}
|
|
94
|
+
return server
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
await fastify.listen({ port: 0 })
|
|
99
|
+
await fastify.close()
|
|
100
|
+
|
|
101
|
+
t.assert.strictEqual(called, 2)
|
|
102
|
+
})
|
|
103
|
+
|
|
66
104
|
test('Should accept user defined serverFactory and ignore secondary server creation', async t => {
|
|
67
105
|
const server = http.createServer(() => { })
|
|
68
106
|
t.after(() => new Promise(resolve => server.close(resolve)))
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { test } = require('node:test')
|
|
4
|
+
const Fastify = require('..')
|
|
5
|
+
|
|
6
|
+
test('hasRequestDecorator returns true for built-in constructor-assigned request properties', t => {
|
|
7
|
+
t.plan(6)
|
|
8
|
+
const fastify = Fastify()
|
|
9
|
+
t.assert.equal(fastify.hasRequestDecorator('id'), true)
|
|
10
|
+
t.assert.equal(fastify.hasRequestDecorator('params'), true)
|
|
11
|
+
t.assert.equal(fastify.hasRequestDecorator('raw'), true)
|
|
12
|
+
t.assert.equal(fastify.hasRequestDecorator('query'), true)
|
|
13
|
+
t.assert.equal(fastify.hasRequestDecorator('log'), true)
|
|
14
|
+
t.assert.equal(fastify.hasRequestDecorator('body'), true)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test('hasReplyDecorator returns true for built-in constructor-assigned reply properties', t => {
|
|
18
|
+
t.plan(3)
|
|
19
|
+
const fastify = Fastify()
|
|
20
|
+
t.assert.equal(fastify.hasReplyDecorator('raw'), true)
|
|
21
|
+
t.assert.equal(fastify.hasReplyDecorator('request'), true)
|
|
22
|
+
t.assert.equal(fastify.hasReplyDecorator('log'), true)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('decorateRequest throws FST_ERR_DEC_ALREADY_PRESENT for built-in request properties', t => {
|
|
26
|
+
t.plan(4)
|
|
27
|
+
const fastify = Fastify()
|
|
28
|
+
for (const name of ['id', 'params', 'raw', 'body']) {
|
|
29
|
+
t.assert.throws(
|
|
30
|
+
() => fastify.decorateRequest(name, null),
|
|
31
|
+
(err) => err.code === 'FST_ERR_DEC_ALREADY_PRESENT'
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('decorateReply throws FST_ERR_DEC_ALREADY_PRESENT for built-in reply properties', t => {
|
|
37
|
+
t.plan(2)
|
|
38
|
+
const fastify = Fastify()
|
|
39
|
+
for (const name of ['raw', 'request']) {
|
|
40
|
+
t.assert.throws(
|
|
41
|
+
() => fastify.decorateReply(name, null),
|
|
42
|
+
(err) => err.code === 'FST_ERR_DEC_ALREADY_PRESENT'
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('hasRequestDecorator returns false for unknown properties', t => {
|
|
48
|
+
t.plan(1)
|
|
49
|
+
const fastify = Fastify()
|
|
50
|
+
t.assert.equal(fastify.hasRequestDecorator('nonExistent'), false)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('decorateRequest still works for non-built-in names', (t, done) => {
|
|
54
|
+
t.plan(2)
|
|
55
|
+
const fastify = Fastify()
|
|
56
|
+
fastify.decorateRequest('myExtra', null)
|
|
57
|
+
t.assert.equal(fastify.hasRequestDecorator('myExtra'), true)
|
|
58
|
+
fastify.get('/', async (req) => req.myExtra)
|
|
59
|
+
fastify.ready((err) => {
|
|
60
|
+
t.assert.ifError(err)
|
|
61
|
+
done()
|
|
62
|
+
})
|
|
63
|
+
})
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { test } = require('node:test')
|
|
4
|
+
const diagnostics = require('node:diagnostics_channel')
|
|
5
|
+
const Fastify = require('../..')
|
|
6
|
+
const Request = require('../../lib/request')
|
|
7
|
+
const Reply = require('../../lib/reply')
|
|
8
|
+
|
|
9
|
+
test('diagnostics channel tracks async operations in async error handlers', async t => {
|
|
10
|
+
t.plan(17)
|
|
11
|
+
let callOrder = 0
|
|
12
|
+
let firstEncounteredMessage
|
|
13
|
+
let asyncStartFired = false
|
|
14
|
+
let asyncEndFired = false
|
|
15
|
+
|
|
16
|
+
const fastify = Fastify()
|
|
17
|
+
|
|
18
|
+
function subscribe (name, handler) {
|
|
19
|
+
const wrapped = (msg) => {
|
|
20
|
+
if (msg.request.server === fastify) handler(msg)
|
|
21
|
+
}
|
|
22
|
+
diagnostics.subscribe(name, wrapped)
|
|
23
|
+
t.after(() => diagnostics.unsubscribe(name, wrapped))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
subscribe('tracing:fastify.request.handler:start', (msg) => {
|
|
27
|
+
t.assert.strictEqual(callOrder++, 0)
|
|
28
|
+
firstEncounteredMessage = msg
|
|
29
|
+
t.assert.ok(msg.request instanceof Request)
|
|
30
|
+
t.assert.ok(msg.reply instanceof Reply)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
subscribe('tracing:fastify.request.handler:error', (msg) => {
|
|
34
|
+
t.assert.strictEqual(callOrder++, 1)
|
|
35
|
+
t.assert.ok(msg.error instanceof Error)
|
|
36
|
+
t.assert.strictEqual(msg.error.message, 'handler error')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
subscribe('tracing:fastify.request.handler:end', (msg) => {
|
|
40
|
+
t.assert.strictEqual(callOrder++, 2)
|
|
41
|
+
t.assert.strictEqual(msg, firstEncounteredMessage)
|
|
42
|
+
t.assert.strictEqual(msg.async, true)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
subscribe('tracing:fastify.request.handler:asyncStart', (msg) => {
|
|
46
|
+
t.assert.strictEqual(callOrder++, 3)
|
|
47
|
+
t.assert.strictEqual(msg, firstEncounteredMessage)
|
|
48
|
+
asyncStartFired = true
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
subscribe('tracing:fastify.request.handler:asyncEnd', (msg) => {
|
|
52
|
+
t.assert.strictEqual(callOrder++, 4)
|
|
53
|
+
t.assert.strictEqual(msg, firstEncounteredMessage)
|
|
54
|
+
asyncEndFired = true
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
fastify.setErrorHandler(async (error, request, reply) => {
|
|
58
|
+
await new Promise(resolve => setImmediate(resolve))
|
|
59
|
+
reply.status(503).send({ error: error.message })
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
fastify.get('/', () => {
|
|
63
|
+
throw new Error('handler error')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const fastifyServer = await fastify.listen({ port: 0 })
|
|
67
|
+
t.after(() => fastify.close())
|
|
68
|
+
|
|
69
|
+
const response = await fetch(fastifyServer)
|
|
70
|
+
t.assert.ok(!response.ok)
|
|
71
|
+
t.assert.strictEqual(response.status, 503)
|
|
72
|
+
t.assert.ok(asyncStartFired, 'asyncStart should fire for async error handler')
|
|
73
|
+
t.assert.ok(asyncEndFired, 'asyncEnd should fire for async error handler')
|
|
74
|
+
})
|
package/test/genReqId.test.js
CHANGED
|
@@ -85,6 +85,30 @@ test('Should handle properly requestIdHeader option', t => {
|
|
|
85
85
|
t.assert.strictEqual(Fastify({ requestIdHeader: 'x-request-id' }).initialConfig.requestIdHeader, 'x-request-id')
|
|
86
86
|
})
|
|
87
87
|
|
|
88
|
+
test('Should expose the genReqId function via the getter', (t, done) => {
|
|
89
|
+
t.plan(5)
|
|
90
|
+
|
|
91
|
+
const fastify = Fastify()
|
|
92
|
+
t.assert.strictEqual(typeof fastify.genReqId, 'function')
|
|
93
|
+
t.assert.strictEqual(typeof fastify.genReqId({ headers: {} }), 'string')
|
|
94
|
+
|
|
95
|
+
const custom = function (req) {
|
|
96
|
+
return 'custom'
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
fastify.register(function (instance, opts, next) {
|
|
100
|
+
instance.setGenReqId(custom)
|
|
101
|
+
t.assert.strictEqual(instance.genReqId(), 'custom')
|
|
102
|
+
next()
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
fastify.ready(err => {
|
|
106
|
+
t.assert.ifError(err)
|
|
107
|
+
t.assert.notStrictEqual(fastify.genReqId, custom)
|
|
108
|
+
done()
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
88
112
|
test('Should accept option to set genReqId with setGenReqId option', (t, done) => {
|
|
89
113
|
t.plan(9)
|
|
90
114
|
|
package/test/hooks.test.js
CHANGED
|
@@ -12,6 +12,7 @@ const proxyquire = require('proxyquire')
|
|
|
12
12
|
const { connect } = require('node:net')
|
|
13
13
|
const { sleep } = require('./helper')
|
|
14
14
|
const { waitForCb } = require('./toolkit.js')
|
|
15
|
+
const { fetch } = require('undici')
|
|
15
16
|
|
|
16
17
|
process.removeAllListeners('warning')
|
|
17
18
|
|
|
@@ -3277,6 +3278,28 @@ test('onTimeout should be triggered and socket _meta is set', async t => {
|
|
|
3277
3278
|
}
|
|
3278
3279
|
})
|
|
3279
3280
|
|
|
3281
|
+
test('socket._meta is cleared after response to prevent keep-alive leaks', async t => {
|
|
3282
|
+
t.plan(3)
|
|
3283
|
+
const fastify = Fastify({ connectionTimeout: 500 })
|
|
3284
|
+
t.after(() => { fastify.close() })
|
|
3285
|
+
|
|
3286
|
+
fastify.addHook('onTimeout', function (req, res, done) { done() })
|
|
3287
|
+
|
|
3288
|
+
fastify.addHook('onResponse', function (req, reply, done) {
|
|
3289
|
+
t.assert.strictEqual(req.raw.socket._meta, null, 'socket._meta must be null after response')
|
|
3290
|
+
done()
|
|
3291
|
+
})
|
|
3292
|
+
|
|
3293
|
+
fastify.get('/', async (req, reply) => {
|
|
3294
|
+
return { hello: 'world' }
|
|
3295
|
+
})
|
|
3296
|
+
|
|
3297
|
+
const address = await fastify.listen({ port: 0 })
|
|
3298
|
+
const result = await fetch(address)
|
|
3299
|
+
t.assert.ok(result.ok)
|
|
3300
|
+
t.assert.strictEqual(result.status, 200)
|
|
3301
|
+
})
|
|
3302
|
+
|
|
3280
3303
|
test('registering invalid hooks should throw an error', async t => {
|
|
3281
3304
|
t.plan(3)
|
|
3282
3305
|
|
|
@@ -3576,3 +3599,74 @@ test('onRequestAbort should handle async errors / 2', (t, testDone) => {
|
|
|
3576
3599
|
sleep(500).then(() => socket.destroy())
|
|
3577
3600
|
})
|
|
3578
3601
|
})
|
|
3602
|
+
|
|
3603
|
+
test('socket timeout listener is removed when socket._meta is cleared after response', async t => {
|
|
3604
|
+
t.plan(4)
|
|
3605
|
+
|
|
3606
|
+
const fastify = Fastify({ connectionTimeout: 200 })
|
|
3607
|
+
t.after(() => fastify.close())
|
|
3608
|
+
|
|
3609
|
+
fastify.addHook('onTimeout', function (req, reply, done) { done() })
|
|
3610
|
+
|
|
3611
|
+
fastify.addHook('onResponse', function (req, reply, done) {
|
|
3612
|
+
const socket = req.raw.socket
|
|
3613
|
+
t.assert.strictEqual(socket._meta, null, 'socket._meta must be null after response')
|
|
3614
|
+
t.assert.strictEqual(socket.listeners('timeout').some(listener => listener.name === 'handleTimeout'), false, 'Fastify timeout listener must be removed after response')
|
|
3615
|
+
done()
|
|
3616
|
+
})
|
|
3617
|
+
|
|
3618
|
+
fastify.get('/', async () => ({ ok: true }))
|
|
3619
|
+
|
|
3620
|
+
const address = await fastify.listen({ port: 0 })
|
|
3621
|
+
const result = await fetch(address)
|
|
3622
|
+
t.assert.ok(result.ok)
|
|
3623
|
+
t.assert.strictEqual(result.status, 200)
|
|
3624
|
+
})
|
|
3625
|
+
|
|
3626
|
+
test('socket._meta and timeout listener are cleared on reply.hijack() when onTimeout is registered', async t => {
|
|
3627
|
+
// reply.hijack() opts out of Fastify's response lifecycle so onResFinished
|
|
3628
|
+
// never fires. Before this fix, socket._meta was left pointing at the
|
|
3629
|
+
// request/reply objects for the full keep-alive socket lifetime when
|
|
3630
|
+
// an onTimeout hook was registered.
|
|
3631
|
+
t.plan(3)
|
|
3632
|
+
|
|
3633
|
+
const net = require('node:net')
|
|
3634
|
+
const fastify = Fastify({ connectionTimeout: 300 })
|
|
3635
|
+
t.after(() => fastify.close())
|
|
3636
|
+
|
|
3637
|
+
fastify.addHook('onTimeout', function (req, reply, done) { done() })
|
|
3638
|
+
|
|
3639
|
+
let metaAfterHijack = 'not-set'
|
|
3640
|
+
let hasFastifyTimeoutListenerAfterHijack = true
|
|
3641
|
+
fastify.get('/', async (req, reply) => {
|
|
3642
|
+
reply.hijack()
|
|
3643
|
+
// _meta must be cleared synchronously inside hijack()
|
|
3644
|
+
metaAfterHijack = req.raw.socket._meta
|
|
3645
|
+
hasFastifyTimeoutListenerAfterHijack = req.raw.socket.listeners('timeout').some(listener => listener.name === 'handleTimeout')
|
|
3646
|
+
// Write a minimal valid HTTP/1.1 response and close
|
|
3647
|
+
reply.raw.write('HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok')
|
|
3648
|
+
reply.raw.end()
|
|
3649
|
+
return reply
|
|
3650
|
+
})
|
|
3651
|
+
|
|
3652
|
+
await fastify.listen({ port: 0 })
|
|
3653
|
+
|
|
3654
|
+
// Use raw TCP so we are not affected by fetch() rejecting a closed socket.
|
|
3655
|
+
// Extract host/port from the server address object to handle IPv6 (::1) on Windows.
|
|
3656
|
+
const { port, address: host } = fastify.server.address()
|
|
3657
|
+
await new Promise((resolve, reject) => {
|
|
3658
|
+
const socket = net.connect(port, host, () => {
|
|
3659
|
+
socket.write('GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n')
|
|
3660
|
+
})
|
|
3661
|
+
socket.on('data', () => {})
|
|
3662
|
+
socket.on('close', resolve)
|
|
3663
|
+
socket.on('error', reject)
|
|
3664
|
+
})
|
|
3665
|
+
|
|
3666
|
+
t.assert.ok(true, 'no crash during hijacked request with onTimeout registered')
|
|
3667
|
+
t.assert.ok(
|
|
3668
|
+
metaAfterHijack == null,
|
|
3669
|
+
`socket._meta must be null or undefined after reply.hijack() — got: ${JSON.stringify(metaAfterHijack)}`
|
|
3670
|
+
)
|
|
3671
|
+
t.assert.strictEqual(hasFastifyTimeoutListenerAfterHijack, false, 'Fastify timeout listener must be removed after reply.hijack()')
|
|
3672
|
+
})
|