fastify 4.25.2 → 4.26.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.
Files changed (55) hide show
  1. package/EXPENSE_POLICY.md +105 -0
  2. package/GOVERNANCE.md +2 -103
  3. package/LICENSE +1 -1
  4. package/README.md +13 -9
  5. package/SECURITY.md +2 -157
  6. package/SPONSORS.md +20 -0
  7. package/build/build-validation.js +3 -1
  8. package/docs/Guides/Ecosystem.md +29 -9
  9. package/docs/Guides/Getting-Started.md +16 -3
  10. package/docs/Guides/Style-Guide.md +7 -7
  11. package/docs/Reference/Decorators.md +1 -1
  12. package/docs/Reference/Errors.md +63 -1
  13. package/docs/Reference/Hooks.md +1 -1
  14. package/docs/Reference/Logging.md +3 -3
  15. package/docs/Reference/Reply.md +70 -1
  16. package/docs/Reference/Server.md +90 -0
  17. package/docs/Reference/Warnings.md +17 -2
  18. package/fastify.d.ts +3 -2
  19. package/fastify.js +25 -7
  20. package/lib/configValidator.js +62 -33
  21. package/lib/contentTypeParser.js +9 -2
  22. package/lib/error-handler.js +1 -1
  23. package/lib/error-serializer.js +30 -29
  24. package/lib/errors.js +6 -1
  25. package/lib/fourOhFour.js +4 -3
  26. package/lib/hooks.js +1 -5
  27. package/lib/reply.js +68 -10
  28. package/lib/reqIdGenFactory.js +5 -0
  29. package/lib/route.js +22 -6
  30. package/lib/schema-controller.js +37 -4
  31. package/lib/symbols.js +1 -0
  32. package/lib/warnings.js +6 -0
  33. package/package.json +18 -6
  34. package/test/async_hooks.test.js +69 -0
  35. package/test/findRoute.test.js +135 -0
  36. package/test/genReqId.test.js +392 -0
  37. package/test/hooks.on-listen.test.js +66 -14
  38. package/test/hooks.on-ready.test.js +1 -1
  39. package/test/internals/errors.test.js +17 -7
  40. package/test/internals/initialConfig.test.js +7 -3
  41. package/test/internals/reply.test.js +80 -5
  42. package/test/plugin.4.test.js +3 -3
  43. package/test/pretty-print.test.js +1 -1
  44. package/test/schema-serialization.test.js +41 -0
  45. package/test/schema-validation.test.js +115 -6
  46. package/test/serialize-response.test.js +187 -0
  47. package/test/types/instance.test-d.ts +14 -1
  48. package/test/types/reply.test-d.ts +4 -2
  49. package/test/types/request.test-d.ts +1 -1
  50. package/test/types/route.test-d.ts +15 -1
  51. package/test/useSemicolonDelimiter.test.js +113 -0
  52. package/test/web-api.test.js +208 -0
  53. package/types/instance.d.ts +23 -10
  54. package/types/reply.d.ts +4 -0
  55. package/types/request.d.ts +5 -4
@@ -3,7 +3,7 @@
3
3
  "use strict";
4
4
  module.exports = validate10;
5
5
  module.exports.default = validate10;
