fastify 6.0.0-alpha.4 → 6.0.0-alpha.6

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 (63) hide show
  1. package/README.md +1 -0
  2. package/SECURITY.md +6 -0
  3. package/build/build-validation.js +2 -0
  4. package/docs/Guides/Ecosystem.md +12 -53
  5. package/docs/Guides/Migration-Guide-V5.md +2 -2
  6. package/docs/Guides/Serverless.md +33 -10
  7. package/docs/Reference/Errors.md +13 -2
  8. package/docs/Reference/Hooks.md +13 -0
  9. package/docs/Reference/Reply.md +787 -681
  10. package/docs/Reference/Request.md +308 -104
  11. package/docs/Reference/Routes.md +6 -2
  12. package/docs/Reference/Server.md +41 -4
  13. package/docs/Reference/Validation-and-Serialization.md +48 -1
  14. package/docs/Reference/Warnings.md +2 -0
  15. package/fastify.d.ts +1 -0
  16. package/fastify.js +35 -1
  17. package/lib/config-validator.js +102 -73
  18. package/lib/content-type-parser.js +35 -14
  19. package/lib/content-type.js +4 -4
  20. package/lib/context.js +2 -1
  21. package/lib/decorate.js +4 -3
  22. package/lib/error-handler.js +21 -4
  23. package/lib/error-status.js +4 -2
  24. package/lib/errors.js +15 -14
  25. package/lib/handle-request.js +7 -20
  26. package/lib/reply.js +18 -13
  27. package/lib/request.js +12 -7
  28. package/lib/route.js +40 -7
  29. package/lib/schemas.js +44 -32
  30. package/lib/validation.js +67 -10
  31. package/lib/warnings.js +10 -1
  32. package/package.json +8 -3
  33. package/test/child-logger-factory.test.js +25 -0
  34. package/test/close-disposable.test.js +154 -0
  35. package/test/constrained-routes.test.js +4 -2
  36. package/test/content-parser.test.js +99 -0
  37. package/test/decorator.test.js +29 -0
  38. package/test/diagnostics-channel/error-status.test.js +35 -2
  39. package/test/handler-timeout.test.js +224 -0
  40. package/test/internals/all.test.js +5 -0
  41. package/test/internals/errors.test.js +19 -20
  42. package/test/internals/initial-config.test.js +2 -0
  43. package/test/internals/reply-serialize.test.js +20 -0
  44. package/test/internals/reply.test.js +30 -1
  45. package/test/internals/request.test.js +50 -0
  46. package/test/internals/server.test.js +6 -4
  47. package/test/reply-error.test.js +82 -0
  48. package/test/reply-media-type.test.js +57 -0
  49. package/test/reply-trailers.test.js +159 -1
  50. package/test/rfc-10008.test.js +6 -9
  51. package/test/router-compile.test.js +241 -0
  52. package/test/schema-feature.test.js +1 -1
  53. package/test/schema-serialization.test.js +120 -7
  54. package/test/schema-validation.test.js +251 -1
  55. package/test/types/errors.tst.ts +0 -1
  56. package/test/types/fastify.tst.ts +9 -0
  57. package/test/types/instance.tst.ts +1 -1
  58. package/test/types/route.tst.ts +37 -0
  59. package/test/types/using.tst.ts +20 -0
  60. package/test/validation-error-handling.test.js +21 -1
  61. package/types/errors.d.ts +0 -1
  62. package/types/instance.d.ts +9 -5
  63. package/types/route.d.ts +4 -2
