fastify 5.9.0 → 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/build/build-validation.js +2 -2
- package/docs/Guides/Delay-Accepting-Requests.md +1 -1
- package/docs/Guides/Ecosystem.md +10 -7
- 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 +6 -13
- package/docs/Guides/Style-Guide.md +1 -1
- package/docs/Reference/Errors.md +2 -0
- package/docs/Reference/Hooks.md +1 -1
- package/docs/Reference/Logging.md +3 -0
- package/docs/Reference/Request.md +2 -2
- package/docs/Reference/Server.md +107 -8
- package/docs/Reference/Type-Providers.md +24 -4
- package/docs/Reference/TypeScript.md +3 -3
- package/docs/Reference/Warnings.md +4 -0
- package/fastify.d.ts +5 -0
- package/fastify.js +22 -22
- package/lib/content-type-parser.js +1 -11
- package/lib/content-type.js +34 -1
- package/lib/context.js +0 -3
- package/lib/error-handler.js +7 -26
- package/lib/errors.js +7 -0
- package/lib/four-oh-four.js +8 -10
- package/lib/handle-request.js +2 -1
- package/lib/log-controller.js +169 -0
- package/lib/logger-factory.js +25 -4
- package/lib/reply.js +35 -44
- package/lib/req-id-gen-factory.js +4 -1
- package/lib/request.js +3 -3
- package/lib/route.js +27 -35
- package/lib/symbols.js +1 -1
- package/lib/validation.js +9 -0
- package/lib/warnings.js +19 -1
- package/package.json +3 -3
- package/test/content-type.test.js +20 -0
- package/test/genReqId.test.js +24 -0
- package/test/hooks.test.js +71 -0
- package/test/internals/errors.test.js +1 -1
- package/test/internals/logger.test.js +322 -0
- package/test/internals/reply.test.js +35 -4
- package/test/internals/request.test.js +10 -7
- package/test/logger/logging.test.js +40 -1
- package/test/stream.4.test.js +2 -2
- package/test/trust-proxy.test.js +49 -30
- package/types/errors.d.ts +1 -0
- package/types/instance.d.ts +2 -0
- package/types/logger.d.ts +22 -0
package/lib/reply.js
CHANGED
|
@@ -16,7 +16,6 @@ const {
|
|
|
16
16
|
kReplyHasStatusCode,
|
|
17
17
|
kReplyIsRunningOnErrorHook,
|
|
18
18
|
kReplyNextErrorHandler,
|
|
19
|
-
kDisableRequestLogging,
|
|
20
19
|
kSchemaResponse,
|
|
21
20
|
kReplyCacheSerializeFns,
|
|
22
21
|
kSchemaController,
|
|
@@ -24,7 +23,8 @@ const {
|
|
|
24
23
|
kRouteContext,
|
|
25
24
|
kTimeoutTimer,
|
|
26
25
|
kOnAbort,
|
|
27
|
-
kRequestSignal
|
|
26
|
+
kRequestSignal,
|
|
27
|
+
kLogController
|
|
28
28
|
} = require('./symbols.js')
|
|
29
29
|
const {
|
|
30
30
|
onSendHookRunner,
|
|
@@ -58,6 +58,7 @@ const {
|
|
|
58
58
|
FST_ERR_DEC_UNDECLARED
|
|
59
59
|
} = require('./errors')
|
|
60
60
|
const decorators = require('./decorate')
|
|
61
|
+
const ContentType = require('./content-type.js')
|
|
61
62
|
|
|
62
63
|
const toString = Object.prototype.toString
|
|
63
64
|
const HTTP2_WRITE_CHUNK_SIZE = 64 * 1024
|
|
@@ -135,6 +136,14 @@ Reply.prototype.hijack = function () {
|
|
|
135
136
|
this.request[kOnAbort] = null
|
|
136
137
|
}
|
|
137
138
|
}
|
|
139
|
+
// Clear socket._meta so hijacked replies (e.g. WebSocket upgrades) do not
|
|
140
|
+
// retain request/reply objects for the lifetime of the keep-alive socket
|
|
141
|
+
// when an onTimeout hook is registered.
|
|
142
|
+
const socket = this.request.raw.socket
|
|
143
|
+
if (socket?._meta?.request === this.request) {
|
|
144
|
+
socket.removeListener('timeout', socket._meta.onTimeout)
|
|
145
|
+
socket._meta = null
|
|
146
|
+
}
|
|
138
147
|
return this
|
|
139
148
|
}
|
|
140
149
|
|
|
@@ -159,8 +168,8 @@ Reply.prototype.send = function (payload) {
|
|
|
159
168
|
return this
|
|
160
169
|
}
|
|
161
170
|
|
|
162
|
-
const contentType = this.getHeader('content-type')
|
|
163
|
-
const hasContentType = contentType
|
|
171
|
+
const contentType = ContentType.from(this.getHeader('content-type'))
|
|
172
|
+
const hasContentType = contentType.isEmpty === false
|
|
164
173
|
|
|
165
174
|
if (payload !== null) {
|
|
166
175
|
if (
|
|
@@ -201,18 +210,17 @@ Reply.prototype.send = function (payload) {
|
|
|
201
210
|
payload = this[kReplySerializer](payload)
|
|
202
211
|
|
|
203
212
|
// The indexOf below also matches custom json mimetypes such as 'application/hal+json' or 'application/ld+json'
|
|
204
|
-
} else if (!hasContentType || contentType.indexOf('json') !== -1) {
|
|
213
|
+
} else if (!hasContentType || contentType.mediaType.indexOf('json') !== -1) {
|
|
205
214
|
if (!hasContentType) {
|
|
206
215
|
this[kReplyHeaders]['content-type'] = CONTENT_TYPE.JSON
|
|
207
|
-
} else if (contentType.
|
|
216
|
+
} else if (contentType.parameters.has('charset') === false) {
|
|
217
|
+
// The lookup is case-insensitive because HTTP parameter names are
|
|
218
|
+
// case-insensitive (RFC 9110 section 5.6.6).
|
|
208
219
|
// If user doesn't set charset, we will set charset to utf-8
|
|
209
|
-
const customContentType = contentType.
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
} else {
|
|
214
|
-
this[kReplyHeaders]['content-type'] = `${customContentType}; charset=utf-8`
|
|
215
|
-
}
|
|
220
|
+
const customContentType = contentType.toString()
|
|
221
|
+
// ContentType.toString already handle the trailing ;
|
|
222
|
+
// We can safely append the charset parameter to the end of the string
|
|
223
|
+
this[kReplyHeaders]['content-type'] = `${customContentType}; charset=utf-8`
|
|
216
224
|
}
|
|
217
225
|
|
|
218
226
|
if (typeof payload !== 'string') {
|
|
@@ -688,6 +696,12 @@ function isHttp2Reply (reply) {
|
|
|
688
696
|
|
|
689
697
|
function writePayload (payload, res, reply) {
|
|
690
698
|
if (!isHttp2Reply(reply) || Buffer.byteLength(payload) <= HTTP2_WRITE_CHUNK_SIZE) {
|
|
699
|
+
if (reply[kReplyTrailers] === null) {
|
|
700
|
+
// end(payload) corks the socket, so headers, payload and the
|
|
701
|
+
// finishing chunk are flushed with a single write
|
|
702
|
+
res.end(payload, null, null) // avoid ArgumentsAdaptorTrampoline from V8
|
|
703
|
+
return
|
|
704
|
+
}
|
|
691
705
|
res.write(payload)
|
|
692
706
|
// then send trailers
|
|
693
707
|
sendTrailer(payload, res, reply)
|
|
@@ -727,14 +741,8 @@ function writeHttp2Payload (payload, res, done) {
|
|
|
727
741
|
writeChunk()
|
|
728
742
|
}
|
|
729
743
|
|
|
730
|
-
function logStreamError (
|
|
731
|
-
|
|
732
|
-
if (!logger[kDisableRequestLogging]) {
|
|
733
|
-
logger.info({ res }, 'stream closed prematurely')
|
|
734
|
-
}
|
|
735
|
-
} else {
|
|
736
|
-
logger.warn({ err }, 'response terminated with an error with headers already sent')
|
|
737
|
-
}
|
|
744
|
+
function logStreamError (err, reply, res) {
|
|
745
|
+
reply.server[kLogController].streamError(err, reply.request, reply)
|
|
738
746
|
}
|
|
739
747
|
|
|
740
748
|
function sendWebStream (payload, res, reply) {
|
|
@@ -751,7 +759,7 @@ function sendWebStream (payload, res, reply) {
|
|
|
751
759
|
if (sourceOpen) {
|
|
752
760
|
if (err != null && res.headersSent && !errorLogged) {
|
|
753
761
|
errorLogged = true
|
|
754
|
-
logStreamError(
|
|
762
|
+
logStreamError(err, reply, res)
|
|
755
763
|
}
|
|
756
764
|
reader.cancel().catch(noop)
|
|
757
765
|
}
|
|
@@ -799,7 +807,7 @@ function sendWebStream (payload, res, reply) {
|
|
|
799
807
|
if (res.headersSent || reply.request.raw.aborted === true) {
|
|
800
808
|
if (!errorLogged) {
|
|
801
809
|
errorLogged = true
|
|
802
|
-
logStreamError(
|
|
810
|
+
logStreamError(err, reply, reply)
|
|
803
811
|
}
|
|
804
812
|
res.destroy()
|
|
805
813
|
} else {
|
|
@@ -823,7 +831,7 @@ function sendStream (payload, res, reply) {
|
|
|
823
831
|
if (res.headersSent || reply.request.raw.aborted === true) {
|
|
824
832
|
if (!errorLogged) {
|
|
825
833
|
errorLogged = true
|
|
826
|
-
logStreamError(
|
|
834
|
+
logStreamError(err, reply, reply)
|
|
827
835
|
}
|
|
828
836
|
res.destroy()
|
|
829
837
|
} else {
|
|
@@ -837,7 +845,7 @@ function sendStream (payload, res, reply) {
|
|
|
837
845
|
if (sourceOpen) {
|
|
838
846
|
if (err != null && res.headersSent && !errorLogged) {
|
|
839
847
|
errorLogged = true
|
|
840
|
-
logStreamError(
|
|
848
|
+
logStreamError(err, reply, res)
|
|
841
849
|
}
|
|
842
850
|
if (typeof payload.destroy === 'function') {
|
|
843
851
|
payload.destroy()
|
|
@@ -966,6 +974,7 @@ function setupResponseListeners (reply) {
|
|
|
966
974
|
// past the response on keep-alive connections.
|
|
967
975
|
const socket = reply.request.raw.socket
|
|
968
976
|
if (socket && socket._meta && socket._meta.request === reply.request) {
|
|
977
|
+
socket.removeListener('timeout', socket._meta.onTimeout)
|
|
969
978
|
socket._meta = null
|
|
970
979
|
}
|
|
971
980
|
|
|
@@ -986,25 +995,7 @@ function setupResponseListeners (reply) {
|
|
|
986
995
|
}
|
|
987
996
|
|
|
988
997
|
function onResponseCallback (err, request, reply) {
|
|
989
|
-
|
|
990
|
-
return
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
const responseTime = reply.elapsedTime
|
|
994
|
-
|
|
995
|
-
if (err != null) {
|
|
996
|
-
reply.log.error({
|
|
997
|
-
res: reply,
|
|
998
|
-
err,
|
|
999
|
-
responseTime
|
|
1000
|
-
}, 'request errored')
|
|
1001
|
-
return
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
reply.log.info({
|
|
1005
|
-
res: reply,
|
|
1006
|
-
responseTime
|
|
1007
|
-
}, 'request completed')
|
|
998
|
+
reply.server[kLogController].requestCompleted(err, request, reply)
|
|
1008
999
|
}
|
|
1009
1000
|
|
|
1010
1001
|
function buildReply (R) {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const { kGenReqId } = require('./symbols.js')
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* @callback GenerateRequestId
|
|
5
7
|
* @param {Object} req
|
|
@@ -22,7 +24,8 @@ function reqIdGenFactory (requestIdHeader, optGenReqId) {
|
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
function getGenReqId (contextServer, req) {
|
|
25
|
-
|
|
27
|
+
// direct symbol access skips the `genReqId` getter indirection
|
|
28
|
+
return contextServer[kGenReqId](req)
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
function buildDefaultGenReqId () {
|
package/lib/request.js
CHANGED
|
@@ -18,6 +18,7 @@ const {
|
|
|
18
18
|
} = require('./symbols')
|
|
19
19
|
const { FST_ERR_REQ_INVALID_VALIDATION_INVOCATION, FST_ERR_DEC_UNDECLARED } = require('./errors')
|
|
20
20
|
const decorators = require('./decorate')
|
|
21
|
+
const ContentType = require('./content-type')
|
|
21
22
|
|
|
22
23
|
const HTTP_PART_SYMBOL_MAP = {
|
|
23
24
|
body: kSchemaBody,
|
|
@@ -258,8 +259,7 @@ Object.defineProperties(Request.prototype, {
|
|
|
258
259
|
port: {
|
|
259
260
|
get () {
|
|
260
261
|
const portReg = /(?<port>:\d+)$/
|
|
261
|
-
const
|
|
262
|
-
const matches = portReg.exec(host)
|
|
262
|
+
const matches = portReg.exec(this.host)
|
|
263
263
|
if (matches === null || matches[1] === undefined) {
|
|
264
264
|
return null
|
|
265
265
|
}
|
|
@@ -287,7 +287,7 @@ Object.defineProperties(Request.prototype, {
|
|
|
287
287
|
mediaType: {
|
|
288
288
|
get () {
|
|
289
289
|
if (!this[kRequestContentType] && this.headers['content-type'] !== undefined) {
|
|
290
|
-
this[kRequestContentType] =
|
|
290
|
+
this[kRequestContentType] = ContentType.from(this.headers['content-type'])
|
|
291
291
|
}
|
|
292
292
|
return this[kRequestContentType]?.mediaType
|
|
293
293
|
}
|
package/lib/route.js
CHANGED
|
@@ -43,7 +43,6 @@ const {
|
|
|
43
43
|
kReplySerializerDefault,
|
|
44
44
|
kReplyIsError,
|
|
45
45
|
kRequestPayloadStream,
|
|
46
|
-
kDisableRequestLogging,
|
|
47
46
|
kSchemaErrorFormatter,
|
|
48
47
|
kErrorHandler,
|
|
49
48
|
kHasBeenDecorated,
|
|
@@ -52,10 +51,11 @@ const {
|
|
|
52
51
|
kRouteContext,
|
|
53
52
|
kRequestSignal,
|
|
54
53
|
kTimeoutTimer,
|
|
55
|
-
kOnAbort
|
|
54
|
+
kOnAbort,
|
|
55
|
+
kLogController
|
|
56
56
|
} = require('./symbols.js')
|
|
57
57
|
const { buildErrorHandler } = require('./error-handler')
|
|
58
|
-
const { createChildLogger } = require('./logger-factory.js')
|
|
58
|
+
const { createChildLogger, defaultChildLoggerFactory } = require('./logger-factory.js')
|
|
59
59
|
const { getGenReqId } = require('./req-id-gen-factory.js')
|
|
60
60
|
const { FSTDEP022 } = require('./warnings')
|
|
61
61
|
|
|
@@ -83,8 +83,6 @@ function buildRouting (options) {
|
|
|
83
83
|
let hasLogger
|
|
84
84
|
let setupResponseListeners
|
|
85
85
|
let throwIfAlreadyStarted
|
|
86
|
-
let disableRequestLogging
|
|
87
|
-
let disableRequestLoggingFn
|
|
88
86
|
let ignoreTrailingSlash
|
|
89
87
|
let ignoreDuplicateSlashes
|
|
90
88
|
let return503OnClosing
|
|
@@ -107,10 +105,6 @@ function buildRouting (options) {
|
|
|
107
105
|
throwIfAlreadyStarted = fastifyArgs.throwIfAlreadyStarted
|
|
108
106
|
|
|
109
107
|
globalExposeHeadRoutes = options.exposeHeadRoutes
|
|
110
|
-
disableRequestLogging = options.disableRequestLogging
|
|
111
|
-
if (typeof disableRequestLogging === 'function') {
|
|
112
|
-
disableRequestLoggingFn = options.disableRequestLogging
|
|
113
|
-
}
|
|
114
108
|
|
|
115
109
|
ignoreTrailingSlash = options.routerOptions.ignoreTrailingSlash
|
|
116
110
|
ignoreDuplicateSlashes = options.routerOptions.ignoreDuplicateSlashes
|
|
@@ -463,16 +457,21 @@ function buildRouting (options) {
|
|
|
463
457
|
function routeHandler (req, res, params, context, query) {
|
|
464
458
|
const id = getGenReqId(context.server, req)
|
|
465
459
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
460
|
+
let childLogger
|
|
461
|
+
if (hasLogger === false && context.childLoggerFactory === defaultChildLoggerFactory) {
|
|
462
|
+
// the null logger children are the logger itself, so we can skip
|
|
463
|
+
// the child logger creation altogether
|
|
464
|
+
childLogger = logger
|
|
465
|
+
} else {
|
|
466
|
+
const loggerOpts = {
|
|
467
|
+
level: context.logLevel
|
|
468
|
+
}
|
|
469
469
|
|
|
470
|
-
|
|
471
|
-
|
|
470
|
+
if (context.logSerializers) {
|
|
471
|
+
loggerOpts.serializers = context.logSerializers
|
|
472
|
+
}
|
|
473
|
+
childLogger = createChildLogger(context, logger, req, id, loggerOpts)
|
|
472
474
|
}
|
|
473
|
-
const childLogger = createChildLogger(context, logger, req, id, loggerOpts)
|
|
474
|
-
// Set initial value; will be re-evaluated after FastifyRequest is constructed if it's a function
|
|
475
|
-
childLogger[kDisableRequestLogging] = disableRequestLoggingFn ? false : disableRequestLogging
|
|
476
475
|
|
|
477
476
|
if (closing === true) {
|
|
478
477
|
/* istanbul ignore next mac, windows */
|
|
@@ -493,7 +492,9 @@ function buildRouting (options) {
|
|
|
493
492
|
}
|
|
494
493
|
res.writeHead(503, headers)
|
|
495
494
|
res.end('{"error":"Service Unavailable","message":"Service Unavailable","statusCode":503}')
|
|
496
|
-
|
|
495
|
+
|
|
496
|
+
context.server[kLogController].serviceUnavailable(childLogger, context.server)
|
|
497
|
+
|
|
497
498
|
return
|
|
498
499
|
}
|
|
499
500
|
}
|
|
@@ -518,16 +519,7 @@ function buildRouting (options) {
|
|
|
518
519
|
const request = new context.Request(id, params, req, query, childLogger, context)
|
|
519
520
|
const reply = new context.Reply(res, request, childLogger)
|
|
520
521
|
|
|
521
|
-
|
|
522
|
-
// so the caller has access to decorations and customizations
|
|
523
|
-
const resolvedDisableRequestLogging = disableRequestLoggingFn
|
|
524
|
-
? disableRequestLoggingFn(request)
|
|
525
|
-
: disableRequestLogging
|
|
526
|
-
childLogger[kDisableRequestLogging] = resolvedDisableRequestLogging
|
|
527
|
-
|
|
528
|
-
if (resolvedDisableRequestLogging === false) {
|
|
529
|
-
childLogger.info({ req: request }, 'incoming request')
|
|
530
|
-
}
|
|
522
|
+
context.server[kLogController].incomingRequest(request, reply)
|
|
531
523
|
|
|
532
524
|
// Handler timeout setup — only when configured (zero overhead otherwise)
|
|
533
525
|
const handlerTimeout = context.handlerTimeout
|
|
@@ -558,6 +550,13 @@ function buildRouting (options) {
|
|
|
558
550
|
setupResponseListeners(reply)
|
|
559
551
|
}
|
|
560
552
|
|
|
553
|
+
if (context.onTimeout !== null) {
|
|
554
|
+
if (!request.raw.socket._meta) {
|
|
555
|
+
request.raw.socket.on('timeout', handleTimeout)
|
|
556
|
+
}
|
|
557
|
+
request.raw.socket._meta = { context, request, reply, onTimeout: handleTimeout }
|
|
558
|
+
}
|
|
559
|
+
|
|
561
560
|
if (context.onRequest !== null) {
|
|
562
561
|
onRequestHookRunner(
|
|
563
562
|
context.onRequest,
|
|
@@ -581,13 +580,6 @@ function buildRouting (options) {
|
|
|
581
580
|
}
|
|
582
581
|
})
|
|
583
582
|
}
|
|
584
|
-
|
|
585
|
-
if (context.onTimeout !== null) {
|
|
586
|
-
if (!request.raw.socket._meta) {
|
|
587
|
-
request.raw.socket.on('timeout', handleTimeout)
|
|
588
|
-
}
|
|
589
|
-
request.raw.socket._meta = { context, request, reply }
|
|
590
|
-
}
|
|
591
583
|
}
|
|
592
584
|
}
|
|
593
585
|
|
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'),
|
|
@@ -67,6 +66,7 @@ const keys = {
|
|
|
67
66
|
kHasBeenDecorated: Symbol('fastify.hasBeenDecorated'),
|
|
68
67
|
kKeepAliveConnections: Symbol('fastify.keepAliveConnections'),
|
|
69
68
|
kRouteByFastify: Symbol('fastify.routeByFastify'),
|
|
69
|
+
kLogController: Symbol('fastify.logController'),
|
|
70
70
|
kDiagnosticsStore: Symbol('fastify.diagnosticsStore')
|
|
71
71
|
}
|
|
72
72
|
|
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) {
|
package/lib/warnings.js
CHANGED
|
@@ -7,6 +7,8 @@ const { createWarning } = require('process-warning')
|
|
|
7
7
|
* - FSTWRN001
|
|
8
8
|
* - FSTSEC001
|
|
9
9
|
* - FSTDEP022
|
|
10
|
+
* - FSTDEP023
|
|
11
|
+
* - FSTDEP024
|
|
10
12
|
*
|
|
11
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.
|
|
@@ -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",
|
|
@@ -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",
|
|
@@ -210,7 +210,7 @@
|
|
|
210
210
|
"@fastify/proxy-addr": "^5.0.0",
|
|
211
211
|
"abstract-logging": "^2.0.1",
|
|
212
212
|
"avvio": "^9.0.0",
|
|
213
|
-
"fast-json-stringify": "^
|
|
213
|
+
"fast-json-stringify": "^7.0.0",
|
|
214
214
|
"find-my-way": "^9.6.0",
|
|
215
215
|
"light-my-request": "^6.0.0",
|
|
216
216
|
"pino": "^9.14.0 || ^10.1.0",
|
|
@@ -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
|
+
})
|
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
|
@@ -3599,3 +3599,74 @@ test('onRequestAbort should handle async errors / 2', (t, testDone) => {
|
|
|
3599
3599
|
sleep(500).then(() => socket.destroy())
|
|
3600
3600
|
})
|
|
3601
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
|
+
})
|