fastify 5.6.2 → 5.7.1
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 +34 -33
- package/SECURITY.md +45 -7
- package/SPONSORS.md +1 -1
- package/build/build-validation.js +1 -2
- package/build/sync-version.js +0 -1
- package/docs/Guides/Detecting-When-Clients-Abort.md +1 -1
- package/docs/Guides/Ecosystem.md +12 -15
- package/docs/Guides/Migration-Guide-V4.md +2 -1
- package/docs/Guides/Migration-Guide-V5.md +9 -0
- package/docs/Guides/Serverless.md +25 -7
- package/docs/Guides/Testing.md +2 -2
- package/docs/Reference/Decorators.md +4 -3
- package/docs/Reference/Encapsulation.md +2 -2
- package/docs/Reference/Logging.md +4 -0
- package/docs/Reference/Plugins.md +2 -2
- package/docs/Reference/Principles.md +2 -2
- package/docs/Reference/Reply.md +3 -2
- package/docs/Reference/Server.md +35 -4
- package/docs/Reference/TypeScript.md +2 -1
- package/docs/Reference/Validation-and-Serialization.md +7 -1
- package/examples/benchmark/webstream.js +27 -0
- package/fastify.d.ts +16 -21
- package/fastify.js +14 -9
- package/lib/config-validator.js +189 -223
- package/lib/error-handler.js +2 -5
- package/lib/error-status.js +14 -0
- package/lib/four-oh-four.js +2 -1
- package/lib/handle-request.js +6 -1
- package/lib/reply.js +53 -3
- package/lib/route.js +26 -12
- package/lib/schema-controller.js +2 -2
- package/lib/wrap-thenable.js +3 -0
- package/package.json +4 -4
- package/test/404s.test.js +69 -0
- package/test/diagnostics-channel/error-status.test.js +84 -0
- package/test/internals/schema-controller-perf.test.js +40 -0
- package/test/issue-4959.test.js +34 -9
- package/test/listen.1.test.js +9 -1
- package/test/logger/logging.test.js +38 -1
- package/test/router-options.test.js +169 -0
- package/test/server.test.js +4 -1
- package/test/types/fastify.test-d.ts +28 -7
- package/test/types/instance.test-d.ts +29 -21
- package/test/types/reply.test-d.ts +55 -4
- package/test/types/type-provider.test-d.ts +6 -6
- package/test/web-api.test.js +136 -0
- package/types/instance.d.ts +1 -1
- package/types/reply.d.ts +2 -2
- package/types/type-provider.d.ts +16 -0
- package/.vscode/settings.json +0 -22
- package/test/decorator-namespace.test._js_ +0 -30
package/lib/error-handler.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
const statusCodes = require('node:http').STATUS_CODES
|
|
4
4
|
const wrapThenable = require('./wrap-thenable.js')
|
|
5
|
+
const { setErrorStatusCode } = require('./error-status.js')
|
|
5
6
|
const {
|
|
6
7
|
kReplyHeaders,
|
|
7
8
|
kReplyNextErrorHandler,
|
|
8
9
|
kReplyIsRunningOnErrorHook,
|
|
9
|
-
kReplyHasStatusCode,
|
|
10
10
|
kRouteContext,
|
|
11
11
|
kDisableRequestLogging
|
|
12
12
|
} = require('./symbols.js')
|
|
@@ -81,10 +81,7 @@ function handleError (reply, error, cb) {
|
|
|
81
81
|
|
|
82
82
|
function defaultErrorHandler (error, request, reply) {
|
|
83
83
|
setErrorHeaders(error, reply)
|
|
84
|
-
|
|
85
|
-
const statusCode = error && (error.statusCode || error.status)
|
|
86
|
-
reply.code(statusCode >= 400 ? statusCode : 500)
|
|
87
|
-
}
|
|
84
|
+
setErrorStatusCode(reply, error)
|
|
88
85
|
if (reply.statusCode < 500) {
|
|
89
86
|
if (!reply.log[kDisableRequestLogging]) {
|
|
90
87
|
reply.log.info(
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
kReplyHasStatusCode
|
|
5
|
+
} = require('./symbols')
|
|
6
|
+
|
|
7
|
+
function setErrorStatusCode (reply, err) {
|
|
8
|
+
if (!reply[kReplyHasStatusCode] || reply.statusCode === 200) {
|
|
9
|
+
const statusCode = err && (err.statusCode || err.status)
|
|
10
|
+
reply.code(statusCode >= 400 ? statusCode : 500)
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = { setErrorStatusCode }
|
package/lib/four-oh-four.js
CHANGED
|
@@ -49,7 +49,8 @@ function fourOhFour (options) {
|
|
|
49
49
|
function basic404 (request, reply) {
|
|
50
50
|
const { url, method } = request.raw
|
|
51
51
|
const message = `Route ${method}:${url} not found`
|
|
52
|
-
|
|
52
|
+
const resolvedDisableRequestLogging = typeof disableRequestLogging === 'function' ? disableRequestLogging(request.raw) : disableRequestLogging
|
|
53
|
+
if (!resolvedDisableRequestLogging) {
|
|
53
54
|
request.log.info(message)
|
|
54
55
|
}
|
|
55
56
|
reply.code(404).send({
|
package/lib/handle-request.js
CHANGED
|
@@ -4,6 +4,7 @@ const diagnostics = require('node:diagnostics_channel')
|
|
|
4
4
|
const { validate: validateSchema } = require('./validation')
|
|
5
5
|
const { preValidationHookRunner, preHandlerHookRunner } = require('./hooks')
|
|
6
6
|
const wrapThenable = require('./wrap-thenable')
|
|
7
|
+
const { setErrorStatusCode } = require('./error-status')
|
|
7
8
|
const {
|
|
8
9
|
kReplyIsError,
|
|
9
10
|
kRouteContext,
|
|
@@ -143,11 +144,13 @@ function preHandlerCallbackInner (err, request, reply, store) {
|
|
|
143
144
|
try {
|
|
144
145
|
if (err != null) {
|
|
145
146
|
reply[kReplyIsError] = true
|
|
146
|
-
reply.send(err)
|
|
147
147
|
if (store) {
|
|
148
148
|
store.error = err
|
|
149
|
+
// Set status code before publishing so subscribers see the correct value
|
|
150
|
+
setErrorStatusCode(reply, err)
|
|
149
151
|
channels.error.publish(store)
|
|
150
152
|
}
|
|
153
|
+
reply.send(err)
|
|
151
154
|
return
|
|
152
155
|
}
|
|
153
156
|
|
|
@@ -158,6 +161,8 @@ function preHandlerCallbackInner (err, request, reply, store) {
|
|
|
158
161
|
} catch (err) {
|
|
159
162
|
if (store) {
|
|
160
163
|
store.error = err
|
|
164
|
+
// Set status code before publishing so subscribers see the correct value
|
|
165
|
+
setErrorStatusCode(reply, err)
|
|
161
166
|
channels.error.publish(store)
|
|
162
167
|
}
|
|
163
168
|
|
package/lib/reply.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const eos = require('node:stream').finished
|
|
4
|
-
const Readable = require('node:stream').Readable
|
|
5
4
|
|
|
6
5
|
const {
|
|
7
6
|
kFourOhFourContext,
|
|
@@ -685,8 +684,59 @@ function sendWebStream (payload, res, reply) {
|
|
|
685
684
|
if (payload.locked) {
|
|
686
685
|
throw new FST_ERR_REP_READABLE_STREAM_LOCKED()
|
|
687
686
|
}
|
|
688
|
-
|
|
689
|
-
|
|
687
|
+
|
|
688
|
+
let sourceOpen = true
|
|
689
|
+
let errorLogged = false
|
|
690
|
+
const reader = payload.getReader()
|
|
691
|
+
|
|
692
|
+
eos(res, function (err) {
|
|
693
|
+
if (sourceOpen) {
|
|
694
|
+
if (err != null && res.headersSent && !errorLogged) {
|
|
695
|
+
errorLogged = true
|
|
696
|
+
logStreamError(reply.log, err, res)
|
|
697
|
+
}
|
|
698
|
+
reader.cancel().catch(noop)
|
|
699
|
+
}
|
|
700
|
+
})
|
|
701
|
+
|
|
702
|
+
if (!res.headersSent) {
|
|
703
|
+
for (const key in reply[kReplyHeaders]) {
|
|
704
|
+
res.setHeader(key, reply[kReplyHeaders][key])
|
|
705
|
+
}
|
|
706
|
+
} else {
|
|
707
|
+
reply.log.warn('response will send, but you shouldn\'t use res.writeHead in stream mode')
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function onRead (result) {
|
|
711
|
+
if (result.done) {
|
|
712
|
+
sourceOpen = false
|
|
713
|
+
sendTrailer(null, res, reply)
|
|
714
|
+
return
|
|
715
|
+
}
|
|
716
|
+
/* c8 ignore next 5 - race condition: eos handler typically fires first */
|
|
717
|
+
if (res.destroyed) {
|
|
718
|
+
sourceOpen = false
|
|
719
|
+
reader.cancel().catch(noop)
|
|
720
|
+
return
|
|
721
|
+
}
|
|
722
|
+
res.write(result.value)
|
|
723
|
+
reader.read().then(onRead, onReadError)
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function onReadError (err) {
|
|
727
|
+
sourceOpen = false
|
|
728
|
+
if (res.headersSent || reply.request.raw.aborted === true) {
|
|
729
|
+
if (!errorLogged) {
|
|
730
|
+
errorLogged = true
|
|
731
|
+
logStreamError(reply.log, err, reply)
|
|
732
|
+
}
|
|
733
|
+
res.destroy()
|
|
734
|
+
} else {
|
|
735
|
+
onErrorHook(reply, err)
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
reader.read().then(onRead, onReadError)
|
|
690
740
|
}
|
|
691
741
|
|
|
692
742
|
function sendStream (payload, res, reply) {
|
package/lib/route.js
CHANGED
|
@@ -29,8 +29,6 @@ const {
|
|
|
29
29
|
FST_ERR_HOOK_INVALID_ASYNC_HANDLER
|
|
30
30
|
} = require('./errors')
|
|
31
31
|
|
|
32
|
-
const { FSTDEP022 } = require('./warnings')
|
|
33
|
-
|
|
34
32
|
const {
|
|
35
33
|
kRoutePrefix,
|
|
36
34
|
kSupportedHTTPMethods,
|
|
@@ -53,6 +51,7 @@ const {
|
|
|
53
51
|
const { buildErrorHandler } = require('./error-handler')
|
|
54
52
|
const { createChildLogger } = require('./logger-factory.js')
|
|
55
53
|
const { getGenReqId } = require('./req-id-gen-factory.js')
|
|
54
|
+
const { FSTDEP022 } = require('./warnings')
|
|
56
55
|
|
|
57
56
|
const routerKeys = [
|
|
58
57
|
'allowUnsafeRegex',
|
|
@@ -69,7 +68,7 @@ const routerKeys = [
|
|
|
69
68
|
]
|
|
70
69
|
|
|
71
70
|
function buildRouting (options) {
|
|
72
|
-
const router = FindMyWay(options
|
|
71
|
+
const router = FindMyWay(options)
|
|
73
72
|
|
|
74
73
|
let avvio
|
|
75
74
|
let fourOhFour
|
|
@@ -78,6 +77,7 @@ function buildRouting (options) {
|
|
|
78
77
|
let setupResponseListeners
|
|
79
78
|
let throwIfAlreadyStarted
|
|
80
79
|
let disableRequestLogging
|
|
80
|
+
let disableRequestLoggingFn
|
|
81
81
|
let ignoreTrailingSlash
|
|
82
82
|
let ignoreDuplicateSlashes
|
|
83
83
|
let return503OnClosing
|
|
@@ -94,13 +94,17 @@ function buildRouting (options) {
|
|
|
94
94
|
setup (options, fastifyArgs) {
|
|
95
95
|
avvio = fastifyArgs.avvio
|
|
96
96
|
fourOhFour = fastifyArgs.fourOhFour
|
|
97
|
+
logger = options.logger
|
|
97
98
|
hasLogger = fastifyArgs.hasLogger
|
|
98
99
|
setupResponseListeners = fastifyArgs.setupResponseListeners
|
|
99
100
|
throwIfAlreadyStarted = fastifyArgs.throwIfAlreadyStarted
|
|
100
101
|
|
|
101
|
-
logger = options.logger
|
|
102
102
|
globalExposeHeadRoutes = options.exposeHeadRoutes
|
|
103
103
|
disableRequestLogging = options.disableRequestLogging
|
|
104
|
+
if (typeof disableRequestLogging === 'function') {
|
|
105
|
+
disableRequestLoggingFn = options.disableRequestLogging
|
|
106
|
+
}
|
|
107
|
+
|
|
104
108
|
ignoreTrailingSlash = options.routerOptions.ignoreTrailingSlash
|
|
105
109
|
ignoreDuplicateSlashes = options.routerOptions.ignoreDuplicateSlashes
|
|
106
110
|
return503OnClosing = Object.hasOwn(options, 'return503OnClosing') ? options.return503OnClosing : true
|
|
@@ -373,9 +377,8 @@ function buildRouting (options) {
|
|
|
373
377
|
context.logSerializers = opts.logSerializers
|
|
374
378
|
context.attachValidation = opts.attachValidation
|
|
375
379
|
context[kReplySerializerDefault] = this[kReplySerializerDefault]
|
|
376
|
-
context.schemaErrorFormatter =
|
|
377
|
-
this[kSchemaErrorFormatter] ||
|
|
378
|
-
context.schemaErrorFormatter
|
|
380
|
+
context.schemaErrorFormatter =
|
|
381
|
+
opts.schemaErrorFormatter || this[kSchemaErrorFormatter] || context.schemaErrorFormatter
|
|
379
382
|
|
|
380
383
|
// Run hooks and more
|
|
381
384
|
avvio.once('preReady', () => {
|
|
@@ -402,9 +405,11 @@ function buildRouting (options) {
|
|
|
402
405
|
context.schema = normalizeSchema(context.schema, this.initialConfig)
|
|
403
406
|
|
|
404
407
|
const schemaController = this[kSchemaController]
|
|
405
|
-
|
|
406
|
-
opts.schema.
|
|
407
|
-
|
|
408
|
+
const hasValidationSchema = opts.schema.body ||
|
|
409
|
+
opts.schema.headers ||
|
|
410
|
+
opts.schema.querystring ||
|
|
411
|
+
opts.schema.params
|
|
412
|
+
if (!opts.validatorCompiler && hasValidationSchema) {
|
|
408
413
|
schemaController.setupValidator(this[kOptions])
|
|
409
414
|
}
|
|
410
415
|
try {
|
|
@@ -455,7 +460,8 @@ function buildRouting (options) {
|
|
|
455
460
|
loggerOpts.serializers = context.logSerializers
|
|
456
461
|
}
|
|
457
462
|
const childLogger = createChildLogger(context, logger, req, id, loggerOpts)
|
|
458
|
-
|
|
463
|
+
// Set initial value; will be re-evaluated after FastifyRequest is constructed if it's a function
|
|
464
|
+
childLogger[kDisableRequestLogging] = disableRequestLoggingFn ? false : disableRequestLogging
|
|
459
465
|
|
|
460
466
|
if (closing === true) {
|
|
461
467
|
/* istanbul ignore next mac, windows */
|
|
@@ -499,7 +505,15 @@ function buildRouting (options) {
|
|
|
499
505
|
|
|
500
506
|
const request = new context.Request(id, params, req, query, childLogger, context)
|
|
501
507
|
const reply = new context.Reply(res, request, childLogger)
|
|
502
|
-
|
|
508
|
+
|
|
509
|
+
// Evaluate disableRequestLogging after FastifyRequest is constructed
|
|
510
|
+
// so the caller has access to decorations and customizations
|
|
511
|
+
const resolvedDisableRequestLogging = disableRequestLoggingFn
|
|
512
|
+
? disableRequestLoggingFn(request)
|
|
513
|
+
: disableRequestLogging
|
|
514
|
+
childLogger[kDisableRequestLogging] = resolvedDisableRequestLogging
|
|
515
|
+
|
|
516
|
+
if (resolvedDisableRequestLogging === false) {
|
|
503
517
|
childLogger.info({ req: request }, 'incoming request')
|
|
504
518
|
}
|
|
505
519
|
|
package/lib/schema-controller.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { buildSchemas } = require('./schemas')
|
|
4
|
-
const SerializerSelector = require('@fastify/fast-json-stringify-compiler')
|
|
5
|
-
const ValidatorSelector = require('@fastify/ajv-compiler')
|
|
6
4
|
|
|
7
5
|
/**
|
|
8
6
|
* Called at every fastify context that is being created.
|
|
@@ -21,9 +19,11 @@ function buildSchemaController (parentSchemaCtrl, opts) {
|
|
|
21
19
|
}, opts?.compilersFactory)
|
|
22
20
|
|
|
23
21
|
if (!compilersFactory.buildValidator) {
|
|
22
|
+
const ValidatorSelector = require('@fastify/ajv-compiler')
|
|
24
23
|
compilersFactory.buildValidator = ValidatorSelector()
|
|
25
24
|
}
|
|
26
25
|
if (!compilersFactory.buildSerializer) {
|
|
26
|
+
const SerializerSelector = require('@fastify/fast-json-stringify-compiler')
|
|
27
27
|
compilersFactory.buildSerializer = SerializerSelector()
|
|
28
28
|
}
|
|
29
29
|
|
package/lib/wrap-thenable.js
CHANGED
|
@@ -4,6 +4,7 @@ const {
|
|
|
4
4
|
kReplyIsError,
|
|
5
5
|
kReplyHijacked
|
|
6
6
|
} = require('./symbols')
|
|
7
|
+
const { setErrorStatusCode } = require('./error-status')
|
|
7
8
|
|
|
8
9
|
const diagnostics = require('node:diagnostics_channel')
|
|
9
10
|
const channels = diagnostics.tracingChannel('fastify.request.handler')
|
|
@@ -52,6 +53,8 @@ function wrapThenable (thenable, reply, store) {
|
|
|
52
53
|
}, function (err) {
|
|
53
54
|
if (store) {
|
|
54
55
|
store.error = err
|
|
56
|
+
// Set status code before publishing so subscribers see the correct value
|
|
57
|
+
setErrorStatusCode(reply, err)
|
|
55
58
|
channels.error.publish(store) // note that error happens before asyncStart
|
|
56
59
|
channels.asyncStart.publish(store)
|
|
57
60
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fastify",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.7.1",
|
|
4
4
|
"description": "Fast and low overhead web framework, for Node.js",
|
|
5
5
|
"main": "fastify.js",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -171,7 +171,7 @@
|
|
|
171
171
|
"@sinonjs/fake-timers": "^11.2.2",
|
|
172
172
|
"@stylistic/eslint-plugin": "^5.1.0",
|
|
173
173
|
"@stylistic/eslint-plugin-js": "^4.1.0",
|
|
174
|
-
"@types/node": "^
|
|
174
|
+
"@types/node": "^25.0.3",
|
|
175
175
|
"ajv": "^8.12.0",
|
|
176
176
|
"ajv-errors": "^3.0.0",
|
|
177
177
|
"ajv-formats": "^3.0.1",
|
|
@@ -191,7 +191,7 @@
|
|
|
191
191
|
"joi": "^18.0.1",
|
|
192
192
|
"json-schema-to-ts": "^3.0.1",
|
|
193
193
|
"JSONStream": "^1.3.5",
|
|
194
|
-
"markdownlint-cli2": "^0.
|
|
194
|
+
"markdownlint-cli2": "^0.20.0",
|
|
195
195
|
"neostandard": "^0.12.0",
|
|
196
196
|
"node-forge": "^1.3.1",
|
|
197
197
|
"proxyquire": "^2.1.3",
|
|
@@ -203,7 +203,7 @@
|
|
|
203
203
|
"yup": "^1.4.0"
|
|
204
204
|
},
|
|
205
205
|
"dependencies": {
|
|
206
|
-
"@fastify/ajv-compiler": "^4.0.
|
|
206
|
+
"@fastify/ajv-compiler": "^4.0.5",
|
|
207
207
|
"@fastify/error": "^4.0.0",
|
|
208
208
|
"@fastify/fast-json-stringify-compiler": "^5.0.0",
|
|
209
209
|
"@fastify/proxy-addr": "^5.0.0",
|
package/test/404s.test.js
CHANGED
|
@@ -1845,6 +1845,26 @@ test('400 in case of bad url (pre find-my-way v2.2.0 was a 404)', async t => {
|
|
|
1845
1845
|
done()
|
|
1846
1846
|
})
|
|
1847
1847
|
})
|
|
1848
|
+
|
|
1849
|
+
await t.test('Bad URL with special characters should be properly JSON escaped', (t, done) => {
|
|
1850
|
+
t.plan(3)
|
|
1851
|
+
const fastify = Fastify()
|
|
1852
|
+
fastify.get('/hello/:id', () => t.assert.fail('we should not be here'))
|
|
1853
|
+
fastify.inject({
|
|
1854
|
+
url: '/hello/%world%22test',
|
|
1855
|
+
method: 'GET'
|
|
1856
|
+
}, (err, response) => {
|
|
1857
|
+
t.assert.ifError(err)
|
|
1858
|
+
t.assert.strictEqual(response.statusCode, 400)
|
|
1859
|
+
t.assert.deepStrictEqual(JSON.parse(response.payload), {
|
|
1860
|
+
error: 'Bad Request',
|
|
1861
|
+
message: '\'/hello/%world%22test\' is not a valid url component',
|
|
1862
|
+
statusCode: 400,
|
|
1863
|
+
code: 'FST_ERR_BAD_URL'
|
|
1864
|
+
})
|
|
1865
|
+
done()
|
|
1866
|
+
})
|
|
1867
|
+
})
|
|
1848
1868
|
})
|
|
1849
1869
|
|
|
1850
1870
|
test('setNotFoundHandler should be chaining fastify instance', async t => {
|
|
@@ -1964,3 +1984,52 @@ test('hooks are applied to not found handlers /3', async t => {
|
|
|
1964
1984
|
const { statusCode } = await fastify.inject('/')
|
|
1965
1985
|
t.assert.strictEqual(statusCode, 401)
|
|
1966
1986
|
})
|
|
1987
|
+
|
|
1988
|
+
test('should honor disableRequestLogging function for 404', async t => {
|
|
1989
|
+
t.plan(3)
|
|
1990
|
+
|
|
1991
|
+
const Writable = require('node:stream').Writable
|
|
1992
|
+
|
|
1993
|
+
const logStream = new Writable()
|
|
1994
|
+
logStream.logs = []
|
|
1995
|
+
logStream._write = function (chunk, encoding, callback) {
|
|
1996
|
+
this.logs.push(JSON.parse(chunk.toString()))
|
|
1997
|
+
callback()
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
const fastify = Fastify({
|
|
2001
|
+
logger: {
|
|
2002
|
+
level: 'info',
|
|
2003
|
+
stream: logStream
|
|
2004
|
+
},
|
|
2005
|
+
disableRequestLogging: (req) => {
|
|
2006
|
+
// Disable logging for URLs containing 'silent'
|
|
2007
|
+
return req.url.includes('silent')
|
|
2008
|
+
}
|
|
2009
|
+
})
|
|
2010
|
+
|
|
2011
|
+
fastify.get('/', function (req, reply) {
|
|
2012
|
+
reply.send({ hello: 'world' })
|
|
2013
|
+
})
|
|
2014
|
+
|
|
2015
|
+
t.after(() => { fastify.close() })
|
|
2016
|
+
|
|
2017
|
+
// First request to a non-existent route (no 'silent' in URL) - should log
|
|
2018
|
+
const response1 = await fastify.inject({
|
|
2019
|
+
method: 'GET',
|
|
2020
|
+
url: '/not-found'
|
|
2021
|
+
})
|
|
2022
|
+
t.assert.strictEqual(response1.statusCode, 404)
|
|
2023
|
+
|
|
2024
|
+
// Second request to a non-existent route with 'silent' in URL - should not log
|
|
2025
|
+
const response2 = await fastify.inject({
|
|
2026
|
+
method: 'GET',
|
|
2027
|
+
url: '/silent-route'
|
|
2028
|
+
})
|
|
2029
|
+
t.assert.strictEqual(response2.statusCode, 404)
|
|
2030
|
+
|
|
2031
|
+
// Check logs: first request should have logged, second should not
|
|
2032
|
+
// We expect: incoming request, Route not found info, request completed (for first request only)
|
|
2033
|
+
const infoLogs = logStream.logs.filter(log => log.msg && log.msg.includes('Route GET:/not-found not found'))
|
|
2034
|
+
t.assert.strictEqual(infoLogs.length, 1, 'Should log 404 info only for non-silent route')
|
|
2035
|
+
})
|
|
@@ -5,6 +5,90 @@ const Fastify = require('../..')
|
|
|
5
5
|
const statusCodes = require('node:http').STATUS_CODES
|
|
6
6
|
const diagnostics = require('node:diagnostics_channel')
|
|
7
7
|
|
|
8
|
+
test('diagnostics channel error event should report correct status code', async (t) => {
|
|
9
|
+
t.plan(3)
|
|
10
|
+
const fastify = Fastify()
|
|
11
|
+
t.after(() => fastify.close())
|
|
12
|
+
|
|
13
|
+
let diagnosticsStatusCode
|
|
14
|
+
|
|
15
|
+
const channel = diagnostics.channel('tracing:fastify.request.handler:error')
|
|
16
|
+
const handler = (msg) => {
|
|
17
|
+
diagnosticsStatusCode = msg.reply.statusCode
|
|
18
|
+
}
|
|
19
|
+
channel.subscribe(handler)
|
|
20
|
+
t.after(() => channel.unsubscribe(handler))
|
|
21
|
+
|
|
22
|
+
fastify.get('/', async () => {
|
|
23
|
+
const err = new Error('test error')
|
|
24
|
+
err.statusCode = 503
|
|
25
|
+
throw err
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const res = await fastify.inject('/')
|
|
29
|
+
|
|
30
|
+
t.assert.strictEqual(res.statusCode, 503)
|
|
31
|
+
t.assert.strictEqual(diagnosticsStatusCode, 503, 'diagnostics channel should report correct status code')
|
|
32
|
+
t.assert.strictEqual(diagnosticsStatusCode, res.statusCode, 'diagnostics status should match response status')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('diagnostics channel error event should report 500 for errors without status', async (t) => {
|
|
36
|
+
t.plan(3)
|
|
37
|
+
const fastify = Fastify()
|
|
38
|
+
t.after(() => fastify.close())
|
|
39
|
+
|
|
40
|
+
let diagnosticsStatusCode
|
|
41
|
+
|
|
42
|
+
const channel = diagnostics.channel('tracing:fastify.request.handler:error')
|
|
43
|
+
const handler = (msg) => {
|
|
44
|
+
diagnosticsStatusCode = msg.reply.statusCode
|
|
45
|
+
}
|
|
46
|
+
channel.subscribe(handler)
|
|
47
|
+
t.after(() => channel.unsubscribe(handler))
|
|
48
|
+
|
|
49
|
+
fastify.get('/', async () => {
|
|
50
|
+
throw new Error('plain error without status')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const res = await fastify.inject('/')
|
|
54
|
+
|
|
55
|
+
t.assert.strictEqual(res.statusCode, 500)
|
|
56
|
+
t.assert.strictEqual(diagnosticsStatusCode, 500, 'diagnostics channel should report 500 for plain errors')
|
|
57
|
+
t.assert.strictEqual(diagnosticsStatusCode, res.statusCode, 'diagnostics status should match response status')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('diagnostics channel error event should report correct status with custom error handler', async (t) => {
|
|
61
|
+
t.plan(3)
|
|
62
|
+
const fastify = Fastify()
|
|
63
|
+
t.after(() => fastify.close())
|
|
64
|
+
|
|
65
|
+
let diagnosticsStatusCode
|
|
66
|
+
|
|
67
|
+
const channel = diagnostics.channel('tracing:fastify.request.handler:error')
|
|
68
|
+
const handler = (msg) => {
|
|
69
|
+
diagnosticsStatusCode = msg.reply.statusCode
|
|
70
|
+
}
|
|
71
|
+
channel.subscribe(handler)
|
|
72
|
+
t.after(() => channel.unsubscribe(handler))
|
|
73
|
+
|
|
74
|
+
fastify.setErrorHandler((error, request, reply) => {
|
|
75
|
+
reply.status(503).send({ error: error.message })
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
fastify.get('/', async () => {
|
|
79
|
+
throw new Error('handler error')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
const res = await fastify.inject('/')
|
|
83
|
+
|
|
84
|
+
// Note: The diagnostics channel fires before the custom error handler runs,
|
|
85
|
+
// so it reports 500 (default) rather than 503 (set by custom handler).
|
|
86
|
+
// This is expected behavior - the error channel reports the initial error state.
|
|
87
|
+
t.assert.strictEqual(res.statusCode, 503)
|
|
88
|
+
t.assert.strictEqual(diagnosticsStatusCode, 500, 'diagnostics channel reports status before custom handler')
|
|
89
|
+
t.assert.notStrictEqual(diagnosticsStatusCode, res.statusCode, 'custom handler can change status after diagnostics')
|
|
90
|
+
})
|
|
91
|
+
|
|
8
92
|
test('Error.status property support', (t, done) => {
|
|
9
93
|
t.plan(4)
|
|
10
94
|
const fastify = Fastify()
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const { sep } = require('node:path')
|
|
2
|
+
const { test } = require('node:test')
|
|
3
|
+
const Fastify = require('../../fastify')
|
|
4
|
+
|
|
5
|
+
test('SchemaController are NOT loaded when the controllers are custom', async t => {
|
|
6
|
+
const app = Fastify({
|
|
7
|
+
schemaController: {
|
|
8
|
+
compilersFactory: {
|
|
9
|
+
buildValidator: () => () => { },
|
|
10
|
+
buildSerializer: () => () => { }
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
await app.ready()
|
|
16
|
+
|
|
17
|
+
const loaded = Object.keys(require.cache)
|
|
18
|
+
const ajvModule = loaded.find((path) => path.includes(`@fastify${sep}ajv-compiler`))
|
|
19
|
+
const stringifyModule = loaded.find((path) => path.includes(`@fastify${sep}fast-json-stringify-compiler`))
|
|
20
|
+
|
|
21
|
+
t.assert.equal(ajvModule, undefined, 'Ajv compiler is loaded')
|
|
22
|
+
t.assert.equal(stringifyModule, undefined, 'Stringify compiler is loaded')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('SchemaController are loaded when the controllers are not custom', async t => {
|
|
26
|
+
const app = Fastify()
|
|
27
|
+
await app.ready()
|
|
28
|
+
|
|
29
|
+
const loaded = Object.keys(require.cache)
|
|
30
|
+
const ajvModule = loaded.find((path) => path.includes(`@fastify${sep}ajv-compiler`))
|
|
31
|
+
const stringifyModule = loaded.find((path) => path.includes(`@fastify${sep}fast-json-stringify-compiler`))
|
|
32
|
+
|
|
33
|
+
t.after(() => {
|
|
34
|
+
delete require.cache[ajvModule]
|
|
35
|
+
delete require.cache[stringifyModule]
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
t.assert.ok(ajvModule, 'Ajv compiler is loaded')
|
|
39
|
+
t.assert.ok(stringifyModule, 'Stringify compiler is loaded')
|
|
40
|
+
})
|
package/test/issue-4959.test.js
CHANGED
|
@@ -11,7 +11,7 @@ const { setTimeout } = require('node:timers')
|
|
|
11
11
|
*
|
|
12
12
|
* @see https://github.com/fastify/fastify/issues/4959
|
|
13
13
|
*/
|
|
14
|
-
function runBadClientCall (reqOptions, payload) {
|
|
14
|
+
function runBadClientCall (reqOptions, payload, waitBeforeDestroy) {
|
|
15
15
|
let innerResolve, innerReject
|
|
16
16
|
const promise = new Promise((resolve, reject) => {
|
|
17
17
|
innerResolve = resolve
|
|
@@ -30,11 +30,25 @@ function runBadClientCall (reqOptions, payload) {
|
|
|
30
30
|
innerReject(new Error('Request should have failed'))
|
|
31
31
|
})
|
|
32
32
|
|
|
33
|
-
// Kill the socket
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
// Kill the socket after the request has been fully written.
|
|
34
|
+
// Destroying it on `connect` can race before any bytes are sent, making the
|
|
35
|
+
// server-side assertions (hooks/handler) non-deterministic.
|
|
36
|
+
//
|
|
37
|
+
// To keep the test deterministic, we optionally wait for a server-side signal
|
|
38
|
+
// (e.g. onSend entered) before aborting the client.
|
|
39
|
+
let socket
|
|
40
|
+
req.on('socket', (s) => { socket = s })
|
|
41
|
+
req.on('finish', () => {
|
|
42
|
+
if (waitBeforeDestroy && typeof waitBeforeDestroy.then === 'function') {
|
|
43
|
+
Promise.race([
|
|
44
|
+
waitBeforeDestroy,
|
|
45
|
+
new Promise(resolve => setTimeout(resolve, 200))
|
|
46
|
+
]).then(() => {
|
|
47
|
+
if (socket) socket.destroy()
|
|
48
|
+
}, innerResolve)
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
setTimeout(() => { socket.destroy() }, 0)
|
|
38
52
|
})
|
|
39
53
|
req.on('error', innerResolve)
|
|
40
54
|
req.write(postData)
|
|
@@ -47,6 +61,11 @@ test('should handle a socket error', async (t) => {
|
|
|
47
61
|
t.plan(4)
|
|
48
62
|
const fastify = Fastify()
|
|
49
63
|
|
|
64
|
+
let resolveOnSendEntered
|
|
65
|
+
const onSendEntered = new Promise((resolve) => {
|
|
66
|
+
resolveOnSendEntered = resolve
|
|
67
|
+
})
|
|
68
|
+
|
|
50
69
|
function shouldNotHappen () {
|
|
51
70
|
t.assert.fail('This should not happen')
|
|
52
71
|
}
|
|
@@ -70,8 +89,14 @@ test('should handle a socket error', async (t) => {
|
|
|
70
89
|
t.assert.ok('onSend hook called')
|
|
71
90
|
request.onSendCalled = true
|
|
72
91
|
|
|
73
|
-
|
|
74
|
-
|
|
92
|
+
if (resolveOnSendEntered) {
|
|
93
|
+
resolveOnSendEntered()
|
|
94
|
+
resolveOnSendEntered = null
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Introduce a delay (gives time for client-side abort to happen while the
|
|
98
|
+
// request has already been processed, exercising the original issue).
|
|
99
|
+
await new Promise(resolve => setTimeout(resolve, 50))
|
|
75
100
|
return payload
|
|
76
101
|
})
|
|
77
102
|
|
|
@@ -88,6 +113,6 @@ test('should handle a socket error', async (t) => {
|
|
|
88
113
|
port: fastify.server.address().port,
|
|
89
114
|
path: '/',
|
|
90
115
|
method: 'PUT'
|
|
91
|
-
}, { test: 'me' })
|
|
116
|
+
}, { test: 'me' }, onSendEntered)
|
|
92
117
|
t.assert.equal(err.code, 'ECONNRESET')
|
|
93
118
|
})
|
package/test/listen.1.test.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const { networkInterfaces } = require('node:os')
|
|
3
4
|
const { test, before } = require('node:test')
|
|
4
5
|
const Fastify = require('..')
|
|
5
6
|
const helper = require('./helper')
|
|
@@ -41,7 +42,14 @@ test('Async/await listen with arguments', async t => {
|
|
|
41
42
|
})
|
|
42
43
|
const addr = await fastify.listen({ port: 0, host: '0.0.0.0' })
|
|
43
44
|
const address = fastify.server.address()
|
|
44
|
-
|
|
45
|
+
const { protocol, hostname, port, pathname } = new URL(addr)
|
|
46
|
+
t.assert.strictEqual(protocol, 'http:')
|
|
47
|
+
t.assert.ok(Object.values(networkInterfaces())
|
|
48
|
+
.flat()
|
|
49
|
+
.filter(({ internal }) => internal)
|
|
50
|
+
.some(({ address }) => address === hostname))
|
|
51
|
+
t.assert.strictEqual(pathname, '/')
|
|
52
|
+
t.assert.strictEqual(Number(port), address.port)
|
|
45
53
|
t.assert.deepEqual(address, {
|
|
46
54
|
address: '0.0.0.0',
|
|
47
55
|
family: 'IPv4',
|