package/fastify.d.ts CHANGED
@@ -96,6 +96,7 @@ declare namespace fastify {
96
96
  type TrustProxyFunction = (address: string, hop: number) => boolean
97
97
 
98
98
  export type FastifyRouterOptions<RawServer extends RawServerBase> = Omit<FindMyWayConfigForServer<RawServer>, 'defaultRoute' | 'onBadUrl' | 'onMaxParamLength' | 'querystringParser' | 'constraints'> & {
99
+ compile?: boolean,
99
100
  constraints?: {
100
101
  [name: string]: ConstraintStrategy<FindMyWayVersion<RawServer>, unknown>,
101
102
  },
package/fastify.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const VERSION = '6.0.0-alpha.4'
3
+ const VERSION = '6.0.0-alpha.6'
4
4
 
5
5
  const Avvio = require('avvio')
6
6
  const http = require('node:http')
@@ -83,6 +83,23 @@ const { buildErrorHandler } = require('./lib/error-handler.js')
83
83
 
84
84
  const initChannel = diagnostics.channel('fastify.initialization')
85
85
 
86
+ function asyncDisposableHook (disposable) {
87
+ return function (instance, done) {
88
+ disposable[Symbol.asyncDispose]().then(() => done(), done)
89
+ }
90
+ }
91
+
92
+ function disposableHook (disposable) {
93
+ return function (instance, done) {
94
+ try {
95
+ disposable[Symbol.dispose]()
96
+ done()
97
+ } catch (err) {
98
+ done(err)
99
+ }
100
+ }
101
+ }
102
+
86
103
  /**
87
104
  * @param {import('./fastify.js').FastifyServerOptions} serverOptions
88
105
  */
@@ -381,6 +398,13 @@ function fastify (serverOptions) {
381
398
 
382
399
  // cache the closing value, since we are checking it in an hot path
383
400
  avvio.once('preReady', () => {
401
+ // All the routes are registered at this point. Routes added or removed
402
+ // afterwards make find-my-way compile the changed trees again lazily.
403
+ if (options.routerOptions.compile === true) {
404
+ router.compileRoutes()
405
+ fourOhFour.router.compile()
406
+ }
407
+
384
408
  fastify.onClose((instance, done) => {
385
409
  fastify[kState].closing = true
386
410
  router.closeRoutes()
@@ -598,6 +622,16 @@ function fastify (serverOptions) {
598
622
  }
599
623
  }
600
624
 
625
+ if (name === 'onClose' && typeof fn !== 'function') {
626
+ if (typeof fn[Symbol.asyncDispose] === 'function') {
627
+ fn = asyncDisposableHook(fn)
628
+ } else if (typeof fn[Symbol.dispose] === 'function') {
629
+ fn = disposableHook(fn)
630
+ } else {
631
+ throw new errorCodes.FST_ERR_HOOK_INVALID_HANDLER(name, Object.prototype.toString.call(fn))
632
+ }
633
+ }
634
+
601
635
  if (name === 'onClose') {
602
636
  this.onClose(fn.bind(this))
603
637
  } else if (name === 'onReady' || name === 'onListen' || name === 'onRoute' || name === 'preClose') {
@@ -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},"handlerTimeout":{"type":"integer","default":0},"bodyLimit":{"type":"integer","default":1048576},"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"},"ignoreDuplicateSlashes":{"type":"boolean"},"onProtoPoisoning":{"type":"string","default":"error"},"onConstructorPoisoning":{"type":"string","default":"error"},"pluginTimeout":{"type":"integer","default":10000},"requestIdHeader":{"anyOf":[{"type":"boolean"},{"type":"string"}],"default":false},"http2SessionTimeout":{"type":"integer","default":72000},"exposeHeadRoutes":{"type":"boolean","default":true},"routerOptions":{"type":"object","additionalProperties":false,"properties":{"allowUnsafeRegex":{"type":"boolean","default":false},"caseSensitive":{"type":"boolean","default":true},"constraints":{"type":"object","additionalProperties":{"type":"object","required":["name","storage","validate","deriveConstraint"],"additionalProperties":true,"properties":{"name":{"type":"string"},"storage":{},"validate":{},"deriveConstraint":{}}}},"ignoreTrailingSlash":{"type":"boolean","default":false},"ignoreDuplicateSlashes":{"type":"boolean","default":false},"maxParamLength":{"type":"integer","default":100},"useSemicolonDelimiter":{"type":"boolean","default":false}}}}};
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},"handlerTimeout":{"type":"integer","default":0},"bodyLimit":{"type":"integer","default":1048576},"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"},"ignoreDuplicateSlashes":{"type":"boolean"},"onProtoPoisoning":{"type":"string","default":"error"},"onConstructorPoisoning":{"type":"string","default":"error"},"pluginTimeout":{"type":"integer","default":10000},"requestIdHeader":{"anyOf":[{"type":"boolean"},{"type":"string"}],"default":false},"http2SessionTimeout":{"type":"integer","default":72000},"exposeHeadRoutes":{"type":"boolean","default":true},"routerOptions":{"type":"object","additionalProperties":false,"properties":{"allowUnsafeRegex":{"type":"boolean","default":false},"caseSensitive":{"type":"boolean","default":true},"compile":{"type":"boolean","default":false},"constraints":{"type":"object","additionalProperties":{"type":"object","required":["name","storage","validate","deriveConstraint"],"additionalProperties":true,"properties":{"name":{"type":"string"},"storage":{},"validate":{},"deriveConstraint":{}}}},"ignoreTrailingSlash":{"type":"boolean","default":false},"ignoreDuplicateSlashes":{"type":"boolean","default":false},"maxParamLength":{"type":"integer","default":100},"useSemicolonDelimiter":{"type":"boolean","default":false}}}}};
7
7
  const func2 = Object.prototype.hasOwnProperty;
