fastify 6.0.0-alpha.5 → 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 (46) hide show
  1. package/README.md +1 -0
  2. package/build/build-validation.js +2 -0
  3. package/docs/Guides/Ecosystem.md +4 -0
  4. package/docs/Guides/Migration-Guide-V5.md +2 -2
  5. package/docs/Guides/Serverless.md +33 -10
  6. package/docs/Reference/Errors.md +13 -0
  7. package/docs/Reference/Hooks.md +13 -0
  8. package/docs/Reference/Reply.md +786 -697
  9. package/docs/Reference/Request.md +303 -107
  10. package/docs/Reference/Server.md +39 -2
  11. package/docs/Reference/Validation-and-Serialization.md +15 -1
  12. package/fastify.d.ts +1 -0
  13. package/fastify.js +35 -1
  14. package/lib/config-validator.js +102 -73
  15. package/lib/content-type-parser.js +14 -2
  16. package/lib/context.js +2 -1
  17. package/lib/decorate.js +4 -3
  18. package/lib/error-handler.js +21 -4
  19. package/lib/error-status.js +4 -2
  20. package/lib/errors.js +15 -9
  21. package/lib/reply.js +12 -11
  22. package/lib/request.js +3 -2
  23. package/lib/route.js +34 -5
  24. package/lib/schemas.js +44 -32
  25. package/package.json +8 -3
  26. package/test/close-disposable.test.js +154 -0
  27. package/test/constrained-routes.test.js +4 -2
  28. package/test/content-parser.test.js +39 -0
  29. package/test/decorator.test.js +29 -0
  30. package/test/diagnostics-channel/error-status.test.js +35 -2
  31. package/test/handler-timeout.test.js +224 -0
  32. package/test/internals/all.test.js +5 -0
  33. package/test/internals/errors.test.js +18 -9
  34. package/test/internals/initial-config.test.js +2 -0
  35. package/test/internals/server.test.js +6 -4
  36. package/test/reply-error.test.js +82 -0
  37. package/test/reply-trailers.test.js +159 -1
  38. package/test/router-compile.test.js +241 -0
  39. package/test/schema-serialization.test.js +120 -7
  40. package/test/types/fastify.tst.ts +4 -0
  41. package/test/types/instance.tst.ts +1 -1
  42. package/test/types/route.tst.ts +16 -0
  43. package/test/types/using.tst.ts +20 -0
  44. package/test/validation-error-handling.test.js +21 -1
  45. package/types/instance.d.ts +5 -1
  46. package/types/route.d.ts +1 -0
@@ -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
  }
@@ -317,7 +323,13 @@ function rawBody (request, reply, options, parser, done) {
317
323
  body = Buffer.concat(chunks, receivedLength)
318
324
  }
319
325
 
320
- 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
+ }
321
333
  if (result && typeof result.then === 'function') {
322
334
  result.then(body => { done(null, body) }, done)
323
335
  }
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
 
package/lib/errors.js CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  const createError = require('@fastify/error')
4
4
 
