fastify 5.4.0 → 5.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/.vscode/settings.json +22 -0
  2. package/LICENSE +1 -1
  3. package/SECURITY.md +158 -2
  4. package/build/build-validation.js +19 -1
  5. package/docs/Guides/Delay-Accepting-Requests.md +8 -5
  6. package/docs/Guides/Ecosystem.md +11 -0
  7. package/docs/Guides/Migration-Guide-V5.md +6 -10
  8. package/docs/Guides/Recommendations.md +1 -1
  9. package/docs/Reference/Errors.md +3 -1
  10. package/docs/Reference/Hooks.md +2 -6
  11. package/docs/Reference/Lifecycle.md +2 -2
  12. package/docs/Reference/Request.md +1 -1
  13. package/docs/Reference/Routes.md +4 -3
  14. package/docs/Reference/Server.md +306 -179
  15. package/docs/Reference/TypeScript.md +1 -3
  16. package/docs/Reference/Validation-and-Serialization.md +55 -3
  17. package/docs/Reference/Warnings.md +2 -1
  18. package/fastify.d.ts +2 -2
  19. package/fastify.js +34 -33
  20. package/lib/configValidator.js +196 -28
  21. package/lib/contentTypeParser.js +41 -48
  22. package/lib/error-handler.js +3 -3
  23. package/lib/errors.js +5 -0
  24. package/lib/handleRequest.js +13 -17
  25. package/lib/promise.js +23 -0
  26. package/lib/reply.js +17 -19
  27. package/lib/route.js +37 -3
  28. package/lib/server.js +36 -35
  29. package/lib/warnings.js +11 -1
  30. package/package.json +7 -7
  31. package/test/async-await.test.js +81 -134
  32. package/test/async_hooks.test.js +18 -37
  33. package/test/body-limit.test.js +51 -0
  34. package/test/buffer.test.js +22 -0
  35. package/test/case-insensitive.test.js +44 -65
  36. package/test/check.test.js +17 -21
  37. package/test/close-pipelining.test.js +24 -15
  38. package/test/constrained-routes.test.js +231 -0
  39. package/test/custom-http-server.test.js +7 -15
  40. package/test/custom-parser.0.test.js +267 -348
  41. package/test/custom-parser.1.test.js +141 -191
  42. package/test/custom-parser.2.test.js +34 -44
  43. package/test/custom-parser.3.test.js +56 -104
  44. package/test/custom-parser.4.test.js +106 -144
  45. package/test/custom-parser.5.test.js +56 -75
  46. package/test/custom-querystring-parser.test.js +51 -77
  47. package/test/decorator.test.js +76 -259
  48. package/test/delete.test.js +101 -110
  49. package/test/diagnostics-channel/404.test.js +7 -15
  50. package/test/diagnostics-channel/async-request.test.js +8 -16
  51. package/test/diagnostics-channel/error-request.test.js +7 -15
  52. package/test/diagnostics-channel/sync-request-reply.test.js +9 -16
  53. package/test/diagnostics-channel/sync-request.test.js +9 -16
  54. package/test/fastify-instance.test.js +1 -1
  55. package/test/header-overflow.test.js +18 -29
  56. package/test/helper.js +138 -134
  57. package/test/hooks-async.test.js +26 -32
  58. package/test/hooks.test.js +261 -447
  59. package/test/http-methods/copy.test.js +14 -19
  60. package/test/http-methods/get.test.js +131 -143
  61. package/test/http-methods/head.test.js +53 -84
  62. package/test/http-methods/mkcalendar.test.js +45 -72
  63. package/test/http-methods/move.test.js +6 -10
  64. package/test/http-methods/propfind.test.js +34 -44
  65. package/test/http-methods/unlock.test.js +5 -9
  66. package/test/http2/secure-with-fallback.test.js +3 -1
  67. package/test/https/custom-https-server.test.js +9 -13
  68. package/test/input-validation.js +139 -150
  69. package/test/internals/errors.test.js +50 -1
  70. package/test/internals/handle-request.test.js +29 -5
  71. package/test/internals/promise.test.js +63 -0
  72. package/test/internals/reply.test.js +277 -496
  73. package/test/plugin.1.test.js +40 -68
  74. package/test/plugin.2.test.js +40 -70
  75. package/test/plugin.3.test.js +25 -68
  76. package/test/promises.test.js +42 -63
  77. package/test/register.test.js +8 -18
  78. package/test/request-error.test.js +57 -100
  79. package/test/request-id.test.js +30 -49
  80. package/test/route-hooks.test.js +12 -16
  81. package/test/route-shorthand.test.js +9 -27
  82. package/test/route.1.test.js +74 -131
  83. package/test/route.8.test.js +9 -17
  84. package/test/router-options.test.js +450 -0
  85. package/test/schema-validation.test.js +30 -31
  86. package/test/server.test.js +143 -5
  87. package/test/stream.1.test.js +33 -50
  88. package/test/stream.4.test.js +18 -28
  89. package/test/stream.5.test.js +11 -19
  90. package/test/types/errors.test-d.ts +13 -1
  91. package/test/types/type-provider.test-d.ts +55 -0
  92. package/test/use-semicolon-delimiter.test.js +117 -59
  93. package/test/versioned-routes.test.js +39 -56
  94. package/types/errors.d.ts +11 -1
  95. package/types/hooks.d.ts +1 -1
  96. package/types/instance.d.ts +1 -1
  97. package/types/reply.d.ts +2 -2