8
8
  const pattern0 = new RegExp("idle", "u");
9
9
 
@@ -876,6 +876,9 @@ data18.allowUnsafeRegex = false;
876
876
  if(data18.caseSensitive === undefined){
877
877
  data18.caseSensitive = true;
878
878
  }
879
+ if(data18.compile === undefined){
880
+ data18.compile = false;
881
+ }
879
882
  if(data18.ignoreTrailingSlash === undefined){
880
883
  data18.ignoreTrailingSlash = false;
881
884
  }
@@ -890,7 +893,7 @@ data18.useSemicolonDelimiter = false;
890
893
  }
891
894
  const _errs61 = errors;
892
895
  for(const key2 in data18){
893
- if(!(((((((key2 === "allowUnsafeRegex") || (key2 === "caseSensitive")) || (key2 === "constraints")) || (key2 === "ignoreTrailingSlash")) || (key2 === "ignoreDuplicateSlashes")) || (key2 === "maxParamLength")) || (key2 === "useSemicolonDelimiter"))){
896
+ if(!((((((((key2 === "allowUnsafeRegex") || (key2 === "caseSensitive")) || (key2 === "compile")) || (key2 === "constraints")) || (key2 === "ignoreTrailingSlash")) || (key2 === "ignoreDuplicateSlashes")) || (key2 === "maxParamLength")) || (key2 === "useSemicolonDelimiter"))){
894
897
  delete data18[key2];
895
898
  }
896
899
  }
@@ -945,43 +948,68 @@ data18["caseSensitive"] = coerced22;
945
948
  }
946
949
  var valid7 = _errs64 === errors;