5
+ // Errors created when Fastify rejects an invalid request. They have no stack
6
+ // trace, see "Errors In Input Data" in docs/Reference/Errors.md.
7
+ function createRequestError (code, message, statusCode, Base = Error) {
8
+ return createError(code, message, statusCode, Base, false)
9
+ }
10
+
5
11
  const codes = {
6
12
  /**
7
13
  * Basic
@@ -47,7 +53,7 @@ const codes = {
47
53
  500,
48
54
  TypeError
49
55
  ),
50
- FST_ERR_VALIDATION: createError(
56
+ FST_ERR_VALIDATION: createRequestError(
51
57
  'FST_ERR_VALIDATION',
52
58
  '%s',
53
59
  400
@@ -102,29 +108,29 @@ const codes = {
102
108
  500,
103
109
  TypeError
104
110
  ),
105
- FST_ERR_CTP_BODY_TOO_LARGE: createError(
111
+ FST_ERR_CTP_BODY_TOO_LARGE: createRequestError(
106
112
  'FST_ERR_CTP_BODY_TOO_LARGE',
107
113
  'Request body is too large',
108
114
  413,
109
115
  RangeError
110
116
  ),
111
- FST_ERR_CTP_INVALID_MEDIA_TYPE: createError(
117
+ FST_ERR_CTP_INVALID_MEDIA_TYPE: createRequestError(
112
118
  'FST_ERR_CTP_INVALID_MEDIA_TYPE',
113
119
  'Unsupported Media Type',
114
120
  415
115
121
  ),
116
- FST_ERR_CTP_INVALID_CONTENT_LENGTH: createError(
122
+ FST_ERR_CTP_INVALID_CONTENT_LENGTH: createRequestError(
117
123
  'FST_ERR_CTP_INVALID_CONTENT_LENGTH',
118
124
  'Request body size did not match Content-Length',
119
125
  400,
120
126
  RangeError
121
127
  ),
122
- FST_ERR_CTP_EMPTY_JSON_BODY: createError(
128
+ FST_ERR_CTP_EMPTY_JSON_BODY: createRequestError(
123
129
  'FST_ERR_CTP_EMPTY_JSON_BODY',
124
130
  "Body cannot be empty when content-type is set to 'application/json'",
125
131
  400
126
132
  ),
127
- FST_ERR_CTP_INVALID_JSON_BODY: createError(
133
+ FST_ERR_CTP_INVALID_JSON_BODY: createRequestError(
128
134
  'FST_ERR_CTP_INVALID_JSON_BODY',
129
135
  "Body is not valid JSON but content-type is set to 'application/json'",
130
136
  400
@@ -365,13 +371,13 @@ const codes = {
365
371
  'FST_ERR_DUPLICATED_ROUTE',
366
372
  "Method '%s' already declared for route '%s'"
367
373
  ),
368
- FST_ERR_BAD_URL: createError(
374
+ FST_ERR_BAD_URL: createRequestError(
369
375
  'FST_ERR_BAD_URL',
370
376
  "'%s' is not a valid url component",
371
377
  400,
372
378
  URIError
373
379
  ),
374
- FST_ERR_MAX_PARAM_LENGTH: createError(
380
+ FST_ERR_MAX_PARAM_LENGTH: createRequestError(
375
381
  'FST_ERR_MAX_PARAM_LENGTH',
376
382
  "'%s' is exceeding the max param length",
377
383
  414,
@@ -461,7 +467,7 @@ const codes = {
461
467
  500,
462
468
  TypeError
463
469
  ),
464
- FST_ERR_ROUTE_MISSING_CONTENT_TYPE: createError(
470
+ FST_ERR_ROUTE_MISSING_CONTENT_TYPE: createRequestError(
465
471
  'FST_ERR_ROUTE_MISSING_CONTENT_TYPE',
466
472
  "Method '%s' must provide a 'Content-Type' header.",
467
473
  400
package/lib/reply.js CHANGED
@@ -843,9 +843,6 @@ function sendStream (payload, res, reply) {
843
843
  let sourceOpen = true
844
844
  let errorLogged = false
845
845
 
846
- // set trailer when stream ended
847
- sendStreamTrailer(payload, res, reply)
848
-
849
846
  eos(payload, { readable: true, writable: false }, function (err) {
850
847
  sourceOpen = false
851
848
  if (err != null) {
@@ -858,8 +855,12 @@ function sendStream (payload, res, reply) {
858
855
  } else {
859
856
  onErrorHook(reply, err)
860
857
  }
858
+ return
859
+ }
860
+ // eos fires even for a stream that already ended
861
+ if (reply[kReplyTrailers] !== null) {
862
+ sendTrailer(null, res, reply)
861
863
  }
862
- // there is nothing to do if there is not an error
863
864
  })
864
865
 
865
866
  eos(res, function (err) {
@@ -891,7 +892,12 @@ function sendStream (payload, res, reply) {
891
892
  } else {
892
893
  reply.log.warn('response will send, but you shouldn\'t use res.writeHead in stream mode')
893
894
  }
894
- payload.pipe(res)
895
+ // trailers set: the eos listener above ends the response, not pipe
896
+ if (reply[kReplyTrailers] !== null) {
897
+ payload.pipe(res, { end: false })
898
+ } else {
899
+ payload.pipe(res)
900
+ }
895
901
  }
896
902
 
897
903
  function sendTrailer (payload, res, reply) {
@@ -951,11 +957,6 @@ function sendTrailer (payload, res, reply) {
951
957
  if (skipped) res.end(null, null, null) // avoid ArgumentsAdaptorTrampoline from V8
952
958
  }
953
959
 
954
- function sendStreamTrailer (payload, res, reply) {
955
- if (reply[kReplyTrailers] === null) return
956
- payload.on('end', () => sendTrailer(null, res, reply))
957
- }
958
-
959
960
  function onErrorHook (reply, error, cb) {
960
961
  if (reply[kRouteContext].onError !== null && !reply[kReplyNextErrorHandler]) {
961
962
  reply[kReplyIsRunningOnErrorHook] = true
@@ -1083,7 +1084,7 @@ function notFound (reply) {
1083
1084
  * @returns {string} the serialized payload
1084
1085
  */
1085
1086
  function serialize (context, data, statusCode, contentType) {
1086
- const fnSerialize = getSchemaSerializer(context, statusCode, contentType)
1087
+ const fnSerialize = getSchemaSerializer(context, statusCode, contentType, { failClosed: true })
1087
1088
  if (fnSerialize) {
1088
1089
  return fnSerialize(data)
1089
1090
  }
package/lib/request.js CHANGED
@@ -20,6 +20,8 @@ const { FST_ERR_REQ_INVALID_VALIDATION_INVOCATION, FST_ERR_DEC_UNDECLARED } = re
20
20
  const decorators = require('./decorate')
21
21
  const ContentType = require('./content-type')
22
22
 
23
+ const PORT_REGEX = /(?<port>:\d+)$/
24
+
23
25
  const HTTP_PART_SYMBOL_MAP = {
24
26
  body: kSchemaBody,
25
27
  headers: kSchemaHeaders,
@@ -263,8 +265,7 @@ Object.defineProperties(Request.prototype, {
263
265
  },
264
266
  port: {
265
267
  get () {
266
- const portReg = /(?<port>:\d+)$/
267
- const matches = portReg.exec(this.host)
268
+ const matches = PORT_REGEX.exec(this.host)
268
269
  if (matches === null || matches[1] === undefined) {
269
270
  return null
270
271
  }
package/lib/route.js CHANGED
@@ -110,7 +110,10 @@ function buildRouting (options) {
110
110
  return503OnClosing = Object.hasOwn(options, 'return503OnClosing') ? options.return503OnClosing : true
111
111
  keepAliveConnections = fastifyArgs.keepAliveConnections
112
112
  },
113
- routing: router.lookup.bind(router), // router func to find the right handler to call
113
+ // router func to find the right handler to call, not bound because
114
+ // router.compile() replaces router.lookup
115
+ routing: (req, res, done) => router.lookup(req, res, done),
116
+ compileRoutes: () => router.compile(),
114
117
  route, // configure a route in the fastify instance
115
118
  hasRoute,
116
119
  prepareRoute,
@@ -535,12 +538,35 @@ function buildRouting (options) {
535
538
  request[kRequestSignal] = ac
536
539
 
537
540
  request[kTimeoutTimer] = setTimeout(() => {
538
- if (!reply.sent) {
539
- const err = new FST_ERR_HANDLER_TIMEOUT(handlerTimeout, context.config?.url)
540
- ac.abort(err)
541
+ // If the reply has already ended (or was hijacked) the response
542
+ // lifecycle is managed elsewhere and there is nothing left to do.
543
+ if (reply.sent === true) {
544
+ return
545
+ }
546
+
547
+ const err = new FST_ERR_HANDLER_TIMEOUT(handlerTimeout, context.config?.url)
548
+ ac.abort(err)
549
+
550
+ // Aborting runs any request.signal listener synchronously; if a
551
+ // listener sent or hijacked the response, leave it intact rather than
552
+ // destroying it, which would reset the connection.
553
+ if (reply.sent === true) {
554
+ return
555
+ }
556
+
557
+ if (reply.raw.headersSent === true) {
558
+ // The response has already started transmitting, e.g. an active
559
+ // stream that has written its headers/body before the timeout.
560
+ // Sending an error response now would raise an uncaught
561
+ // ERR_HTTP_HEADERS_SENT and terminate the process, so abort the
562
+ // in-flight response instead.
541
563
  reply[kReplyIsError] = true
542
- reply.send(err)
564
+ reply.raw.destroy(err)
565
+ return
543
566
  }
567
+
568
+ reply[kReplyIsError] = true
569
+ reply.send(err)
544
570
  }, handlerTimeout)
545
571
 
546
572
  const onAbort = () => {
@@ -678,6 +704,9 @@ function buildRouterOptions (options, defaultOptions) {
678
704
  }
679
705
  }
680
706
 
707
+ // compile is only accepted in routerOptions, not as a top-level option
708
+ routerOptions.compile ??= defaultInitOptions.routerOptions.compile
709
+
681
710
  return routerOptions
682
711
  }
683
712