6
- const schema11 = {"type":"object","additionalProperties":false,"properties":{"connectionTimeout":{"type":"integer","default":0},"keepAliveTimeout":{"type":"integer","default":72000},"forceCloseConnections":{"oneOf":[{"type":"string","pattern":"idle"},{"type":"boolean"}]},"maxRequestsPerSocket":{"type":"integer","default":0,"nullable":true},"requestTimeout":{"type":"integer","default":0},"bodyLimit":{"type":"integer","default":1048576},"caseSensitive":{"type":"boolean","default":true},"allowUnsafeRegex":{"type":"boolean","default":false},"http2":{"type":"boolean"},"https":{"if":{"not":{"oneOf":[{"type":"boolean"},{"type":"null"},{"type":"object","additionalProperties":false,"required":["allowHTTP1"],"properties":{"allowHTTP1":{"type":"boolean"}}}]}},"then":{"setDefaultValue":true}},"ignoreTrailingSlash":{"type":"boolean","default":false},"ignoreDuplicateSlashes":{"type":"boolean","default":false},"disableRequestLogging":{"type":"boolean","default":false},"jsonShorthand":{"type":"boolean","default":true},"maxParamLength":{"type":"integer","default":100},"onProtoPoisoning":{"type":"string","default":"error"},"onConstructorPoisoning":{"type":"string","default":"error"},"pluginTimeout":{"type":"integer","default":10000},"requestIdHeader":{"anyOf":[{"enum":[false]},{"type":"string"}],"default":"request-id"},"requestIdLogLabel":{"type":"string","default":"reqId"},"http2SessionTimeout":{"type":"integer","default":72000},"exposeHeadRoutes":{"type":"boolean","default":true},"versioning":{"type":"object","additionalProperties":true,"required":["storage","deriveVersion"],"properties":{"storage":{},"deriveVersion":{}}},"constraints":{"type":"object","additionalProperties":{"type":"object","required":["name","storage","validate","deriveConstraint"],"additionalProperties":true,"properties":{"name":{"type":"string"},"storage":{},"validate":{},"deriveConstraint":{}}}}}};
6
+ const schema11 = {"type":"object","additionalProperties":false,"properties":{"connectionTimeout":{"type":"integer","default":0},"keepAliveTimeout":{"type":"integer","default":72000},"forceCloseConnections":{"oneOf":[{"type":"string","pattern":"idle"},{"type":"boolean"}]},"maxRequestsPerSocket":{"type":"integer","default":0,"nullable":true},"requestTimeout":{"type":"integer","default":0},"bodyLimit":{"type":"integer","default":1048576},"caseSensitive":{"type":"boolean","default":true},"allowUnsafeRegex":{"type":"boolean","default":false},"http2":{"type":"boolean"},"https":{"if":{"not":{"oneOf":[{"type":"boolean"},{"type":"null"},{"type":"object","additionalProperties":false,"required":["allowHTTP1"],"properties":{"allowHTTP1":{"type":"boolean"}}}]}},"then":{"setDefaultValue":true}},"ignoreTrailingSlash":{"type":"boolean","default":false},"ignoreDuplicateSlashes":{"type":"boolean","default":false},"disableRequestLogging":{"type":"boolean","default":false},"jsonShorthand":{"type":"boolean","default":true},"maxParamLength":{"type":"integer","default":100},"onProtoPoisoning":{"type":"string","default":"error"},"onConstructorPoisoning":{"type":"string","default":"error"},"pluginTimeout":{"type":"integer","default":10000},"requestIdHeader":{"anyOf":[{"enum":[false]},{"type":"string"}],"default":"request-id"},"requestIdLogLabel":{"type":"string","default":"reqId"},"http2SessionTimeout":{"type":"integer","default":72000},"exposeHeadRoutes":{"type":"boolean","default":true},"useSemicolonDelimiter":{"type":"boolean","default":true},"versioning":{"type":"object","additionalProperties":true,"required":["storage","deriveVersion"],"properties":{"storage":{},"deriveVersion":{}}},"constraints":{"type":"object","additionalProperties":{"type":"object","required":["name","storage","validate","deriveConstraint"],"additionalProperties":true,"properties":{"name":{"type":"string"},"storage":{},"validate":{},"deriveConstraint":{}}}}}};
7
7
  const func2 = Object.prototype.hasOwnProperty;
8
8
  const pattern0 = new RegExp("idle", "u");
9
9
 
@@ -69,6 +69,9 @@ data.http2SessionTimeout = 72000;
69
69
  if(data.exposeHeadRoutes === undefined){
70
70
  data.exposeHeadRoutes = true;
71
71
  }
72
+ if(data.useSemicolonDelimiter === undefined){
73
+ data.useSemicolonDelimiter = true;
74
+ }
72
75
  const _errs1 = errors;
