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/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,8 +58,10 @@ 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
|
|
64
|
+
const HTTP2_WRITE_CHUNK_SIZE = 64 * 1024
|
|
63
65
|
|
|
64
66
|
function Reply (res, request, log) {
|
|
65
67
|
this.raw = res
|
|
@@ -75,6 +77,7 @@ function Reply (res, request, log) {
|
|
|
75
77
|
this.log = log
|
|
76
78
|
}
|
|
77
79
|
Reply.props = []
|
|
80
|
+
Reply.instanceProperties = new Set(['raw', 'request', 'log'])
|
|
78
81
|
|
|
79
82
|
Object.defineProperties(Reply.prototype, {
|
|
80
83
|
[kRouteContext]: {
|
|
@@ -133,6 +136,14 @@ Reply.prototype.hijack = function () {
|
|
|
133
136
|
this.request[kOnAbort] = null
|
|
134
137
|
}
|
|
135
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
|
+
}
|
|
136
147
|
return this
|
|
137
148
|
}
|
|
138
149
|
|
|
@@ -157,8 +168,8 @@ Reply.prototype.send = function (payload) {
|
|
|
157
168
|
return this
|
|
158
169
|
}
|
|
159
170
|
|
|
160
|
-
const contentType = this.getHeader('content-type')
|
|
161
|
-
const hasContentType = contentType
|
|
171
|
+
const contentType = ContentType.from(this.getHeader('content-type'))
|
|
172
|
+
const hasContentType = contentType.isEmpty === false
|
|
162
173
|
|
|
163
174
|
if (payload !== null) {
|
|
164
175
|
if (
|
|
@@ -167,7 +178,7 @@ Reply.prototype.send = function (payload) {
|
|
|
167
178
|
// node:stream/web
|
|
168
179
|
typeof payload.getReader === 'function' ||
|
|
169
180
|
// Response
|
|
170
|
-
toString.call(payload) === '[object Response]'
|
|
181
|
+
(typeof payload === 'object' && toString.call(payload) === '[object Response]')
|
|
171
182
|
) {
|
|
172
183
|
onSendHook(this, payload)
|
|
173
184
|
return this
|
|
@@ -199,18 +210,17 @@ Reply.prototype.send = function (payload) {
|
|
|
199
210
|
payload = this[kReplySerializer](payload)
|
|
200
211
|
|
|
201
212
|
// The indexOf below also matches custom json mimetypes such as 'application/hal+json' or 'application/ld+json'
|
|
202
|
-
} else if (!hasContentType || contentType.indexOf('json') !== -1) {
|
|
213
|
+
} else if (!hasContentType || contentType.mediaType.indexOf('json') !== -1) {
|
|
203
214
|
if (!hasContentType) {
|
|
204
215
|
this[kReplyHeaders]['content-type'] = CONTENT_TYPE.JSON
|
|
205
|
-
} 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).
|
|
206
219
|
// If user doesn't set charset, we will set charset to utf-8
|
|
207
|
-
const customContentType = contentType.
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
} else {
|
|
212
|
-
this[kReplyHeaders]['content-type'] = `${customContentType}; charset=utf-8`
|
|
213
|
-
}
|
|
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`
|
|
214
224
|
}
|
|
215
225
|
|
|
216
226
|
if (typeof payload !== 'string') {
|
|
@@ -593,7 +603,7 @@ function onSendEnd (reply, payload) {
|
|
|
593
603
|
// since Response contain status code, headers and body,
|
|
594
604
|
// we need to update the status, add the headers and use it's body as payload
|
|
595
605
|
// before continuing
|
|
596
|
-
if (toString.call(payload) === '[object Response]') {
|
|
606
|
+
if (payload != null && typeof payload === 'object' && toString.call(payload) === '[object Response]') {
|
|
597
607
|
// https://developer.mozilla.org/en-US/docs/Web/API/Response/status
|
|
598
608
|
if (typeof payload.status === 'number') {
|
|
599
609
|
reply.code(payload.status)
|
|
@@ -677,19 +687,62 @@ function onSendEnd (reply, payload) {
|
|
|
677
687
|
|
|
678
688
|
safeWriteHead(reply, statusCode)
|
|
679
689
|
// write payload first
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
690
|
+
writePayload(payload, res, reply)
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function isHttp2Reply (reply) {
|
|
694
|
+
return reply.request.raw?.httpVersionMajor === 2
|
|
683
695
|
}
|
|
684
696
|
|
|
685
|
-
function
|
|
686
|
-
if (
|
|
687
|
-
if (
|
|
688
|
-
|
|
697
|
+
function writePayload (payload, res, reply) {
|
|
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
|
|
689
704
|
}
|
|
690
|
-
|
|
691
|
-
|
|
705
|
+
res.write(payload)
|
|
706
|
+
// then send trailers
|
|
707
|
+
sendTrailer(payload, res, reply)
|
|
708
|
+
return
|
|
692
709
|
}
|
|
710
|
+
|
|
711
|
+
writeHttp2Payload(payload, res, () => {
|
|
712
|
+
// then send trailers
|
|
713
|
+
sendTrailer(payload, res, reply)
|
|
714
|
+
})
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function writeHttp2Payload (payload, res, done) {
|
|
718
|
+
const buffer = Buffer.isBuffer(payload) ? payload : Buffer.from(payload)
|
|
719
|
+
let offset = 0
|
|
720
|
+
|
|
721
|
+
function writeChunk () {
|
|
722
|
+
while (offset < buffer.length) {
|
|
723
|
+
/* c8 ignore next 3 - defensive guard for aborted responses */
|
|
724
|
+
if (res.destroyed || res.writableEnded) {
|
|
725
|
+
return
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const end = Math.min(offset + HTTP2_WRITE_CHUNK_SIZE, buffer.length)
|
|
729
|
+
const shouldContinue = res.write(buffer.subarray(offset, end))
|
|
730
|
+
offset = end
|
|
731
|
+
|
|
732
|
+
if (shouldContinue === false) {
|
|
733
|
+
res.once('drain', writeChunk)
|
|
734
|
+
return
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
done()
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
writeChunk()
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function logStreamError (err, reply, res) {
|
|
745
|
+
reply.server[kLogController].streamError(err, reply.request, reply)
|
|
693
746
|
}
|
|
694
747
|
|
|
695
748
|
function sendWebStream (payload, res, reply) {
|
|
@@ -706,7 +759,7 @@ function sendWebStream (payload, res, reply) {
|
|
|
706
759
|
if (sourceOpen) {
|
|
707
760
|
if (err != null && res.headersSent && !errorLogged) {
|
|
708
761
|
errorLogged = true
|
|
709
|
-
logStreamError(
|
|
762
|
+
logStreamError(err, reply, res)
|
|
710
763
|
}
|
|
711
764
|
reader.cancel().catch(noop)
|
|
712
765
|
}
|
|
@@ -754,7 +807,7 @@ function sendWebStream (payload, res, reply) {
|
|
|
754
807
|
if (res.headersSent || reply.request.raw.aborted === true) {
|
|
755
808
|
if (!errorLogged) {
|
|
756
809
|
errorLogged = true
|
|
757
|
-
logStreamError(
|
|
810
|
+
logStreamError(err, reply, reply)
|
|
758
811
|
}
|
|
759
812
|
res.destroy()
|
|
760
813
|
} else {
|
|
@@ -778,7 +831,7 @@ function sendStream (payload, res, reply) {
|
|
|
778
831
|
if (res.headersSent || reply.request.raw.aborted === true) {
|
|
779
832
|
if (!errorLogged) {
|
|
780
833
|
errorLogged = true
|
|
781
|
-
logStreamError(
|
|
834
|
+
logStreamError(err, reply, reply)
|
|
782
835
|
}
|
|
783
836
|
res.destroy()
|
|
784
837
|
} else {
|
|
@@ -792,7 +845,7 @@ function sendStream (payload, res, reply) {
|
|
|
792
845
|
if (sourceOpen) {
|
|
793
846
|
if (err != null && res.headersSent && !errorLogged) {
|
|
794
847
|
errorLogged = true
|
|
795
|
-
logStreamError(
|
|
848
|
+
logStreamError(err, reply, res)
|
|
796
849
|
}
|
|
797
850
|
if (typeof payload.destroy === 'function') {
|
|
798
851
|
payload.destroy()
|
|
@@ -830,10 +883,12 @@ function sendTrailer (payload, res, reply) {
|
|
|
830
883
|
const trailers = {}
|
|
831
884
|
let handled = 0
|
|
832
885
|
let skipped = true
|
|
886
|
+
let sent = false
|
|
833
887
|
function send () {
|
|
834
888
|
// add trailers when all handler handled
|
|
835
889
|
/* istanbul ignore else */
|
|
836
|
-
if (handled === 0) {
|
|
890
|
+
if (handled === 0 && !sent) {
|
|
891
|
+
sent = true
|
|
837
892
|
res.addTrailers(trailers)
|
|
838
893
|
// we need to properly close the stream
|
|
839
894
|
// after trailers sent
|
|
@@ -846,9 +901,10 @@ function sendTrailer (payload, res, reply) {
|
|
|
846
901
|
skipped = false
|
|
847
902
|
handled--
|
|
848
903
|
|
|
904
|
+
let cbAlreadyCalled = false
|
|
849
905
|
function cb (err, value) {
|
|
850
|
-
|
|
851
|
-
|
|
906
|
+
if (cbAlreadyCalled) return
|
|
907
|
+
cbAlreadyCalled = true
|
|
852
908
|
handled++
|
|
853
909
|
|
|
854
910
|
// we can safely ignore error for trailer
|
|
@@ -914,6 +970,14 @@ function setupResponseListeners (reply) {
|
|
|
914
970
|
}
|
|
915
971
|
}
|
|
916
972
|
|
|
973
|
+
// Fix: release socket._meta so request/reply objects are not retained
|
|
974
|
+
// past the response on keep-alive connections.
|
|
975
|
+
const socket = reply.request.raw.socket
|
|
976
|
+
if (socket && socket._meta && socket._meta.request === reply.request) {
|
|
977
|
+
socket.removeListener('timeout', socket._meta.onTimeout)
|
|
978
|
+
socket._meta = null
|
|
979
|
+
}
|
|
980
|
+
|
|
917
981
|
if (ctx && ctx.onResponse !== null) {
|
|
918
982
|
onResponseHookRunner(
|
|
919
983
|
ctx.onResponse,
|
|
@@ -931,25 +995,7 @@ function setupResponseListeners (reply) {
|
|
|
931
995
|
}
|
|
932
996
|
|
|
933
997
|
function onResponseCallback (err, request, reply) {
|
|
934
|
-
|
|
935
|
-
return
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
const responseTime = reply.elapsedTime
|
|
939
|
-
|
|
940
|
-
if (err != null) {
|
|
941
|
-
reply.log.error({
|
|
942
|
-
res: reply,
|
|
943
|
-
err,
|
|
944
|
-
responseTime
|
|
945
|
-
}, 'request errored')
|
|
946
|
-
return
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
reply.log.info({
|
|
950
|
-
res: reply,
|
|
951
|
-
responseTime
|
|
952
|
-
}, 'request completed')
|
|
998
|
+
reply.server[kLogController].requestCompleted(err, request, reply)
|
|
953
999
|
}
|
|
954
1000
|
|
|
955
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
|
@@ -10,6 +10,7 @@ const {
|
|
|
10
10
|
kSchemaController,
|
|
11
11
|
kOptions,
|
|
12
12
|
kRequestCacheValidateFns,
|
|
13
|
+
kRequestContentType,
|
|
13
14
|
kRouteContext,
|
|
14
15
|
kRequestOriginalUrl,
|
|
15
16
|
kRequestSignal,
|
|
@@ -17,6 +18,7 @@ const {
|
|
|
17
18
|
} = require('./symbols')
|
|
18
19
|
const { FST_ERR_REQ_INVALID_VALIDATION_INVOCATION, FST_ERR_DEC_UNDECLARED } = require('./errors')
|
|
19
20
|
const decorators = require('./decorate')
|
|
21
|
+
const ContentType = require('./content-type')
|
|
20
22
|
|
|
21
23
|
const HTTP_PART_SYMBOL_MAP = {
|
|
22
24
|
body: kSchemaBody,
|
|
@@ -36,6 +38,7 @@ function Request (id, params, req, query, log, context) {
|
|
|
36
38
|
this.body = undefined
|
|
37
39
|
}
|
|
38
40
|
Request.props = []
|
|
41
|
+
Request.instanceProperties = new Set(['id', 'params', 'raw', 'query', 'log', 'body'])
|
|
39
42
|
|
|
40
43
|
function getTrustProxyFn (tp) {
|
|
41
44
|
if (typeof tp === 'function') {
|
|
@@ -117,8 +120,8 @@ function buildRequestWithTrustProxy (R, trustProxy) {
|
|
|
117
120
|
},
|
|
118
121
|
host: {
|
|
119
122
|
get () {
|
|
120
|
-
const
|
|
121
|
-
if (this.headers['x-forwarded-host'] &&
|
|
123
|
+
const socket = this.raw.socket
|
|
124
|
+
if (this.headers['x-forwarded-host'] && socket != null && proxyFn(socket.remoteAddress, 0)) {
|
|
122
125
|
return getLastEntryInMultiHeaderValue(this.headers['x-forwarded-host'])
|
|
123
126
|
}
|
|
124
127
|
/**
|
|
@@ -132,8 +135,8 @@ function buildRequestWithTrustProxy (R, trustProxy) {
|
|
|
132
135
|
},
|
|
133
136
|
protocol: {
|
|
134
137
|
get () {
|
|
135
|
-
const
|
|
136
|
-
if (this.headers['x-forwarded-proto'] &&
|
|
138
|
+
const socket = this.raw.socket
|
|
139
|
+
if (this.headers['x-forwarded-proto'] && socket != null && proxyFn(socket.remoteAddress, 0)) {
|
|
137
140
|
return getLastEntryInMultiHeaderValue(this.headers['x-forwarded-proto'])
|
|
138
141
|
}
|
|
139
142
|
if (this.socket) {
|
|
@@ -256,8 +259,7 @@ Object.defineProperties(Request.prototype, {
|
|
|
256
259
|
port: {
|
|
257
260
|
get () {
|
|
258
261
|
const portReg = /(?<port>:\d+)$/
|
|
259
|
-
const
|
|
260
|
-
const matches = portReg.exec(host)
|
|
262
|
+
const matches = portReg.exec(this.host)
|
|
261
263
|
if (matches === null || matches[1] === undefined) {
|
|
262
264
|
return null
|
|
263
265
|
}
|
|
@@ -282,6 +284,14 @@ Object.defineProperties(Request.prototype, {
|
|
|
282
284
|
this.additionalHeaders = headers
|
|
283
285
|
}
|
|
284
286
|
},
|
|
287
|
+
mediaType: {
|
|
288
|
+
get () {
|
|
289
|
+
if (!this[kRequestContentType] && this.headers['content-type'] !== undefined) {
|
|
290
|
+
this[kRequestContentType] = ContentType.from(this.headers['content-type'])
|
|
291
|
+
}
|
|
292
|
+
return this[kRequestContentType]?.mediaType
|
|
293
|
+
}
|
|
294
|
+
},
|
|
285
295
|
getValidationFunction: {
|
|
286
296
|
value: function (httpPartOrSchema) {
|
|
287
297
|
if (typeof httpPartOrSchema === 'string') {
|
package/lib/route.js
CHANGED
|
@@ -24,6 +24,7 @@ const {
|
|
|
24
24
|
FST_ERR_ROUTE_MISSING_HANDLER,
|
|
25
25
|
FST_ERR_ROUTE_METHOD_NOT_SUPPORTED,
|
|
26
26
|
FST_ERR_ROUTE_METHOD_INVALID,
|
|
27
|
+
FST_ERR_ROUTE_LOG_LEVEL_INVALID,
|
|
27
28
|
FST_ERR_ROUTE_BODY_VALIDATION_SCHEMA_NOT_SUPPORTED,
|
|
28
29
|
FST_ERR_ROUTE_BODY_LIMIT_OPTION_NOT_INT,
|
|
29
30
|
FST_ERR_ROUTE_HANDLER_TIMEOUT_OPTION_NOT_INT,
|
|
@@ -42,7 +43,6 @@ const {
|
|
|
42
43
|
kReplySerializerDefault,
|
|
43
44
|
kReplyIsError,
|
|
44
45
|
kRequestPayloadStream,
|
|
45
|
-
kDisableRequestLogging,
|
|
46
46
|
kSchemaErrorFormatter,
|
|
47
47
|
kErrorHandler,
|
|
48
48
|
kHasBeenDecorated,
|
|
@@ -51,10 +51,11 @@ const {
|
|
|
51
51
|
kRouteContext,
|
|
52
52
|
kRequestSignal,
|
|
53
53
|
kTimeoutTimer,
|
|
54
|
-
kOnAbort
|
|
54
|
+
kOnAbort,
|
|
55
|
+
kLogController
|
|
55
56
|
} = require('./symbols.js')
|
|
56
57
|
const { buildErrorHandler } = require('./error-handler')
|
|
57
|
-
const { createChildLogger } = require('./logger-factory.js')
|
|
58
|
+
const { createChildLogger, defaultChildLoggerFactory } = require('./logger-factory.js')
|
|
58
59
|
const { getGenReqId } = require('./req-id-gen-factory.js')
|
|
59
60
|
const { FSTDEP022 } = require('./warnings')
|
|
60
61
|
|
|
@@ -68,6 +69,7 @@ const routerKeys = [
|
|
|
68
69
|
'ignoreTrailingSlash',
|
|
69
70
|
'maxParamLength',
|
|
70
71
|
'onBadUrl',
|
|
72
|
+
'onMaxParamLength',
|
|
71
73
|
'querystringParser',
|
|
72
74
|
'useSemicolonDelimiter'
|
|
73
75
|
]
|
|
@@ -81,8 +83,6 @@ function buildRouting (options) {
|
|
|
81
83
|
let hasLogger
|
|
82
84
|
let setupResponseListeners
|
|
83
85
|
let throwIfAlreadyStarted
|
|
84
|
-
let disableRequestLogging
|
|
85
|
-
let disableRequestLoggingFn
|
|
86
86
|
let ignoreTrailingSlash
|
|
87
87
|
let ignoreDuplicateSlashes
|
|
88
88
|
let return503OnClosing
|
|
@@ -105,10 +105,6 @@ function buildRouting (options) {
|
|
|
105
105
|
throwIfAlreadyStarted = fastifyArgs.throwIfAlreadyStarted
|
|
106
106
|
|
|
107
107
|
globalExposeHeadRoutes = options.exposeHeadRoutes
|
|
108
|
-
disableRequestLogging = options.disableRequestLogging
|
|
109
|
-
if (typeof disableRequestLogging === 'function') {
|
|
110
|
-
disableRequestLoggingFn = options.disableRequestLogging
|
|
111
|
-
}
|
|
112
108
|
|
|
113
109
|
ignoreTrailingSlash = options.routerOptions.ignoreTrailingSlash
|
|
114
110
|
ignoreDuplicateSlashes = options.routerOptions.ignoreDuplicateSlashes
|
|
@@ -284,6 +280,7 @@ function buildRouting (options) {
|
|
|
284
280
|
opts.routePath = path
|
|
285
281
|
opts.prefix = prefix
|
|
286
282
|
opts.logLevel = opts.logLevel || this[kLogLevel]
|
|
283
|
+
validateLogLevelOption(opts.logLevel, opts.method, opts.url, logger)
|
|
287
284
|
|
|
288
285
|
if (this[kLogSerializers] || opts.logSerializers) {
|
|
289
286
|
opts.logSerializers = Object.assign(Object.create(this[kLogSerializers]), opts.logSerializers)
|
|
@@ -365,7 +362,8 @@ function buildRouting (options) {
|
|
|
365
362
|
// any route insertion error created by fastify can be safely ignore
|
|
366
363
|
// because it only duplicate route for head
|
|
367
364
|
if (!context[kRouteByFastify]) {
|
|
368
|
-
const
|
|
365
|
+
const methods = Array.isArray(opts.method) ? opts.method : [opts.method]
|
|
366
|
+
const isDuplicatedRoute = methods.some(method => error.message.includes(`Method '${method}' already declared for route`))
|
|
369
367
|
if (isDuplicatedRoute) {
|
|
370
368
|
throw new FST_ERR_DUPLICATED_ROUTE(opts.method, opts.url)
|
|
371
369
|
}
|
|
@@ -459,16 +457,21 @@ function buildRouting (options) {
|
|
|
459
457
|
function routeHandler (req, res, params, context, query) {
|
|
460
458
|
const id = getGenReqId(context.server, req)
|
|
461
459
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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
|
+
}
|
|
465
469
|
|
|
466
|
-
|
|
467
|
-
|
|
470
|
+
if (context.logSerializers) {
|
|
471
|
+
loggerOpts.serializers = context.logSerializers
|
|
472
|
+
}
|
|
473
|
+
childLogger = createChildLogger(context, logger, req, id, loggerOpts)
|
|
468
474
|
}
|
|
469
|
-
const childLogger = createChildLogger(context, logger, req, id, loggerOpts)
|
|
470
|
-
// Set initial value; will be re-evaluated after FastifyRequest is constructed if it's a function
|
|
471
|
-
childLogger[kDisableRequestLogging] = disableRequestLoggingFn ? false : disableRequestLogging
|
|
472
475
|
|
|
473
476
|
if (closing === true) {
|
|
474
477
|
/* istanbul ignore next mac, windows */
|
|
@@ -476,19 +479,22 @@ function buildRouting (options) {
|
|
|
476
479
|
res.setHeader('Connection', 'close')
|
|
477
480
|
}
|
|
478
481
|
|
|
479
|
-
//
|
|
480
|
-
|
|
482
|
+
// Load-shedding fast path during drain. server.close() and
|
|
483
|
+
// closeIdleConnections() only reap idle sockets; requests already
|
|
484
|
+
// pipelined or arriving on an active keep-alive connection still reach
|
|
485
|
+
// this point with closing === true. Short-circuiting with 503 avoids
|
|
486
|
+
// running the full handler chain (and any downstream calls) for work
|
|
487
|
+
// the load balancer should redirect elsewhere.
|
|
481
488
|
if (return503OnClosing) {
|
|
482
|
-
// On Node v19 we cannot test this behavior as it won't be necessary
|
|
483
|
-
// anymore. It will close all the idle connections before they reach this
|
|
484
|
-
// stage.
|
|
485
489
|
const headers = {
|
|
486
490
|
'Content-Type': 'application/json',
|
|
487
491
|
'Content-Length': '80'
|
|
488
492
|
}
|
|
489
493
|
res.writeHead(503, headers)
|
|
490
494
|
res.end('{"error":"Service Unavailable","message":"Service Unavailable","statusCode":503}')
|
|
491
|
-
|
|
495
|
+
|
|
496
|
+
context.server[kLogController].serviceUnavailable(childLogger, context.server)
|
|
497
|
+
|
|
492
498
|
return
|
|
493
499
|
}
|
|
494
500
|
}
|
|
@@ -513,16 +519,7 @@ function buildRouting (options) {
|
|
|
513
519
|
const request = new context.Request(id, params, req, query, childLogger, context)
|
|
514
520
|
const reply = new context.Reply(res, request, childLogger)
|
|
515
521
|
|
|
516
|
-
|
|
517
|
-
// so the caller has access to decorations and customizations
|
|
518
|
-
const resolvedDisableRequestLogging = disableRequestLoggingFn
|
|
519
|
-
? disableRequestLoggingFn(request)
|
|
520
|
-
: disableRequestLogging
|
|
521
|
-
childLogger[kDisableRequestLogging] = resolvedDisableRequestLogging
|
|
522
|
-
|
|
523
|
-
if (resolvedDisableRequestLogging === false) {
|
|
524
|
-
childLogger.info({ req: request }, 'incoming request')
|
|
525
|
-
}
|
|
522
|
+
context.server[kLogController].incomingRequest(request, reply)
|
|
526
523
|
|
|
527
524
|
// Handler timeout setup — only when configured (zero overhead otherwise)
|
|
528
525
|
const handlerTimeout = context.handlerTimeout
|
|
@@ -553,6 +550,13 @@ function buildRouting (options) {
|
|
|
553
550
|
setupResponseListeners(reply)
|
|
554
551
|
}
|
|
555
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
|
+
|
|
556
560
|
if (context.onRequest !== null) {
|
|
557
561
|
onRequestHookRunner(
|
|
558
562
|
context.onRequest,
|
|
@@ -576,13 +580,6 @@ function buildRouting (options) {
|
|
|
576
580
|
}
|
|
577
581
|
})
|
|
578
582
|
}
|
|
579
|
-
|
|
580
|
-
if (context.onTimeout !== null) {
|
|
581
|
-
if (!request.raw.socket._meta) {
|
|
582
|
-
request.raw.socket.on('timeout', handleTimeout)
|
|
583
|
-
}
|
|
584
|
-
request.raw.socket._meta = { context, request, reply }
|
|
585
|
-
}
|
|
586
583
|
}
|
|
587
584
|
}
|
|
588
585
|
|
|
@@ -635,6 +632,15 @@ function validateHandlerTimeoutOption (handlerTimeout) {
|
|
|
635
632
|
}
|
|
636
633
|
}
|
|
637
634
|
|
|
635
|
+
function validateLogLevelOption (logLevel, method, path, logger) {
|
|
636
|
+
if (logLevel == null || logLevel === '') return
|
|
637
|
+
if (logger?.levels?.values == null) return
|
|
638
|
+
|
|
639
|
+
if (typeof logLevel !== 'string' || logger.levels.values[logLevel] === undefined) {
|
|
640
|
+
throw new FST_ERR_ROUTE_LOG_LEVEL_INVALID(method, path, logLevel)
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
638
644
|
function runPreParsing (err, request, reply) {
|
|
639
645
|
if (reply.sent === true) return
|
|
640
646
|
if (err != null) {
|
package/lib/schema-controller.js
CHANGED
|
@@ -31,7 +31,7 @@ function buildSchemaController (parentSchemaCtrl, opts) {
|
|
|
31
31
|
bucket: (opts && opts.bucket) || buildSchemas,
|
|
32
32
|
compilersFactory,
|
|
33
33
|
isCustomValidatorCompiler: typeof opts?.compilersFactory?.buildValidator === 'function',
|
|
34
|
-
isCustomSerializerCompiler: typeof opts?.compilersFactory?.
|
|
34
|
+
isCustomSerializerCompiler: typeof opts?.compilersFactory?.buildSerializer === 'function'
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
return new SchemaController(undefined, option)
|
package/lib/schemas.js
CHANGED
|
@@ -10,6 +10,7 @@ const {
|
|
|
10
10
|
FST_ERR_SCH_DUPLICATE,
|
|
11
11
|
FST_ERR_SCH_CONTENT_MISSING_SCHEMA
|
|
12
12
|
} = require('./errors')
|
|
13
|
+
const ContentType = require('./content-type')
|
|
13
14
|
|
|
14
15
|
const SCHEMAS_SOURCE = ['params', 'body', 'querystring', 'query', 'headers']
|
|
15
16
|
|
|
@@ -147,52 +148,58 @@ function getSchemaSerializer (context, statusCode, contentType) {
|
|
|
147
148
|
return false
|
|
148
149
|
}
|
|
149
150
|
if (responseSchemaDef[statusCode]) {
|
|
150
|
-
if (responseSchemaDef[statusCode].constructor === Object
|
|
151
|
-
const
|
|
152
|
-
if (
|
|
153
|
-
|
|
154
|
-
|
|
151
|
+
if (responseSchemaDef[statusCode].constructor === Object) {
|
|
152
|
+
const ct = new ContentType(contentType)
|
|
153
|
+
if (ct.isValid) {
|
|
154
|
+
if (responseSchemaDef[statusCode][ct.mediaType]) {
|
|
155
|
+
return responseSchemaDef[statusCode][ct.mediaType]
|
|
156
|
+
}
|
|
155
157
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
158
|
+
// fallback to match all media-type
|
|
159
|
+
if (responseSchemaDef[statusCode]['*/*']) {
|
|
160
|
+
return responseSchemaDef[statusCode]['*/*']
|
|
161
|
+
}
|
|
160
162
|
|
|
161
|
-
|
|
163
|
+
return false
|
|
164
|
+
}
|
|
162
165
|
}
|
|
163
166
|
return responseSchemaDef[statusCode]
|
|
164
167
|
}
|
|
165
168
|
const fallbackStatusCode = (statusCode + '')[0] + 'xx'
|
|
166
169
|
if (responseSchemaDef[fallbackStatusCode]) {
|
|
167
|
-
if (responseSchemaDef[fallbackStatusCode].constructor === Object
|
|
168
|
-
const
|
|
169
|
-
if (
|
|
170
|
-
|
|
171
|
-
|
|
170
|
+
if (responseSchemaDef[fallbackStatusCode].constructor === Object) {
|
|
171
|
+
const ct = new ContentType(contentType)
|
|
172
|
+
if (ct.isValid) {
|
|
173
|
+
if (responseSchemaDef[fallbackStatusCode][ct.mediaType]) {
|
|
174
|
+
return responseSchemaDef[fallbackStatusCode][ct.mediaType]
|
|
175
|
+
}
|
|
172
176
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
+
// fallback to match all media-type
|
|
178
|
+
if (responseSchemaDef[fallbackStatusCode]['*/*']) {
|
|
179
|
+
return responseSchemaDef[fallbackStatusCode]['*/*']
|
|
180
|
+
}
|
|
177
181
|
|
|
178
|
-
|
|
182
|
+
return false
|
|
183
|
+
}
|
|
179
184
|
}
|
|
180
185
|
|
|
181
186
|
return responseSchemaDef[fallbackStatusCode]
|
|
182
187
|
}
|
|
183
188
|
if (responseSchemaDef.default) {
|
|
184
|
-
if (responseSchemaDef.default.constructor === Object
|
|
185
|
-
const
|
|
186
|
-
if (
|
|
187
|
-
|
|
188
|
-
|
|
189
|
+
if (responseSchemaDef.default.constructor === Object) {
|
|
190
|
+
const ct = new ContentType(contentType)
|
|
191
|
+
if (ct.isValid) {
|
|
192
|
+
if (responseSchemaDef.default[ct.mediaType]) {
|
|
193
|
+
return responseSchemaDef.default[ct.mediaType]
|
|
194
|
+
}
|
|
189
195
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
196
|
+
// fallback to match all media-type
|
|
197
|
+
if (responseSchemaDef.default['*/*']) {
|
|
198
|
+
return responseSchemaDef.default['*/*']
|
|
199
|
+
}
|
|
194
200
|
|
|
195
|
-
|
|
201
|
+
return false
|
|
202
|
+
}
|
|
196
203
|
}
|
|
197
204
|
|
|
198
205
|
return responseSchemaDef.default
|