@@ -3,7 +3,6 @@
3
3
  const { test } = require('node:test')
4
4
  const { S } = require('fluent-json-schema')
5
5
  const Fastify = require('..')
6
- const sget = require('simple-get').concat
7
6
 
8
7
  const BadRequestSchema = S.object()
9
8
  .prop('statusCode', S.number())
@@ -105,32 +104,29 @@ const handler = (request, reply) => {
105
104
  })
106
105
  }
107
106
 
108
- test('serialize the response for a Bad Request error, as defined on the schema', (t, done) => {
107
+ test('serialize the response for a Bad Request error, as defined on the schema', async t => {
109
108
  const fastify = Fastify({})
110
109
 
111
110
  t.after(() => fastify.close())
112
111
 
113
112
  fastify.post('/', options, handler)
114
113
 
115
- fastify.listen({ port: 0 }, err => {
116
- t.assert.ifError(err)
117
-
118
- const url = `http://localhost:${fastify.server.address().port}/`
119
-
120
- sget({
121
- method: 'POST',
122
- url,
123
- json: true
124
- }, (err, response, body) => {
125
- t.assert.ifError(err)
126
- t.assert.strictEqual(response.statusCode, 400)
127
- t.assert.deepStrictEqual(body, {
128
- statusCode: 400,
129
- error: 'Bad Request',
130
- message: 'body must be object'
131
- })
132
- done()
133
- })
114
+ const fastifyServer = await fastify.listen({ port: 0 })
115
+
116
+ const result = await fetch(fastifyServer, {
117
+ method: 'POST',
118
+ headers: {
119
+ 'Content-Type': 'application/json'
120
+ },
121
+ body: '12'
122
+ })
123
+
124
+ t.assert.ok(!result.ok)
125
+ t.assert.strictEqual(result.status, 400)
126
+ t.assert.deepStrictEqual(await result.json(), {
127
+ statusCode: 400,
128
+ error: 'Bad Request',
129
+ message: 'body must be object'
134
130
  })
135
131
  })
136
132
 
@@ -11,8 +11,10 @@ test('Should return 503 while closing - pipelining', async t => {
11
11
  })
12
12
 
13
13
  fastify.get('/', async (req, reply) => {
14
+ // Simulate a delay to allow pipelining to kick in
15
+ await new Promise(resolve => setTimeout(resolve, 5))
16
+ reply.send({ hello: 'world' })
14
17
  fastify.close()
15
- return { hello: 'world' }
16
18
  })
17
19
 
18
20
  await fastify.listen({ port: 0 })
@@ -21,14 +23,19 @@ test('Should return 503 while closing - pipelining', async t => {
21
23
  pipelining: 2
22
24
  })
23
25
 
24
- const codes = [200, 200, 503]
25
- const responses = await Promise.all([
26
- instance.request({ path: '/', method: 'GET' }),
27
- instance.request({ path: '/', method: 'GET' }),
28
- instance.request({ path: '/', method: 'GET' })
26
+ const [firstRequest, secondRequest, thirdRequest] = await Promise.allSettled([
27
+ instance.request({ path: '/', method: 'GET', blocking: false }),
28
+ instance.request({ path: '/', method: 'GET', blocking: false }),
29
+ instance.request({ path: '/', method: 'GET', blocking: false })
29
30
  ])
30
- const actual = responses.map(r => r.statusCode)
31
- t.assert.deepStrictEqual(actual, codes)
31
+ t.assert.strictEqual(firstRequest.status, 'fulfilled')
32
+ t.assert.strictEqual(secondRequest.status, 'fulfilled')
33
+
34
+ t.assert.strictEqual(firstRequest.value.statusCode, 200)
35
+ t.assert.strictEqual(secondRequest.value.statusCode, 200)
36
+
37
+ t.assert.strictEqual(thirdRequest.status, 'fulfilled')
38
+ t.assert.strictEqual(thirdRequest.value.statusCode, 503)
32
39
 
33
40
  await instance.close()
34
41
  })
@@ -42,6 +49,8 @@ test('Should close the socket abruptly - pipelining - return503OnClosing: false'
42
49
  })
43
50
 
44
51
  fastify.get('/', async (req, reply) => {
52
+ // Simulate a delay to allow pipelining to kick in
53
+ await new Promise(resolve => setTimeout(resolve, 5))
45
54
  reply.send({ hello: 'world' })
46
55
  fastify.close()
47
56
  })
@@ -49,21 +58,21 @@ test('Should close the socket abruptly - pipelining - return503OnClosing: false'
49
58
  await fastify.listen({ port: 0 })
50
59
 
51
60
  const instance = new Client('http://localhost:' + fastify.server.address().port, {
52
- pipelining: 2
61
+ pipelining: 1
53
62
  })
54
63
 
55
64
  const responses = await Promise.allSettled([
56
- instance.request({ path: '/', method: 'GET' }),
57
- instance.request({ path: '/', method: 'GET' }),
58
- instance.request({ path: '/', method: 'GET' }),
59
- instance.request({ path: '/', method: 'GET' })
65
+ instance.request({ path: '/', method: 'GET', blocking: false }),
66
+ instance.request({ path: '/', method: 'GET', blocking: false }),
67
+ instance.request({ path: '/', method: 'GET', blocking: false }),
68
+ instance.request({ path: '/', method: 'GET', blocking: false })
60
69
  ])
61
70
 
62
71
  const fulfilled = responses.filter(r => r.status === 'fulfilled')
63
72
  const rejected = responses.filter(r => r.status === 'rejected')
64
73
 
65
- t.assert.strictEqual(fulfilled.length, 2)
66
- t.assert.strictEqual(rejected.length, 2)
74
+ t.assert.strictEqual(fulfilled.length, 1)
75
+ t.assert.strictEqual(rejected.length, 3)
67
76
 
68
77
  await instance.close()
69
78
  })
@@ -905,3 +905,234 @@ test('Allow regex constraints in routes', async t => {
905
905
  t.assert.strictEqual(res.statusCode, 404)
906
906
  }
907
907
  })
908
+
909
+ test('Should allow registering custom rotuerOptions constrained routes', async t => {
910
+ t.plan(5)
911
+
912
+ const constraint = {
913
+ name: 'secret',
914
+ storage: function () {
915
+ const secrets = {}
916
+ return {
917
+ get: (secret) => { return secrets[secret] || null },
918
+ set: (secret, store) => { secrets[secret] = store }
919
+ }
920
+ },
921
+ deriveConstraint: (req, ctx) => {
922
+ return req.headers['x-secret']
923
+ },
924
+ validate () { return true }
925
+ }
926
+
927
+ const fastify = Fastify({ routerOptions: { constraints: { secret: constraint } } })
928
+
929
+ fastify.route({
930
+ method: 'GET',
931
+ url: '/',
932
+ constraints: { secret: 'alpha' },
933
+ handler: (req, reply) => {
934
+ reply.send({ hello: 'from alpha' })
935
+ }
936
+ })
937
+
938
+ fastify.route({
939
+ method: 'GET',
940
+ url: '/',
941
+ constraints: { secret: 'beta' },
942
+ handler: (req, reply) => {
943
+ reply.send({ hello: 'from beta' })
944
+ }
945
+ })
946
+
947
+ {
948
+ const res = await fastify.inject({
949
+ method: 'GET',
950
+ url: '/',
951
+ headers: {
952
+ 'X-Secret': 'alpha'
953
+ }
954
+ })
955
+ t.assert.deepStrictEqual(JSON.parse(res.payload), { hello: 'from alpha' })
956
+ t.assert.strictEqual(res.statusCode, 200)
957
+ }
958
+
959
+ {
960
+ const res = await fastify.inject({
961
+ method: 'GET',
962
+ url: '/',
963
+ headers: {
964
+ 'X-Secret': 'beta'
965
+ }
966
+ })
967
+ t.assert.deepStrictEqual(JSON.parse(res.payload), { hello: 'from beta' })
968
+ t.assert.strictEqual(res.statusCode, 200)
969
+ }
970
+
971
+ {
972
+ const res = await fastify.inject({
973
+ method: 'GET',
974
+ url: '/',
975
+ headers: {
976
+ 'X-Secret': 'gamma'
977
+ }
978
+ })
979
+ t.assert.strictEqual(res.statusCode, 404)
980
+ }
981
+ })
982
+
983
+ test('Custom rotuerOptions constrained routes registered also for HEAD method generated by fastify', (t, done) => {
984
+ t.plan(3)
985
+
986
+ const constraint = {
987
+ name: 'secret',
988
+ storage: function () {
989
+ const secrets = {}
990
+ return {
991
+ get: (secret) => { return secrets[secret] || null },
992
+ set: (secret, store) => { secrets[secret] = store }
993
+ }
994
+ },
995
+ deriveConstraint: (req, ctx) => {
996
+ return req.headers['x-secret']
997
+ },
998
+ validate () { return true }
999
+ }
1000
+
1001
+ const fastify = Fastify({ routerOptions: { constraints: { secret: constraint } } })
1002
+
1003
+ fastify.route({
1004
+ method: 'GET',
1005
+ url: '/',
1006
+ constraints: { secret: 'mySecret' },
1007
+ handler: (req, reply) => {
1008
+ reply.send('from mySecret - my length is 31')
1009
+ }
1010
+ })
1011
+
1012
+ fastify.inject({
1013
+ method: 'HEAD',
1014
+ url: '/',
1015
+ headers: {
1016
+ 'X-Secret': 'mySecret'
1017
+ }
1018
+ }, (err, res) => {
1019
+ t.assert.ifError(err)
1020
+ t.assert.deepStrictEqual(res.headers['content-length'], '31')
1021
+ t.assert.strictEqual(res.statusCode, 200)
1022
+ done()
1023
+ })
1024
+ })
1025
+
1026
+ test('allow async rotuerOptions constraints', async (t) => {
1027
+ t.plan(5)
1028
+
1029
+ const constraint = {
1030
+ name: 'secret',
1031
+ storage: function () {
1032
+ const secrets = {}
1033
+ return {
1034
+ get: (secret) => { return secrets[secret] || null },
1035
+ set: (secret, store) => { secrets[secret] = store }
1036
+ }
1037
+ },
1038
+ deriveConstraint: (req, ctx, done) => {
1039
+ done(null, req.headers['x-secret'])
1040
+ },
1041
+ validate () { return true }
1042
+ }
1043
+
1044
+ const fastify = Fastify({ routerOptions: { constraints: { secret: constraint } } })
1045
+
1046
+ fastify.route({
1047
+ method: 'GET',
1048
+ url: '/',
1049
+ constraints: { secret: 'alpha' },
1050
+ handler: (req, reply) => {
1051
+ reply.send({ hello: 'from alpha' })
1052
+ }
1053
+ })
1054
+
1055
+ fastify.route({
1056
+ method: 'GET',
1057
+ url: '/',
1058
+ constraints: { secret: 'beta' },
1059
+ handler: (req, reply) => {
1060
+ reply.send({ hello: 'from beta' })
1061
+ }
1062
+ })
1063
+
1064
+ {
1065
+ const { statusCode, payload } = await fastify.inject({ method: 'GET', path: '/', headers: { 'X-Secret': 'alpha' } })
1066
+ t.assert.deepStrictEqual(JSON.parse(payload), { hello: 'from alpha' })
1067
+ t.assert.strictEqual(statusCode, 200)
1068
+ }
1069
+ {
1070
+ const { statusCode, payload } = await fastify.inject({ method: 'GET', path: '/', headers: { 'X-Secret': 'beta' } })
1071
+ t.assert.deepStrictEqual(JSON.parse(payload), { hello: 'from beta' })
1072
+ t.assert.strictEqual(statusCode, 200)
1073
+ }
1074
+ {
1075
+ const { statusCode } = await fastify.inject({ method: 'GET', path: '/', headers: { 'X-Secret': 'gamma' } })
1076
+ t.assert.strictEqual(statusCode, 404)
1077
+ }
1078
+ })
1079
+
1080
+ test('error in async rotuerOptions constraints', async (t) => {
1081
+ t.plan(8)
1082
+
1083
+ const constraint = {
1084
+ name: 'secret',
1085
+ storage: function () {
1086
+ const secrets = {}
1087
+ return {
1088
+ get: (secret) => { return secrets[secret] || null },
1089
+ set: (secret, store) => { secrets[secret] = store }
1090
+ }
1091
+ },
1092
+ deriveConstraint: (req, ctx, done) => {
1093
+ done(Error('kaboom'))
1094
+ },
1095
+ validate () { return true }
1096
+ }
1097
+
1098
+ const fastify = Fastify({ routerOptions: { constraints: { secret: constraint } } })
1099
+
1100
+ fastify.route({
1101
+ method: 'GET',
1102
+ url: '/',
1103
+ constraints: { secret: 'alpha' },
1104
+ handler: (req, reply) => {
1105
+ reply.send({ hello: 'from alpha' })
1106
+ }
1107
+ })
1108
+
1109
+ fastify.route({
1110
+ method: 'GET',
1111
+ url: '/',
1112
+ constraints: { secret: 'beta' },
1113
+ handler: (req, reply) => {
1114
+ reply.send({ hello: 'from beta' })
1115
+ }
1116
+ })
1117
+
1118
+ {
1119
+ const { statusCode, payload } = await fastify.inject({ method: 'GET', path: '/', headers: { 'X-Secret': 'alpha' } })
1120
+ t.assert.deepStrictEqual(JSON.parse(payload), { error: 'Internal Server Error', message: 'Unexpected error from async constraint', statusCode: 500 })
1121
+ t.assert.strictEqual(statusCode, 500)
1122
+ }
1123
+ {
1124
+ const { statusCode, payload } = await fastify.inject({ method: 'GET', path: '/', headers: { 'X-Secret': 'beta' } })
1125
+ t.assert.deepStrictEqual(JSON.parse(payload), { error: 'Internal Server Error', message: 'Unexpected error from async constraint', statusCode: 500 })
1126
+ t.assert.strictEqual(statusCode, 500)
1127
+ }
1128
+ {
1129
+ const { statusCode, payload } = await fastify.inject({ method: 'GET', path: '/', headers: { 'X-Secret': 'gamma' } })
1130
+ t.assert.deepStrictEqual(JSON.parse(payload), { error: 'Internal Server Error', message: 'Unexpected error from async constraint', statusCode: 500 })
1131
+ t.assert.strictEqual(statusCode, 500)
1132
+ }
1133
+ {
1134
+ const { statusCode, payload } = await fastify.inject({ method: 'GET', path: '/' })
1135
+ t.assert.deepStrictEqual(JSON.parse(payload), { error: 'Internal Server Error', message: 'Unexpected error from async constraint', statusCode: 500 })
1136
+ t.assert.strictEqual(statusCode, 500)
1137
+ }
1138
+ })
@@ -3,7 +3,6 @@
3
3
  const { test } = require('node:test')
4
4
  const http = require('node:http')
5
5
  const dns = require('node:dns').promises
6
- const sget = require('simple-get').concat
7
6
  const Fastify = require('..')
8
7
  const { FST_ERR_FORCE_CLOSE_CONNECTIONS_IDLE_NOT_AVAILABLE } = require('../lib/errors')
9
8
 
@@ -11,7 +10,7 @@ async function setup () {
11
10
  const localAddresses = await dns.lookup('localhost', { all: true })
12
11
 
13
12
  test('Should support a custom http server', { skip: localAddresses.length < 1 }, async t => {
14
- t.plan(4)
13
+ t.plan(5)
15
14
 
16
15
  const fastify = Fastify({
17
16
  serverFactory: (handler, opts) => {
@@ -34,20 +33,13 @@ async function setup () {
34
33
 
35
34
  await fastify.listen({ port: 0 })
36
35
 
37
- await new Promise((resolve, reject) => {
38
- sget({
39
- method: 'GET',
40
- url: 'http://localhost:' + fastify.server.address().port,
41
- rejectUnauthorized: false
42
- }, (err, response, body) => {
43
- if (err) {
44
- return reject(err)
45
- }
46
- t.assert.strictEqual(response.statusCode, 200)
47
- t.assert.deepStrictEqual(JSON.parse(body), { hello: 'world' })
48
- resolve()
49
- })
36
+ const response = await fetch('http://localhost:' + fastify.server.address().port, {
37
+ method: 'GET'
50
38
  })
39
+ t.assert.ok(response.ok)
40
+ t.assert.strictEqual(response.status, 200)
41
+ const body = await response.text()
42
+ t.assert.deepStrictEqual(JSON.parse(body), { hello: 'world' })
51
43
  })
52
44
 
53
45
  test('Should not allow forceCloseConnection=idle if the server does not support closeIdleConnections', t => {