73
76
  for(const key0 in data){
74
77
  if(!(func2.call(schema11.properties, key0))){
@@ -985,13 +988,38 @@ data["exposeHeadRoutes"] = coerced24;
985
988
  }
986
989
  var valid0 = _errs66 === errors;
987
990
  if(valid0){
988
- if(data.versioning !== undefined){
989
- let data23 = data.versioning;
991
+ let data23 = data.useSemicolonDelimiter;
990
992
  const _errs68 = errors;
991
- if(errors === _errs68){
992
- if(data23 && typeof data23 == "object" && !Array.isArray(data23)){
993
+ if(typeof data23 !== "boolean"){
994
+ let coerced25 = undefined;
995
+ if(!(coerced25 !== undefined)){
996
+ if(data23 === "false" || data23 === 0 || data23 === null){
997
+ coerced25 = false;
998
+ }
999
+ else if(data23 === "true" || data23 === 1){
1000
+ coerced25 = true;
1001
+ }
1002
+ else {
1003
+ validate10.errors = [{instancePath:instancePath+"/useSemicolonDelimiter",schemaPath:"#/properties/useSemicolonDelimiter/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}];
1004
+ return false;
1005
+ }
1006
+ }
1007
+ if(coerced25 !== undefined){
1008
+ data23 = coerced25;
1009
+ if(data !== undefined){
1010
+ data["useSemicolonDelimiter"] = coerced25;
1011
+ }
1012
+ }
1013
+ }
1014
+ var valid0 = _errs68 === errors;
1015
+ if(valid0){
1016
+ if(data.versioning !== undefined){
1017
+ let data24 = data.versioning;
1018
+ const _errs70 = errors;
1019
+ if(errors === _errs70){
1020
+ if(data24 && typeof data24 == "object" && !Array.isArray(data24)){
993
1021
  let missing1;
994
- if(((data23.storage === undefined) && (missing1 = "storage")) || ((data23.deriveVersion === undefined) && (missing1 = "deriveVersion"))){
1022
+ if(((data24.storage === undefined) && (missing1 = "storage")) || ((data24.deriveVersion === undefined) && (missing1 = "deriveVersion"))){
995
1023
  validate10.errors = [{instancePath:instancePath+"/versioning",schemaPath:"#/properties/versioning/required",keyword:"required",params:{missingProperty: missing1},message:"must have required property '"+missing1+"'"}];
996
1024
  return false;
997
1025
  }
@@ -1001,49 +1029,49 @@ validate10.errors = [{instancePath:instancePath+"/versioning",schemaPath:"#/prop
1001
1029
  return false;
1002
1030
  }
1003
1031
  }
1004
- var valid0 = _errs68 === errors;
1032
+ var valid0 = _errs70 === errors;
1005
1033
  }
1006
1034
  else {
1007
1035
  var valid0 = true;
1008
1036
  }
1009
1037
  if(valid0){
1010
1038
  if(data.constraints !== undefined){
1011
- let data24 = data.constraints;
1012
- const _errs71 = errors;
1013
- if(errors === _errs71){
1014
- if(data24 && typeof data24 == "object" && !Array.isArray(data24)){
1015
- for(const key2 in data24){
1016
- let data25 = data24[key2];
1017
- const _errs74 = errors;
1018
- if(errors === _errs74){
1039
+ let data25 = data.constraints;
1040
+ const _errs73 = errors;
1041
+ if(errors === _errs73){
1019
1042
  if(data25 && typeof data25 == "object" && !Array.isArray(data25)){
1043
+ for(const key2 in data25){
1044
+ let data26 = data25[key2];
1045
+ const _errs76 = errors;
1046
+ if(errors === _errs76){
1047
+ if(data26 && typeof data26 == "object" && !Array.isArray(data26)){
1020
1048
  let missing2;
1021
- if(((((data25.name === undefined) && (missing2 = "name")) || ((data25.storage === undefined) && (missing2 = "storage"))) || ((data25.validate === undefined) && (missing2 = "validate"))) || ((data25.deriveConstraint === undefined) && (missing2 = "deriveConstraint"))){
1049
+ if(((((data26.name === undefined) && (missing2 = "name")) || ((data26.storage === undefined) && (missing2 = "storage"))) || ((data26.validate === undefined) && (missing2 = "validate"))) || ((data26.deriveConstraint === undefined) && (missing2 = "deriveConstraint"))){
1022
1050
  validate10.errors = [{instancePath:instancePath+"/constraints/" + key2.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/constraints/additionalProperties/required",keyword:"required",params:{missingProperty: missing2},message:"must have required property '"+missing2+"'"}];
1023
1051
  return false;
1024
1052
  }
1025
1053
  else {
1026
- if(data25.name !== undefined){
1027
- let data26 = data25.name;
1028
- if(typeof data26 !== "string"){
1029
- let dataType25 = typeof data26;
1030
- let coerced25 = undefined;
1031
- if(!(coerced25 !== undefined)){
1032
- if(dataType25 == "number" || dataType25 == "boolean"){
1033
- coerced25 = "" + data26;
1054
+ if(data26.name !== undefined){
1055
+ let data27 = data26.name;
1056
+ if(typeof data27 !== "string"){
1057
+ let dataType26 = typeof data27;
1058
+ let coerced26 = undefined;
1059
+ if(!(coerced26 !== undefined)){
1060
+ if(dataType26 == "number" || dataType26 == "boolean"){
1061
+ coerced26 = "" + data27;
1034
1062
  }
1035
- else if(data26 === null){
1036
- coerced25 = "";
1063
+ else if(data27 === null){
1064
+ coerced26 = "";
1037
1065
  }
1038
1066
  else {
1039
1067
  validate10.errors = [{instancePath:instancePath+"/constraints/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/name",schemaPath:"#/properties/constraints/additionalProperties/properties/name/type",keyword:"type",params:{type: "string"},message:"must be string"}];
1040
1068
  return false;
1041
1069
  }
1042
1070
  }
1043
- if(coerced25 !== undefined){
1044
- data26 = coerced25;
1045
- if(data25 !== undefined){
1046
- data25["name"] = coerced25;
1071
+ if(coerced26 !== undefined){
1072
+ data27 = coerced26;
1073
+ if(data26 !== undefined){
1074
+ data26["name"] = coerced26;
1047
1075
  }
1048
1076
  }
1049
1077
  }
@@ -1055,7 +1083,7 @@ validate10.errors = [{instancePath:instancePath+"/constraints/" + key2.replace(/
1055
1083
  return false;
1056
1084
  }
1057
1085
  }
1058
- var valid7 = _errs74 === errors;
1086
+ var valid7 = _errs76 === errors;
1059
1087
  if(!valid7){
1060
1088
  break;
1061
1089
  }
@@ -1066,7 +1094,7 @@ validate10.errors = [{instancePath:instancePath+"/constraints",schemaPath:"#/pro
1066
1094
  return false;
1067
1095
  }
1068
1096
  }
1069
- var valid0 = _errs71 === errors;
1097
+ var valid0 = _errs73 === errors;
1070
1098
  }
1071
1099
  else {
1072
1100
  var valid0 = true;
@@ -1096,6 +1124,7 @@ var valid0 = true;
1096
1124
  }
1097
1125
  }
1098
1126
  }
1127
+ }
1099
1128
  else {
1100
1129
  validate10.errors = [{instancePath,schemaPath:"#/type",keyword:"type",params:{type: "object"},message:"must be object"}];
1101
1130
  return false;
@@ -1106,4 +1135,4 @@ return errors === 0;
1106
1135
  }
1107
1136
 
1108
1137
 
1109
- module.exports.defaultInitOptions = {"connectionTimeout":0,"keepAliveTimeout":72000,"maxRequestsPerSocket":0,"requestTimeout":0,"bodyLimit":1048576,"caseSensitive":true,"allowUnsafeRegex":false,"disableRequestLogging":false,"jsonShorthand":true,"ignoreTrailingSlash":false,"ignoreDuplicateSlashes":false,"maxParamLength":100,"onProtoPoisoning":"error","onConstructorPoisoning":"error","pluginTimeout":10000,"requestIdHeader":"request-id","requestIdLogLabel":"reqId","http2SessionTimeout":72000,"exposeHeadRoutes":true}
1138
+ module.exports.defaultInitOptions = {"connectionTimeout":0,"keepAliveTimeout":72000,"maxRequestsPerSocket":0,"requestTimeout":0,"bodyLimit":1048576,"caseSensitive":true,"allowUnsafeRegex":false,"disableRequestLogging":false,"jsonShorthand":true,"ignoreTrailingSlash":false,"ignoreDuplicateSlashes":false,"maxParamLength":100,"onProtoPoisoning":"error","onConstructorPoisoning":"error","pluginTimeout":10000,"requestIdHeader":"request-id","requestIdLogLabel":"reqId","http2SessionTimeout":72000,"exposeHeadRoutes":true,"useSemicolonDelimiter":true}
@@ -157,7 +157,6 @@ ContentTypeParser.prototype.remove = function (contentType) {
157
157
 
158
158
  ContentTypeParser.prototype.run = function (contentType, handler, request, reply) {
159
159
  const parser = this.getParser(contentType)
160
- const resource = new AsyncResource('content-type-parser:run', request)
161
160
 
162
161
  if (parser === undefined) {
163
162
  if (request.is404) {
@@ -165,7 +164,14 @@ ContentTypeParser.prototype.run = function (contentType, handler, request, reply
165
164
  } else {
166
165
  reply.send(new FST_ERR_CTP_INVALID_MEDIA_TYPE(contentType || undefined))
167
166
  }
168
- } else if (parser.asString === true || parser.asBuffer === true) {
167
+
168
+ // Early return to avoid allocating an AsyncResource if it's not needed
169
+ return
170
+ }
171
+
172
+ const resource = new AsyncResource('content-type-parser:run', request)
173
+
174
+ if (parser.asString === true || parser.asBuffer === true) {
169
175
  rawBody(
170
176
  request,
171
177
  reply,
@@ -184,6 +190,7 @@ ContentTypeParser.prototype.run = function (contentType, handler, request, reply
184
190
  function done (error, body) {
185
191
  // We cannot use resource.bind() because it is broken in node v12 and v14
186
192
  resource.runInAsyncScope(() => {
193
+ resource.emitDestroy()
187
194
  if (error) {
188
195
  reply[kReplyIsError] = true
189
196
  reply.send(error)
@@ -95,6 +95,7 @@ function defaultErrorHandler (error, request, reply) {
95
95
  function fallbackErrorHandler (error, reply, cb) {
96
96
  const res = reply.raw
97
97
  const statusCode = reply.statusCode
98
+ reply[kReplyHeaders]['content-type'] = reply[kReplyHeaders]['content-type'] ?? 'application/json; charset=utf-8'
98
99
  let payload
99
100
  try {
100
101
  const serializerFn = getSchemaSerializer(reply[kRouteContext], statusCode, reply[kReplyHeaders]['content-type'])
@@ -121,7 +122,6 @@ function fallbackErrorHandler (error, reply, cb) {
121
122
  payload = serializeError(new FST_ERR_REP_INVALID_PAYLOAD_TYPE(typeof payload))
122
123
  }
123
124
 
124
- reply[kReplyHeaders]['content-type'] = 'application/json; charset=utf-8'
125
125
  reply[kReplyHeaders]['content-length'] = '' + Buffer.byteLength(payload)
126
126
 
127
127
  cb(reply, payload)
@@ -23,35 +23,36 @@
23
23
  ? input.toJSON()
24
24
  : input
25
25
 
26
-
27
- let addComma = false
28
- let json = '{'
29
-
30
- if (obj["statusCode"] !== undefined) {
31
- !addComma && (addComma = true) || (json += ',')
32
- json += "\"statusCode\":"
33
- json += serializer.asNumber(obj["statusCode"])
34
- }
35
-
36
- if (obj["code"] !== undefined) {
37
- !addComma && (addComma = true) || (json += ',')
38
- json += "\"code\":"
39
- json += serializer.asString(obj["code"])
40
- }
41
-
42
- if (obj["error"] !== undefined) {
43
- !addComma && (addComma = true) || (json += ',')
44
- json += "\"error\":"
45
- json += serializer.asString(obj["error"])
46
- }
47
-
48
- if (obj["message"] !== undefined) {
49
- !addComma && (addComma = true) || (json += ',')
50
- json += "\"message\":"
51
- json += serializer.asString(obj["message"])
52
- }
53
-
54
- return json + '}'
26
+ if (obj === null) return '{}'
27
+
28
+ let json = '{'
29
+ let addComma = false
30
+
31
+ if (obj["statusCode"] !== undefined) {
32
+ !addComma && (addComma = true) || (json += ',')
33
+ json += "\"statusCode\":"
34
+ json += serializer.asNumber(obj["statusCode"])
35
+ }
36
+
37
+ if (obj["code"] !== undefined) {
38
+ !addComma && (addComma = true) || (json += ',')
39
+ json += "\"code\":"
40
+ json += serializer.asString(obj["code"])
41
+ }
42
+
43
+ if (obj["error"] !== undefined) {
44
+ !addComma && (addComma = true) || (json += ',')
45
+ json += "\"error\":"
46
+ json += serializer.asString(obj["error"])
47
+ }
48
+
49
+ if (obj["message"] !== undefined) {
50
+ !addComma && (addComma = true) || (json += ',')
51
+ json += "\"message\":"
52
+ json += serializer.asString(obj["message"])
53
+ }
54
+
55
+ return json + '}'
55
56
 
56
57
  }
57
58
 
package/lib/errors.js CHANGED
@@ -212,6 +212,10 @@ const codes = {
212
212
  500,
213
213
  TypeError
214
214
  ),
215
+ FST_ERR_REP_RESPONSE_BODY_CONSUMED: createError(
216
+ 'FST_ERR_REP_RESPONSE_BODY_CONSUMED',
217
+ 'Response.body is already consumed.'
218
+ ),
215
219
  FST_ERR_REP_ALREADY_SENT: createError(
216
220
  'FST_ERR_REP_ALREADY_SENT',
217
221
  'Reply was already sent, did you forget to "return reply" in "%s" (%s)?'
@@ -460,5 +464,6 @@ module.exports.AVVIO_ERRORS_MAP = {
460
464
  AVV_ERR_PLUGIN_NOT_VALID: codes.FST_ERR_PLUGIN_NOT_VALID,
461
465
  AVV_ERR_ROOT_PLG_BOOTED: codes.FST_ERR_ROOT_PLG_BOOTED,
462
466
  AVV_ERR_PARENT_PLG_LOADED: codes.FST_ERR_PARENT_PLUGIN_BOOTED,
463
- AVV_ERR_READY_TIMEOUT: codes.FST_ERR_PLUGIN_TIMEOUT
467
+ AVV_ERR_READY_TIMEOUT: codes.FST_ERR_PLUGIN_TIMEOUT,
468
+ AVV_ERR_PLUGIN_EXEC_TIMEOUT: codes.FST_ERR_PLUGIN_TIMEOUT
464
469
  }
package/lib/fourOhFour.js CHANGED
@@ -19,6 +19,7 @@ const {
19
19
  FST_ERR_NOT_FOUND
20
20
  } = require('./errors')
21
21
  const { createChildLogger } = require('./logger')
22
+ const { getGenReqId } = require('./reqIdGenFactory.js')
22
23
 
23
24
  /**
24
25
  * Each fastify instance have a:
@@ -28,7 +29,7 @@ const { createChildLogger } = require('./logger')
28
29
  * kFourOhFourContext: the context in the reply object where the handler will be executed
29
30
  */
30
31
  function fourOhFour (options) {
31
- const { logger, genReqId } = options
32
+ const { logger } = options
32
33
 
33
34
  // 404 router, used for handling encapsulated 404 handlers
34
35
  const router = FindMyWay({ onBadUrl: createOnBadUrl(), defaultRoute: fourOhFourFallBack })
@@ -58,8 +59,8 @@ function fourOhFour (options) {
58
59
 
59
60
  function createOnBadUrl () {
60
61
  return function onBadUrl (path, req, res) {
61
- const id = genReqId(req)
62
62
  const fourOhFourContext = this[kFourOhFourLevelInstance][kFourOhFourContext]
63
+ const id = getGenReqId(fourOhFourContext.server, req)
63
64
  const childLogger = createChildLogger(fourOhFourContext, logger, req, id)
64
65
  const request = new Request(id, null, req, null, childLogger, fourOhFourContext)
65
66
  const reply = new Reply(res, request, childLogger)
@@ -166,8 +167,8 @@ function fourOhFour (options) {
166
167
  // we might want to do some hard debugging
167
168
  // here, let's print out as much info as
168
169
  // we can
169
- const id = genReqId(req)
170
170
  const fourOhFourContext = this[kFourOhFourLevelInstance][kFourOhFourContext]
171
+ const id = getGenReqId(fourOhFourContext.server, req)
171
172
  const childLogger = createChildLogger(fourOhFourContext, logger, req, id)
172
173
 
173
174
  childLogger.info({ req }, 'incoming request')
package/lib/hooks.js CHANGED
@@ -181,10 +181,6 @@ function onListenHookRunner (server) {
181
181
  const hooks = server[kHooks].onListen
182
182
  const hooksLen = hooks.length
183
183
 
184
- if (hooksLen === 0) {
185
- return
186
- }
187
-
188
184
  let i = 0
189
185
  let c = 0
190
186
 
@@ -196,7 +192,7 @@ function onListenHookRunner (server) {
196
192
  if (
197
193
  i === hooksLen
198
194
  ) {
199
- if (c < server[kChildren].length) {
195
+ while (c < server[kChildren].length) {
200
196
  const child = server[kChildren][c++]
201
197
  onListenHookRunner(child)
202
198
  }
package/lib/reply.js CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const eos = require('node:stream').finished
4
+ const Readable = require('node:stream').Readable
4
5
 
5
6
  const {
6
7
  kFourOhFourContext,
@@ -44,6 +45,7 @@ const CONTENT_TYPE = {
44
45
  }
45
46
  const {
46
47
  FST_ERR_REP_INVALID_PAYLOAD_TYPE,
48
+ FST_ERR_REP_RESPONSE_BODY_CONSUMED,
47
49
  FST_ERR_REP_ALREADY_SENT,
48
50
  FST_ERR_REP_SENT_VALUE,
49
51
  FST_ERR_SEND_INSIDE_ONERR,
@@ -53,7 +55,9 @@ const {
53
55
  FST_ERR_MISSING_SERIALIZATION_FN,
54
56
  FST_ERR_MISSING_CONTENTTYPE_SERIALIZATION_FN
55
57
  } = require('./errors')
56
- const { FSTDEP010, FSTDEP013, FSTDEP019 } = require('./warnings')
58
+ const { FSTDEP010, FSTDEP013, FSTDEP019, FSTDEP020 } = require('./warnings')
59
+
60
+ const toString = Object.prototype.toString
57
61
 
58
62
  function Reply (res, request, log) {
59
63
  this.raw = res
@@ -84,6 +88,14 @@ Object.defineProperties(Reply.prototype, {
84
88
  return this.request[kRouteContext]
85
89
  }
86
90
  },
91
+ elapsedTime: {
92
+ get () {
93
+ if (this[kReplyStartTime] === undefined) {
94
+ return 0
95
+ }
96
+ return (this[kReplyEndTime] || now()) - this[kReplyStartTime]
97
+ }
98
+ },
87
99
  server: {
88
100
  get () {
89
101
  return this.request[kRouteContext].server
@@ -155,7 +167,14 @@ Reply.prototype.send = function (payload) {
155
167
  const hasContentType = contentType !== undefined
156
168
 
157
169
  if (payload !== null) {
158
- if (typeof payload.pipe === 'function') {
170
+ if (
171
+ // node:stream
172
+ typeof payload.pipe === 'function' ||
173
+ // node:stream/web
174
+ typeof payload.getReader === 'function' ||
175
+ // Response
176
+ toString.call(payload) === '[object Response]'
177
+ ) {
159
178
  onSendHook(this, payload)
160
179
  return this
161
180
  }
@@ -452,14 +471,11 @@ Reply.prototype.callNotFound = function () {
452
471
  return this
453
472
  }
454
473
 
474
+ // TODO: should be removed in fastify@5
455
475
  Reply.prototype.getResponseTime = function () {
456
- let responseTime = 0
476
+ FSTDEP020()
457
477
 
458
- if (this[kReplyStartTime] !== undefined) {
459
- responseTime = (this[kReplyEndTime] || now()) - this[kReplyStartTime]
460
- }
461
-
462
- return responseTime
478
+ return this.elapsedTime
463
479
  }
464
480
 
465
481
  // Make reply a thenable, so it could be used with async/await.
@@ -565,7 +581,6 @@ function safeWriteHead (reply, statusCode) {
565
581
  function onSendEnd (reply, payload) {
566
582
  const res = reply.raw
567
583
  const req = reply.request
568
- const statusCode = res.statusCode
569
584
 
570
585
  // we check if we need to update the trailers header and set it
571
586
  if (reply[kReplyTrailers] !== null) {
@@ -581,6 +596,17 @@ function onSendEnd (reply, payload) {
581
596
  reply.header('Trailer', header.trim())
582
597
  }
583
598
 
599
+ // since Response contain status code, we need to update before
600
+ // any action that used statusCode
601
+ const isResponse = toString.call(payload) === '[object Response]'
602
+ if (isResponse) {
603
+ // https://developer.mozilla.org/en-US/docs/Web/API/Response/status
604
+ if (typeof payload.status === 'number') {
605
+ reply.code(payload.status)
606
+ }
607
+ }
608
+ const statusCode = res.statusCode
609
+
584
610
  if (payload === undefined || payload === null) {
585
611
  // according to https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.2
586
612
  // we cannot send a content-length for 304 and 204, and all status code
@@ -612,11 +638,38 @@ function onSendEnd (reply, payload) {
612
638
  return
613
639
  }
614
640
 
641
+ // node:stream
615
642
  if (typeof payload.pipe === 'function') {
616
643
  sendStream(payload, res, reply)
617
644
  return
618
645
  }
619
646
 
647
+ // node:stream/web
648
+ if (typeof payload.getReader === 'function') {
649
+ sendWebStream(payload, res, reply)
650
+ return
651
+ }
652
+
653
+ // Response
654
+ if (isResponse) {
655
+ // https://developer.mozilla.org/en-US/docs/Web/API/Response/headers
656
+ if (typeof payload.headers === 'object' && typeof payload.headers.forEach === 'function') {
657
+ for (const [headerName, headerValue] of payload.headers) {
658
+ reply.header(headerName, headerValue)
659
+ }
660
+ }
661
+
662
+ // https://developer.mozilla.org/en-US/docs/Web/API/Response/body
663
+ if (payload.body != null) {
664
+ if (payload.bodyUsed) {
665
+ throw new FST_ERR_REP_RESPONSE_BODY_CONSUMED()
666
+ }
667
+ // Response.body always a ReadableStream
668
+ sendWebStream(payload.body, res, reply)
669
+ }
670
+ return
671
+ }
672
+
620
673
  if (typeof payload !== 'string' && !Buffer.isBuffer(payload)) {
621
674
  throw new FST_ERR_REP_INVALID_PAYLOAD_TYPE(typeof payload)
622
675
  }
@@ -649,6 +702,11 @@ function logStreamError (logger, err, res) {
649
702
  }
650
703
  }
651
704
 
705
+ function sendWebStream (payload, res, reply) {
706
+ const nodeStream = Readable.fromWeb(payload)
707
+ sendStream(nodeStream, res, reply)
708
+ }
709
+
652
710
  function sendStream (payload, res, reply) {
653
711
  let sourceOpen = true
654
712
  let errorLogged = false
@@ -813,7 +871,7 @@ function onResponseCallback (err, request, reply) {
813
871
  return
814
872
  }
815
873
 
816
- const responseTime = reply.getResponseTime()
874
+ const responseTime = reply.elapsedTime
817
875
 
818
876
  if (err != null) {
819
877
  reply.log.error({
@@ -21,6 +21,10 @@ function reqIdGenFactory (requestIdHeader, optGenReqId) {
21
21
  return genReqId
22
22
  }
23
23
 
24
+ function getGenReqId (contextServer, req) {
25
+ return contextServer.genReqId(req)
26
+ }
27
+
24
28
  function buildDefaultGenReqId () {
25
29
  // 2,147,483,647 (2^31 − 1) stands for max SMI value (an internal optimization of V8).
26
30
  // With this upper bound, if you'll be generating 1k ids/sec, you're going to hit it in ~25 days.
@@ -43,5 +47,6 @@ function buildOptionalHeaderReqId (requestIdHeader, genReqId) {
43
47
  }
44
48
 
45
49
  module.exports = {
50
+ getGenReqId,
46
51
  reqIdGenFactory
47
52
  }
package/lib/route.js CHANGED
@@ -56,6 +56,7 @@ const {
56
56
  } = require('./symbols.js')
57
57
  const { buildErrorHandler } = require('./error-handler')
58
58
  const { createChildLogger } = require('./logger')
59
+ const { getGenReqId } = require('./reqIdGenFactory.js')
59
60
 
60
61
  function buildRouting (options) {
61
62
  const router = FindMyWay(options.config)
@@ -66,7 +67,6 @@ function buildRouting (options) {
66
67
  let hasLogger
67
68
  let setupResponseListeners
68
69
  let throwIfAlreadyStarted
69
- let genReqId
70
70
  let disableRequestLogging
71
71
  let ignoreTrailingSlash
72
72
  let ignoreDuplicateSlashes
@@ -92,7 +92,6 @@ function buildRouting (options) {
92
92
  validateHTTPVersion = fastifyArgs.validateHTTPVersion
93
93
 
94
94
  globalExposeHeadRoutes = options.exposeHeadRoutes
95
- genReqId = options.genReqId
96
95
  disableRequestLogging = options.disableRequestLogging
97
96
  ignoreTrailingSlash = options.ignoreTrailingSlash
98
97
  ignoreDuplicateSlashes = options.ignoreDuplicateSlashes
@@ -120,7 +119,8 @@ function buildRouting (options) {
120
119
  printRoutes: router.prettyPrint.bind(router),
121
120
  addConstraintStrategy,
122
121
  hasConstraintStrategy,
123
- isAsyncConstraint
122
+ isAsyncConstraint,
123
+ findRoute
124
124
  }
125
125
 
126
126
  function addConstraintStrategy (strategy) {
@@ -168,11 +168,27 @@ function buildRouting (options) {
168
168
  }
169
169
 
170
170
  function hasRoute ({ options }) {
171
- return router.find(
171
+ return findRoute(options) !== null
172
+ }
173
+
174
+ function findRoute (options) {
175
+ const route = router.find(
172
176
  options.method,
173
177
  options.url || '',
174
178
  options.constraints
175
- ) !== null
179
+ )
180
+ if (route) {
181
+ // we must reduce the expose surface, otherwise
182
+ // we provide the ability for the user to modify
183
+ // all the route and server information in runtime
184
+ return {
185
+ handler: route.handler,
186
+ params: route.params,
187
+ searchParams: route.searchParams
188
+ }
189
+ } else {
190
+ return null
191
+ }
176
192
  }
177
193
 
178
194
  /**
@@ -428,7 +444,7 @@ function buildRouting (options) {
428
444
 
429
445
  // HTTP request entry point, the routing has already been executed
430
446
  function routeHandler (req, res, params, context, query) {
431
- const id = genReqId(req)
447
+ const id = getGenReqId(context.server, req)
432
448
 
433
449
  const loggerOpts = {
434
450
  level: context.logLevel