947
950
  if(valid7){
948
- if(data18.constraints !== undefined){
949
- let data21 = data18.constraints;
951
+ let data21 = data18.compile;
950
952
  const _errs66 = errors;
951
- if(errors === _errs66){
952
- if(data21 && typeof data21 == "object" && !Array.isArray(data21)){
953
- for(const key3 in data21){
954
- let data22 = data21[key3];
955
- const _errs69 = errors;
956
- if(errors === _errs69){
953
+ if(typeof data21 !== "boolean"){
954
+ let coerced23 = undefined;
955
+ if(!(coerced23 !== undefined)){
956
+ if(data21 === "false" || data21 === 0 || data21 === null){
957
+ coerced23 = false;
958
+ }
959
+ else if(data21 === "true" || data21 === 1){
960
+ coerced23 = true;
961
+ }
962
+ else {
963
+ validate10.errors = [{instancePath:instancePath+"/routerOptions/compile",schemaPath:"#/properties/routerOptions/properties/compile/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}];
964
+ return false;
965
+ }
966
+ }
967
+ if(coerced23 !== undefined){
968
+ data21 = coerced23;
969
+ if(data18 !== undefined){
970
+ data18["compile"] = coerced23;
971
+ }
972
+ }
973
+ }
974
+ var valid7 = _errs66 === errors;
975
+ if(valid7){
976
+ if(data18.constraints !== undefined){
977
+ let data22 = data18.constraints;
978
+ const _errs68 = errors;
979
+ if(errors === _errs68){
957
980
  if(data22 && typeof data22 == "object" && !Array.isArray(data22)){
981
+ for(const key3 in data22){
982
+ let data23 = data22[key3];
983
+ const _errs71 = errors;
984
+ if(errors === _errs71){
985
+ if(data23 && typeof data23 == "object" && !Array.isArray(data23)){
958
986
  let missing1;
959
- if(((((data22.name === undefined) && (missing1 = "name")) || ((data22.storage === undefined) && (missing1 = "storage"))) || ((data22.validate === undefined) && (missing1 = "validate"))) || ((data22.deriveConstraint === undefined) && (missing1 = "deriveConstraint"))){
987
+ if(((((data23.name === undefined) && (missing1 = "name")) || ((data23.storage === undefined) && (missing1 = "storage"))) || ((data23.validate === undefined) && (missing1 = "validate"))) || ((data23.deriveConstraint === undefined) && (missing1 = "deriveConstraint"))){
960
988
  validate10.errors = [{instancePath:instancePath+"/routerOptions/constraints/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/routerOptions/properties/constraints/additionalProperties/required",keyword:"required",params:{missingProperty: missing1},message:"must have required property '"+missing1+"'"}];
961
989
  return false;
962
990
  }
963
991
  else {
964
- if(data22.name !== undefined){
965
- let data23 = data22.name;
966
- if(typeof data23 !== "string"){
967
- let dataType23 = typeof data23;
968
- let coerced23 = undefined;
969
- if(!(coerced23 !== undefined)){
970
- if(dataType23 == "number" || dataType23 == "boolean"){
971
- coerced23 = "" + data23;
992
+ if(data23.name !== undefined){
993
+ let data24 = data23.name;
994
+ if(typeof data24 !== "string"){
995
+ let dataType24 = typeof data24;
996
+ let coerced24 = undefined;
997
+ if(!(coerced24 !== undefined)){
998
+ if(dataType24 == "number" || dataType24 == "boolean"){
999
+ coerced24 = "" + data24;
972
1000
  }
973
- else if(data23 === null){
974
- coerced23 = "";
1001
+ else if(data24 === null){
1002
+ coerced24 = "";
975
1003
  }
976
1004
  else {
977
1005
  validate10.errors = [{instancePath:instancePath+"/routerOptions/constraints/" + key3.replace(/~/g, "~0").replace(/\//g, "~1")+"/name",schemaPath:"#/properties/routerOptions/properties/constraints/additionalProperties/properties/name/type",keyword:"type",params:{type: "string"},message:"must be string"}];
978
1006
  return false;
979
1007
  }
980
1008
  }
981
- if(coerced23 !== undefined){
982
- data23 = coerced23;
983
- if(data22 !== undefined){
984
- data22["name"] = coerced23;
1009
+ if(coerced24 !== undefined){
1010
+ data24 = coerced24;
1011
+ if(data23 !== undefined){
1012
+ data23["name"] = coerced24;
985
1013
  }
986
1014
  }
987
1015
  }
@@ -993,7 +1021,7 @@ validate10.errors = [{instancePath:instancePath+"/routerOptions/constraints/" +
993
1021
  return false;
994
1022
  }
995
1023
  }
996
- var valid8 = _errs69 === errors;
1024
+ var valid8 = _errs71 === errors;
997
1025
  if(!valid8){
998
1026
  break;
999
1027
  }
@@ -1004,38 +1032,13 @@ validate10.errors = [{instancePath:instancePath+"/routerOptions/constraints",sch
1004
1032
  return false;
1005
1033
  }
1006
1034
  }
1007
- var valid7 = _errs66 === errors;
1035
+ var valid7 = _errs68 === errors;
1008
1036
  }
1009
1037
  else {
1010
1038
  var valid7 = true;
1011
1039
  }
1012
1040
  if(valid7){
1013
- let data24 = data18.ignoreTrailingSlash;
1014
- const _errs74 = errors;
1015
- if(typeof data24 !== "boolean"){
1016
- let coerced24 = undefined;
1017
- if(!(coerced24 !== undefined)){
1018
- if(data24 === "false" || data24 === 0 || data24 === null){
1019
- coerced24 = false;
1020
- }
1021
- else if(data24 === "true" || data24 === 1){
1022
- coerced24 = true;
1023
- }
1024
- else {
1025
- validate10.errors = [{instancePath:instancePath+"/routerOptions/ignoreTrailingSlash",schemaPath:"#/properties/routerOptions/properties/ignoreTrailingSlash/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}];
1026
- return false;
1027
- }
1028
- }
1029
- if(coerced24 !== undefined){
1030
- data24 = coerced24;
1031
- if(data18 !== undefined){
1032
- data18["ignoreTrailingSlash"] = coerced24;
1033
- }
1034
- }
1035
- }
1036
- var valid7 = _errs74 === errors;
1037
- if(valid7){
1038
- let data25 = data18.ignoreDuplicateSlashes;
1041
+ let data25 = data18.ignoreTrailingSlash;
1039
1042
  const _errs76 = errors;
1040
1043
  if(typeof data25 !== "boolean"){
1041
1044
  let coerced25 = undefined;
@@ -1047,67 +1050,93 @@ else if(data25 === "true" || data25 === 1){
1047
1050
  coerced25 = true;
1048
1051
  }
1049
1052
  else {
1050
- validate10.errors = [{instancePath:instancePath+"/routerOptions/ignoreDuplicateSlashes",schemaPath:"#/properties/routerOptions/properties/ignoreDuplicateSlashes/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}];
1053
+ validate10.errors = [{instancePath:instancePath+"/routerOptions/ignoreTrailingSlash",schemaPath:"#/properties/routerOptions/properties/ignoreTrailingSlash/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}];
1051
1054
  return false;
1052
1055
  }
1053
1056
  }
1054
1057
  if(coerced25 !== undefined){
1055
1058
  data25 = coerced25;
1056
1059
  if(data18 !== undefined){
1057
- data18["ignoreDuplicateSlashes"] = coerced25;
1060
+ data18["ignoreTrailingSlash"] = coerced25;
1058
1061
  }
1059
1062
  }
1060
1063
  }
1061
1064
  var valid7 = _errs76 === errors;
1062
1065
  if(valid7){
1063
- let data26 = data18.maxParamLength;
1066
+ let data26 = data18.ignoreDuplicateSlashes;
1064
1067
  const _errs78 = errors;
1065
- if(!(((typeof data26 == "number") && (!(data26 % 1) && !isNaN(data26))) && (isFinite(data26)))){
1066
- let dataType26 = typeof data26;
1068
+ if(typeof data26 !== "boolean"){
1067
1069
  let coerced26 = undefined;
1068
1070
  if(!(coerced26 !== undefined)){
1069
- if(dataType26 === "boolean" || data26 === null
1070
- || (dataType26 === "string" && data26 && data26 == +data26 && !(data26 % 1))){
1071
- coerced26 = +data26;
1071
+ if(data26 === "false" || data26 === 0 || data26 === null){
1072
+ coerced26 = false;
1073
+ }
1074
+ else if(data26 === "true" || data26 === 1){
1075
+ coerced26 = true;
1072
1076
  }
1073
1077
  else {
1074
- validate10.errors = [{instancePath:instancePath+"/routerOptions/maxParamLength",schemaPath:"#/properties/routerOptions/properties/maxParamLength/type",keyword:"type",params:{type: "integer"},message:"must be integer"}];
1078
+ validate10.errors = [{instancePath:instancePath+"/routerOptions/ignoreDuplicateSlashes",schemaPath:"#/properties/routerOptions/properties/ignoreDuplicateSlashes/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}];
1075
1079
  return false;
1076
1080
  }
1077
1081
  }
1078
1082
  if(coerced26 !== undefined){
1079
1083
  data26 = coerced26;
1080
1084
  if(data18 !== undefined){
1081
- data18["maxParamLength"] = coerced26;
1085
+ data18["ignoreDuplicateSlashes"] = coerced26;
1082
1086
  }
1083
1087
  }
1084
1088
  }
1085
1089
  var valid7 = _errs78 === errors;
1086
1090
  if(valid7){
1087
- let data27 = data18.useSemicolonDelimiter;
1091
+ let data27 = data18.maxParamLength;
1088
1092
  const _errs80 = errors;
1089
- if(typeof data27 !== "boolean"){
1093
+ if(!(((typeof data27 == "number") && (!(data27 % 1) && !isNaN(data27))) && (isFinite(data27)))){
1094
+ let dataType27 = typeof data27;
1090
1095
  let coerced27 = undefined;
1091
1096
  if(!(coerced27 !== undefined)){
1092
- if(data27 === "false" || data27 === 0 || data27 === null){
1093
- coerced27 = false;
1094
- }
1095
- else if(data27 === "true" || data27 === 1){
1096
- coerced27 = true;
1097
+ if(dataType27 === "boolean" || data27 === null
1098
+ || (dataType27 === "string" && data27 && data27 == +data27 && !(data27 % 1))){
1099
+ coerced27 = +data27;
1097
1100
  }
1098
1101
  else {
1099
- validate10.errors = [{instancePath:instancePath+"/routerOptions/useSemicolonDelimiter",schemaPath:"#/properties/routerOptions/properties/useSemicolonDelimiter/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}];
1102
+ validate10.errors = [{instancePath:instancePath+"/routerOptions/maxParamLength",schemaPath:"#/properties/routerOptions/properties/maxParamLength/type",keyword:"type",params:{type: "integer"},message:"must be integer"}];
1100
1103
  return false;
1101
1104
  }
1102
1105
  }
1103
1106
  if(coerced27 !== undefined){
1104
1107
  data27 = coerced27;
1105
1108
  if(data18 !== undefined){
1106
- data18["useSemicolonDelimiter"] = coerced27;
1109
+ data18["maxParamLength"] = coerced27;
1107
1110
  }
1108
1111
  }
1109
1112
  }
1110
1113
  var valid7 = _errs80 === errors;
1114
+ if(valid7){
1115
+ let data28 = data18.useSemicolonDelimiter;
1116
+ const _errs82 = errors;
1117
+ if(typeof data28 !== "boolean"){
1118
+ let coerced28 = undefined;
1119
+ if(!(coerced28 !== undefined)){
1120
+ if(data28 === "false" || data28 === 0 || data28 === null){
1121
+ coerced28 = false;
1122
+ }
1123
+ else if(data28 === "true" || data28 === 1){
1124
+ coerced28 = true;
1125
+ }
1126
+ else {
1127
+ validate10.errors = [{instancePath:instancePath+"/routerOptions/useSemicolonDelimiter",schemaPath:"#/properties/routerOptions/properties/useSemicolonDelimiter/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}];
1128
+ return false;
1129
+ }
1130
+ }
1131
+ if(coerced28 !== undefined){
1132
+ data28 = coerced28;
1133
+ if(data18 !== undefined){
1134
+ data18["useSemicolonDelimiter"] = coerced28;
1135
+ }
1136
+ }
1137
+ }
1138
+ var valid7 = _errs82 === errors;
1139
+ }
1111
1140
  }
1112
1141
  }
1113
1142
  }
@@ -1155,5 +1184,5 @@ return errors === 0;
1155
1184
  }
1156
1185
 
1157
1186
 
1158
- module.exports.defaultInitOptions = {"connectionTimeout":0,"keepAliveTimeout":72000,"maxRequestsPerSocket":0,"requestTimeout":0,"handlerTimeout":0,"bodyLimit":1048576,"onProtoPoisoning":"error","onConstructorPoisoning":"error","pluginTimeout":10000,"requestIdHeader":false,"http2SessionTimeout":72000,"exposeHeadRoutes":true,"allowErrorHandlerOverride":false,"routerOptions":{"allowUnsafeRegex":false,"caseSensitive":true,"ignoreTrailingSlash":false,"ignoreDuplicateSlashes":false,"maxParamLength":100,"useSemicolonDelimiter":false}}
1187
+ module.exports.defaultInitOptions = {"connectionTimeout":0,"keepAliveTimeout":72000,"maxRequestsPerSocket":0,"requestTimeout":0,"handlerTimeout":0,"bodyLimit":1048576,"onProtoPoisoning":"error","onConstructorPoisoning":"error","pluginTimeout":10000,"requestIdHeader":false,"http2SessionTimeout":72000,"exposeHeadRoutes":true,"allowErrorHandlerOverride":false,"routerOptions":{"allowUnsafeRegex":false,"caseSensitive":true,"compile":false,"ignoreTrailingSlash":false,"ignoreDuplicateSlashes":false,"maxParamLength":100,"useSemicolonDelimiter":false}}
1159
1188
  /* c8 ignore stop */
@@ -222,7 +222,13 @@ ContentTypeParser.prototype.run = function (contentType, handler, request, reply
222
222
  return
223
223
  }
224
224
 
225
- const result = parser.fn(request, request[kRequestPayloadStream], done)
225
+ let result
226
+ try {
227
+ result = parser.fn(request, request[kRequestPayloadStream], done)
228
+ } catch (error) {
229
+ done(error, undefined)
230
+ return
231
+ }
226
232
  if (result && typeof result.then === 'function') {
227
233
  result.then(body => { done(null, body) }, done)
228
234
  }
@@ -253,12 +259,12 @@ function rawBody (request, reply, options, parser, done) {
253
259
  }
254
260
 
255
261
  let receivedLength = 0
256
- let body = asString ? '' : []
262
+ const chunks = []
257
263
  const payload = request[kRequestPayloadStream] || request.raw
258
264
 
259
- if (asString) {
260
- payload.setEncoding('utf8')
261
- }
265
+ // Always read raw Buffers and decode once at the end: setEncoding() would
266
+ // allocate a StringDecoder per request and force a Buffer.byteLength()
267
+ // rescan of every decoded chunk just to enforce the byte limit.
262
268
 
263
269
  payload.on('data', onData)
264
270
  payload.on('end', onEnd)
@@ -266,7 +272,11 @@ function rawBody (request, reply, options, parser, done) {
266
272
  payload.resume()
267
273
 
268
274
  function onData (chunk) {
269
- receivedLength += asString ? Buffer.byteLength(chunk) : chunk.length
275
+ // A custom preParsing stream may still emit strings
276
+ if (typeof chunk === 'string') {
277
+ chunk = Buffer.from(chunk)
278
+ }
279
+ receivedLength += chunk.length
270
280
  const { receivedEncodedLength = 0 } = payload
271
281
  // The resulting body length must not exceed bodyLimit (see "zip bomb").
272
282
  // The case when encoded length is larger than received length is rather theoretical,
@@ -279,11 +289,7 @@ function rawBody (request, reply, options, parser, done) {
279
289
  return
280
290
  }
281
291
 
282
- if (asString) {
283
- body += chunk
284
- } else {
285
- body.push(chunk)
286
- }
292
+ chunks.push(chunk)
287
293
  }
288
294
 
289
295
  function onEnd (err) {
@@ -304,11 +310,26 @@ function rawBody (request, reply, options, parser, done) {
304
310
  return
305
311
  }
306
312
 
307
- if (!asString) {
308
- body = Buffer.concat(body)
313
+ let body
314
+ if (asString) {
315
+ // Decoding the whole body at once also keeps multi-byte characters
316
+ // split across chunks intact
317
+ body = chunks.length === 1
318
+ ? chunks[0].toString('utf8')
319
+ : Buffer.concat(chunks, receivedLength).toString('utf8')
320
+ } else {
321
+ // Always copy so the parser never receives a view over the shared
322
+ // socket read buffer
323
+ body = Buffer.concat(chunks, receivedLength)
309
324
  }
310
325
 
311
- const result = parser.fn(request, body, done)
326
+ let result
327
+ try {
328
+ result = parser.fn(request, body, done)
329
+ } catch (error) {
330
+ done(error, undefined)
331
+ return
332
+ }
312
333
  if (result && typeof result.then === 'function') {
313
334
  result.then(body => { done(null, body) }, done)
314
335
  }
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const { LruMap: Lru } = require('toad-cache')
3
+ const { FifoMap: Fifo } = require('toad-cache')
4
4
 
5
5
  /**
6
6
  * keyValuePairsReg matches a single `parameter-name "=" parameter-value`
@@ -61,9 +61,9 @@ const subtypeNameReg = /^[\w!#$%&'*+.^`|~-]+\s*$/
61
61
 
62
62
  /**
63
63
  * Content-Type internal shared cache
64
- * @type {Lru<ContentType>}
64
+ * @type {Fifo<ContentType>}
65
65
  */
66
- const cache = new Lru(100)
66
+ const cache = new Fifo(100)
67
67
 
68
68
  /**
69
69
  * ContentType parses and represents the value of the content-type header.
@@ -82,7 +82,7 @@ class ContentType {
82
82
  /**
83
83
  * The shared cache of ContentType instances. The cache is used to avoid
84
84
  * creating multiple instances of ContentType for the same header value.
85
- * @type {Lru<ContentType>}
85
+ * @type {Fifo<ContentType>}
86
86
  */
87
87
  static get cache () { return cache }
88
88
 
package/lib/context.js CHANGED
@@ -16,6 +16,7 @@ const {
16
16
  kReplyCacheSerializeFns,
17
17
  kHandlerTimeout
18
18
  } = require('./symbols.js')
19
+ const { FST_ERR_VALIDATION } = require('./errors.js')
19
20
 
20
21
  // Object that holds the context of every request
21
22
  // Every route holds an instance of this object.
@@ -89,7 +90,7 @@ function defaultSchemaErrorFormatter (errors, dataVar) {
89
90
  const e = errors[i]
90
91
  text += dataVar + (e.instancePath || '') + ' ' + e.message + separator
91
92
  }
92
- return new Error(text.slice(0, -separator.length))
93
+ return new FST_ERR_VALIDATION(text.slice(0, -separator.length))
93
94
  }
94
95
 
95
96
  module.exports = Context
package/lib/decorate.js CHANGED
@@ -38,11 +38,12 @@ function getInstanceDecorator (name) {
38
38
  throw new FST_ERR_DEC_UNDECLARED(name, 'instance')
39
39
  }
40
40
 
41
- if (typeof this[name] === 'function') {
42
- return this[name].bind(this)
41
+ const decorator = this[name]
42
+ if (typeof decorator === 'function') {
43
+ return decorator.bind(this)
43
44
  }
44
45
 
45
- return this[name]
46
+ return decorator
46
47
  }
47
48
 
48
49
  function decorateConstructor (konstructor, name, fn, dependencies) {
@@ -33,10 +33,27 @@ function handleError (reply, error, cb) {
33
33
  const context = reply[kRouteContext]
34
34
  if (reply[kReplyNextErrorHandler] === false) {
35
35
  fallbackErrorHandler(error, reply, function (reply, payload) {
36
+ if (reply.raw.headersSent === true || reply.raw.destroyed === true) {
37
+ // The response has already started transmitting (e.g. an active
38
+ // stream that timed out after its headers were sent). We cannot
39
+ // write a new error response, so abort the in-flight response
40
+ // instead of raising an uncaught ERR_HTTP_HEADERS_SENT.
41
+ reply.raw.destroy(error)
42
+ return
43
+ }
36
44
  try {
37
45
  reply.raw.writeHead(reply.raw.statusCode, reply[kReplyHeaders])
38
46
  } catch (error) {
39
47
  reply.server[kLogController].writeHeadError(error, reply.request, reply)
48
+ // If the response has started transmitting, retrying writeHead
49
+ // would raise an uncaught ERR_HTTP_HEADERS_SENT and terminate the
50
+ // process, so abort the in-flight response instead.
51
+ if (error.code === 'ERR_HTTP_HEADERS_SENT' || reply.raw.headersSent === true || reply.raw.destroyed === true) {
52
+ reply.raw.destroy(error)
53
+ return
54
+ }
55
+ // For other writeHead failures (e.g. an invalid header value in
56
+ // the error payload) retry without the offending headers.
40
57
  reply.raw.writeHead(reply.raw.statusCode)
41
58
  }
42
59
  reply.raw.end(payload)
@@ -134,16 +151,16 @@ function buildErrorHandler (parent = rootErrorHandler, func) {
134
151
  function setErrorHeaders (error, reply) {
135
152
  const res = reply.raw
136
153
  let statusCode = res.statusCode
137
- statusCode = (statusCode >= 400) ? statusCode : 500
154
+ statusCode = (statusCode >= 400 && statusCode <= 599) ? statusCode : 500
138
155
  // treat undefined and null as same
139
156
  if (error != null) {
140
157
  if (error.headers !== undefined) {
141
158
  reply.headers(error.headers)
142
159
  }
143
- if (error.status >= 400) {
144
- statusCode = error.status
145
- } else if (error.statusCode >= 400) {
160
+ if (error.statusCode >= 400 && error.statusCode <= 599) {
146
161
  statusCode = error.statusCode
162
+ } else if (error.status >= 400 && error.status <= 599) {
163
+ statusCode = error.status
147
164
  }
148
165
  }
149
166
  res.statusCode = statusCode
@@ -6,8 +6,10 @@ const {
6
6
 
7
7
  function setErrorStatusCode (reply, err) {
8
8
  if (!reply[kReplyHasStatusCode] || reply.statusCode === 200) {
9
- const statusCode = err && (err.statusCode || err.status)
10
- reply.code(statusCode >= 400 ? statusCode : 500)
9
+ let statusCode = 500
10
+ if (err?.statusCode >= 400 && err.statusCode <= 599) statusCode = err.statusCode
11
+ else if (err?.status >= 400 && err.status <= 599) statusCode = err.status
12
+ reply.code(statusCode)
11
13
  }
12
14
  }
13
15