fastify 4.24.0 → 4.24.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 (53) hide show
  1. package/fastify.js +1 -1
  2. package/lib/route.js +10 -4
  3. package/package.json +32 -32
  4. package/test/404s.test.js +31 -39
  5. package/test/async-await.test.js +1 -1
  6. package/test/build-certificate.js +90 -1
  7. package/test/close-pipelining.test.js +5 -5
  8. package/test/close.test.js +1 -5
  9. package/test/custom-http-server.test.js +94 -91
  10. package/test/custom-parser.0.test.js +21 -47
  11. package/test/custom-parser.1.test.js +10 -732
  12. package/test/custom-parser.2.test.js +102 -0
  13. package/test/custom-parser.3.test.js +245 -0
  14. package/test/custom-parser.4.test.js +239 -0
  15. package/test/custom-parser.5.test.js +149 -0
  16. package/test/head.test.js +204 -0
  17. package/test/helper.js +30 -8
  18. package/test/hooks-async.test.js +3 -3
  19. package/test/hooks.on-listen.test.js +7 -6
  20. package/test/hooks.test.js +4 -15
  21. package/test/http2/closing.test.js +7 -15
  22. package/test/https/custom-https-server.test.js +43 -40
  23. package/test/listen.1.test.js +101 -0
  24. package/test/listen.2.test.js +103 -0
  25. package/test/listen.3.test.js +87 -0
  26. package/test/listen.4.test.js +164 -0
  27. package/test/listen.deprecated.test.js +3 -9
  28. package/test/logger/instantiation.test.js +25 -16
  29. package/test/logger/logger-test-utils.js +1 -1
  30. package/test/plugin.1.test.js +249 -0
  31. package/test/plugin.2.test.js +328 -0
  32. package/test/plugin.3.test.js +311 -0
  33. package/test/plugin.4.test.js +416 -0
  34. package/test/reply-trailers.test.js +1 -2
  35. package/test/route.1.test.js +309 -0
  36. package/test/route.2.test.js +99 -0
  37. package/test/route.3.test.js +205 -0
  38. package/test/route.4.test.js +131 -0
  39. package/test/route.5.test.js +230 -0
  40. package/test/route.6.test.js +306 -0
  41. package/test/route.7.test.js +370 -0
  42. package/test/route.8.test.js +142 -0
  43. package/test/stream.1.test.js +108 -0
  44. package/test/stream.2.test.js +119 -0
  45. package/test/stream.3.test.js +192 -0
  46. package/test/stream.4.test.js +223 -0
  47. package/test/stream.5.test.js +194 -0
  48. package/test/trust-proxy.test.js +2 -4
  49. package/test/upgrade.test.js +3 -3
  50. package/test/listen.test.js +0 -427
  51. package/test/plugin.test.js +0 -1275
  52. package/test/route.test.js +0 -1762
  53. package/test/stream.test.js +0 -816
@@ -0,0 +1,370 @@
1
+ 'use strict'
2
+
3
+ const stream = require('node:stream')
4
+ const split = require('split2')
5
+ const t = require('tap')
6
+ const test = t.test
7
+ const Fastify = require('..')
8
+ const proxyquire = require('proxyquire')
9
+
10
+ test("HEAD route should handle stream.on('error')", t => {
11
+ t.plan(6)
12
+
13
+ const resStream = stream.Readable.from('Hello with error!')
14
+ const logStream = split(JSON.parse)
15
+ const expectedError = new Error('Hello!')
16
+ const fastify = Fastify({
17
+ logger: {
18
+ stream: logStream,
19
+ level: 'error'
20
+ }
21
+ })
22
+
23
+ fastify.route({
24
+ method: 'GET',
25
+ path: '/more-coffee',
26
+ exposeHeadRoute: true,
27
+ handler: (req, reply) => {
28
+ process.nextTick(() => resStream.emit('error', expectedError))
29
+ return resStream
30
+ }
31
+ })
32
+
33
+ logStream.once('data', line => {
34
+ const { message, stack } = expectedError
35
+ t.same(line.err, { type: 'Error', message, stack })
36
+ t.equal(line.msg, 'Error on Stream found for HEAD route')
37
+ t.equal(line.level, 50)
38
+ })
39
+
40
+ fastify.inject({
41
+ method: 'HEAD',
42
+ url: '/more-coffee'
43
+ }, (error, res) => {
44
+ t.error(error)
45
+ t.equal(res.statusCode, 200)
46
+ t.equal(res.headers['content-type'], undefined)
47
+ })
48
+ })
49
+
50
+ test('HEAD route should be exposed by default', t => {
51
+ t.plan(7)
52
+
53
+ const resStream = stream.Readable.from('Hello with error!')
54
+ const resJson = { hello: 'world' }
55
+ const fastify = Fastify()
56
+
57
+ fastify.route({
58
+ method: 'GET',
59
+ path: '/without-flag',
60
+ handler: (req, reply) => {
61
+ return resStream
62
+ }
63
+ })
64
+
65
+ fastify.route({
66
+ exposeHeadRoute: true,
67
+ method: 'GET',
68
+ path: '/with-flag',
69
+ handler: (req, reply) => {
70
+ return resJson
71
+ }
72
+ })
73
+
74
+ fastify.inject({
75
+ method: 'HEAD',
76
+ url: '/without-flag'
77
+ }, (error, res) => {
78
+ t.error(error)
79
+ t.equal(res.statusCode, 200)
80
+ })
81
+
82
+ fastify.inject({
83
+ method: 'HEAD',
84
+ url: '/with-flag'
85
+ }, (error, res) => {
86
+ t.error(error)
87
+ t.equal(res.statusCode, 200)
88
+ t.equal(res.headers['content-type'], 'application/json; charset=utf-8')
89
+ t.equal(res.headers['content-length'], `${Buffer.byteLength(JSON.stringify(resJson))}`)
90
+ t.equal(res.body, '')
91
+ })
92
+ })
93
+
94
+ test('HEAD route should be exposed if route exposeHeadRoute is set', t => {
95
+ t.plan(7)
96
+
97
+ const resBuffer = Buffer.from('I am a coffee!')
98
+ const resJson = { hello: 'world' }
99
+ const fastify = Fastify({ exposeHeadRoutes: false })
100
+
101
+ fastify.route({
102
+ exposeHeadRoute: true,
103
+ method: 'GET',
104
+ path: '/one',
105
+ handler: (req, reply) => {
106
+ return resBuffer
107
+ }
108
+ })
109
+
110
+ fastify.route({
111
+ method: 'GET',
112
+ path: '/two',
113
+ handler: (req, reply) => {
114
+ return resJson
115
+ }
116
+ })
117
+
118
+ fastify.inject({
119
+ method: 'HEAD',
120
+ url: '/one'
121
+ }, (error, res) => {
122
+ t.error(error)
123
+ t.equal(res.statusCode, 200)
124
+ t.equal(res.headers['content-type'], 'application/octet-stream')
125
+ t.equal(res.headers['content-length'], `${resBuffer.byteLength}`)
126
+ t.equal(res.body, '')
127
+ })
128
+
129
+ fastify.inject({
130
+ method: 'HEAD',
131
+ url: '/two'
132
+ }, (error, res) => {
133
+ t.error(error)
134
+ t.equal(res.statusCode, 404)
135
+ })
136
+ })
137
+
138
+ test('Set a custom HEAD route before GET one without disabling exposeHeadRoutes (global)', t => {
139
+ t.plan(6)
140
+
141
+ const resBuffer = Buffer.from('I am a coffee!')
142
+ const fastify = Fastify({
143
+ exposeHeadRoutes: true
144
+ })
145
+
146
+ fastify.route({
147
+ method: 'HEAD',
148
+ path: '/one',
149
+ handler: (req, reply) => {
150
+ reply.header('content-type', 'application/pdf')
151
+ reply.header('content-length', `${resBuffer.byteLength}`)
152
+ reply.header('x-custom-header', 'some-custom-header')
153
+ reply.send()
154
+ }
155
+ })
156
+
157
+ fastify.route({
158
+ method: 'GET',
159
+ path: '/one',
160
+ handler: (req, reply) => {
161
+ return resBuffer
162
+ }
163
+ })
164
+
165
+ fastify.inject({
166
+ method: 'HEAD',
167
+ url: '/one'
168
+ }, (error, res) => {
169
+ t.error(error)
170
+ t.equal(res.statusCode, 200)
171
+ t.equal(res.headers['content-type'], 'application/pdf')
172
+ t.equal(res.headers['content-length'], `${resBuffer.byteLength}`)
173
+ t.equal(res.headers['x-custom-header'], 'some-custom-header')
174
+ t.equal(res.body, '')
175
+ })
176
+ })
177
+
178
+ test('Set a custom HEAD route before GET one without disabling exposeHeadRoutes (route)', t => {
179
+ t.plan(7)
180
+
181
+ function onWarning (code) {
182
+ t.equal(code, 'FSTDEP007')
183
+ }
184
+ const warning = {
185
+ emit: onWarning
186
+ }
187
+
188
+ const route = proxyquire('../lib/route', { './warnings': warning })
189
+ const fastify = proxyquire('..', { './lib/route.js': route })()
190
+
191
+ const resBuffer = Buffer.from('I am a coffee!')
192
+
193
+ fastify.route({
194
+ method: 'HEAD',
195
+ path: '/one',
196
+ handler: (req, reply) => {
197
+ reply.header('content-type', 'application/pdf')
198
+ reply.header('content-length', `${resBuffer.byteLength}`)
199
+ reply.header('x-custom-header', 'some-custom-header')
200
+ reply.send()
201
+ }
202
+ })
203
+
204
+ fastify.route({
205
+ method: 'GET',
206
+ exposeHeadRoute: true,
207
+ path: '/one',
208
+ handler: (req, reply) => {
209
+ return resBuffer
210
+ }
211
+ })
212
+
213
+ fastify.inject({
214
+ method: 'HEAD',
215
+ url: '/one'
216
+ }, (error, res) => {
217
+ t.error(error)
218
+ t.equal(res.statusCode, 200)
219
+ t.equal(res.headers['content-type'], 'application/pdf')
220
+ t.equal(res.headers['content-length'], `${resBuffer.byteLength}`)
221
+ t.equal(res.headers['x-custom-header'], 'some-custom-header')
222
+ t.equal(res.body, '')
223
+ })
224
+ })
225
+
226
+ test('HEAD routes properly auto created for GET routes when prefixTrailingSlash: \'no-slash\'', t => {
227
+ t.plan(2)
228
+
229
+ const fastify = Fastify()
230
+
231
+ fastify.register(function routes (f, opts, next) {
232
+ f.route({
233
+ method: 'GET',
234
+ url: '/',
235
+ exposeHeadRoute: true,
236
+ prefixTrailingSlash: 'no-slash',
237
+ handler: (req, reply) => {
238
+ reply.send({ hello: 'world' })
239
+ }
240
+ })
241
+
242
+ next()
243
+ }, { prefix: '/prefix' })
244
+
245
+ fastify.inject({ url: '/prefix/prefix', method: 'HEAD' }, (err, res) => {
246
+ t.error(err)
247
+ t.equal(res.statusCode, 404)
248
+ })
249
+ })
250
+
251
+ test('HEAD routes properly auto created for GET routes when prefixTrailingSlash: \'both\'', async t => {
252
+ t.plan(3)
253
+
254
+ const fastify = Fastify()
255
+
256
+ fastify.register(function routes (f, opts, next) {
257
+ f.route({
258
+ method: 'GET',
259
+ url: '/',
260
+ exposeHeadRoute: true,
261
+ prefixTrailingSlash: 'both',
262
+ handler: (req, reply) => {
263
+ reply.send({ hello: 'world' })
264
+ }
265
+ })
266
+
267
+ next()
268
+ }, { prefix: '/prefix' })
269
+
270
+ const doublePrefixReply = await fastify.inject({ url: '/prefix/prefix', method: 'HEAD' })
271
+ const trailingSlashReply = await fastify.inject({ url: '/prefix/', method: 'HEAD' })
272
+ const noneTrailingReply = await fastify.inject({ url: '/prefix', method: 'HEAD' })
273
+
274
+ t.equal(doublePrefixReply.statusCode, 404)
275
+ t.equal(trailingSlashReply.statusCode, 200)
276
+ t.equal(noneTrailingReply.statusCode, 200)
277
+ })
278
+
279
+ test('GET route with body schema should throw', t => {
280
+ t.plan(1)
281
+
282
+ const fastify = Fastify()
283
+
284
+ t.throws(() => {
285
+ fastify.route({
286
+ method: 'GET',
287
+ path: '/get',
288
+ schema: {
289
+ body: {}
290
+ },
291
+ handler: function (req, reply) {
292
+ reply.send({ hello: 'world' })
293
+ }
294
+ })
295
+ }, new Error('Body validation schema for GET:/get route is not supported!'))
296
+ })
297
+
298
+ test('HEAD route with body schema should throw', t => {
299
+ t.plan(1)
300
+
301
+ const fastify = Fastify()
302
+
303
+ t.throws(() => {
304
+ fastify.route({
305
+ method: 'HEAD',
306
+ path: '/shouldThrow',
307
+ schema: {
308
+ body: {}
309
+ },
310
+ handler: function (req, reply) {
311
+ reply.send({ hello: 'world' })
312
+ }
313
+ })
314
+ }, new Error('Body validation schema for HEAD:/shouldThrow route is not supported!'))
315
+ })
316
+
317
+ test('[HEAD, GET] route with body schema should throw', t => {
318
+ t.plan(1)
319
+
320
+ const fastify = Fastify()
321
+
322
+ t.throws(() => {
323
+ fastify.route({
324
+ method: ['HEAD', 'GET'],
325
+ path: '/shouldThrowHead',
326
+ schema: {
327
+ body: {}
328
+ },
329
+ handler: function (req, reply) {
330
+ reply.send({ hello: 'world' })
331
+ }
332
+ })
333
+ }, new Error('Body validation schema for HEAD:/shouldThrowHead route is not supported!'))
334
+ })
335
+
336
+ test('GET route with body schema should throw - shorthand', t => {
337
+ t.plan(1)
338
+
339
+ const fastify = Fastify()
340
+
341
+ t.throws(() => {
342
+ fastify.get('/shouldThrow', {
343
+ schema: {
344
+ body: {}
345
+ }
346
+ },
347
+ function (req, reply) {
348
+ reply.send({ hello: 'world' })
349
+ }
350
+ )
351
+ }, new Error('Body validation schema for GET:/shouldThrow route is not supported!'))
352
+ })
353
+
354
+ test('HEAD route with body schema should throw - shorthand', t => {
355
+ t.plan(1)
356
+
357
+ const fastify = Fastify()
358
+
359
+ t.throws(() => {
360
+ fastify.head('/shouldThrow2', {
361
+ schema: {
362
+ body: {}
363
+ }
364
+ },
365
+ function (req, reply) {
366
+ reply.send({ hello: 'world' })
367
+ }
368
+ )
369
+ }, new Error('Body validation schema for HEAD:/shouldThrow2 route is not supported!'))
370
+ })
@@ -0,0 +1,142 @@
1
+ 'use strict'
2
+
3
+ const t = require('tap')
4
+ const test = t.test
5
+ const sget = require('simple-get').concat
6
+ const Fastify = require('../fastify')
7
+ const { FST_ERR_INVALID_URL } = require('../lib/errors')
8
+ const { getServerUrl } = require('./helper')
9
+
10
+ test('Request and Reply share the route config', async t => {
11
+ t.plan(3)
12
+
13
+ const fastify = Fastify()
14
+
15
+ const config = {
16
+ this: 'is a string',
17
+ thisIs: function aFunction () {}
18
+ }
19
+
20
+ fastify.route({
21
+ method: 'GET',
22
+ url: '/',
23
+ config,
24
+ handler: (req, reply) => {
25
+ t.same(req.context, reply.context)
26
+ t.same(req.context.config, reply.context.config)
27
+ t.match(req.context.config, config, 'there are url and method additional properties')
28
+
29
+ reply.send({ hello: 'world' })
30
+ }
31
+ })
32
+
33
+ await fastify.inject('/')
34
+ })
35
+
36
+ test('Will not try to re-createprefixed HEAD route if it already exists and exposeHeadRoutes is true', async (t) => {
37
+ t.plan(1)
38
+
39
+ const fastify = Fastify({ exposeHeadRoutes: true })
40
+
41
+ fastify.register((scope, opts, next) => {
42
+ scope.route({
43
+ method: 'HEAD',
44
+ path: '/route',
45
+ handler: (req, reply) => {
46
+ reply.header('content-type', 'text/plain')
47
+ reply.send('custom HEAD response')
48
+ }
49
+ })
50
+ scope.route({
51
+ method: 'GET',
52
+ path: '/route',
53
+ handler: (req, reply) => {
54
+ reply.send({ ok: true })
55
+ }
56
+ })
57
+
58
+ next()
59
+ }, { prefix: '/prefix' })
60
+
61
+ await fastify.ready()
62
+
63
+ t.ok(true)
64
+ })
65
+
66
+ test('route with non-english characters', t => {
67
+ t.plan(4)
68
+
69
+ const fastify = Fastify()
70
+
71
+ fastify.get('/föö', (request, reply) => {
72
+ reply.send('here /föö')
73
+ })
74
+
75
+ fastify.listen({ port: 0 }, err => {
76
+ t.error(err)
77
+ t.teardown(() => { fastify.close() })
78
+
79
+ sget({
80
+ method: 'GET',
81
+ url: getServerUrl(fastify) + encodeURI('/föö')
82
+ }, (err, response, body) => {
83
+ t.error(err)
84
+ t.equal(response.statusCode, 200)
85
+ t.equal(body.toString(), 'here /föö')
86
+ })
87
+ })
88
+ })
89
+
90
+ test('invalid url attribute - non string URL', t => {
91
+ t.plan(1)
92
+ const fastify = Fastify()
93
+
94
+ try {
95
+ fastify.get(/^\/(donations|skills|blogs)/, () => { })
96
+ } catch (error) {
97
+ t.equal(error.code, FST_ERR_INVALID_URL().code)
98
+ }
99
+ })
100
+
101
+ test('exposeHeadRoute should not reuse the same route option', async t => {
102
+ t.plan(2)
103
+
104
+ const fastify = Fastify()
105
+
106
+ // we update the onRequest hook in onRoute hook
107
+ // if we reuse the same route option
108
+ // that means we will append another function inside the array
109
+ fastify.addHook('onRoute', function (routeOption) {
110
+ if (Array.isArray(routeOption.onRequest)) {
111
+ routeOption.onRequest.push(() => {})
112
+ } else {
113
+ routeOption.onRequest = [() => {}]
114
+ }
115
+ })
116
+
117
+ fastify.addHook('onRoute', function (routeOption) {
118
+ t.equal(routeOption.onRequest.length, 1)
119
+ })
120
+
121
+ fastify.route({
122
+ method: 'GET',
123
+ path: '/more-coffee',
124
+ async handler () {
125
+ return 'hello world'
126
+ }
127
+ })
128
+ })
129
+
130
+ test('using fastify.all when a catchall is defined does not degrade performance', { timeout: 30000 }, async t => {
131
+ t.plan(1)
132
+
133
+ const fastify = Fastify()
134
+
135
+ fastify.get('/*', async (_, reply) => reply.json({ ok: true }))
136
+
137
+ for (let i = 0; i < 100; i++) {
138
+ fastify.all(`/${i}`, async (_, reply) => reply.json({ ok: true }))
139
+ }
140
+
141
+ t.pass()
142
+ })
@@ -0,0 +1,108 @@
1
+ 'use strict'
2
+
3
+ const t = require('tap')
4
+ const test = t.test
5
+ const sget = require('simple-get').concat
6
+ const fs = require('node:fs')
7
+ const Fastify = require('../fastify')
8
+
9
+ test('should respond with a stream', t => {
10
+ t.plan(6)
11
+ const fastify = Fastify()
12
+
13
+ fastify.get('/', function (req, reply) {
14
+ const stream = fs.createReadStream(__filename, 'utf8')
15
+ reply.code(200).send(stream)
16
+ })
17
+
18
+ fastify.listen({ port: 0 }, err => {
19
+ t.error(err)
20
+ t.teardown(() => { fastify.close() })
21
+
22
+ sget(`http://localhost:${fastify.server.address().port}`, function (err, response, data) {
23
+ t.error(err)
24
+ t.equal(response.headers['content-type'], undefined)
25
+ t.equal(response.statusCode, 200)
26
+
27
+ fs.readFile(__filename, (err, expected) => {
28
+ t.error(err)
29
+ t.equal(expected.toString(), data.toString())
30
+ })
31
+ })
32
+ })
33
+ })
34
+
35
+ test('should respond with a stream (error)', t => {
36
+ t.plan(3)
37
+ const fastify = Fastify()
38
+
39
+ fastify.get('/error', function (req, reply) {
40
+ const stream = fs.createReadStream('not-existing-file', 'utf8')
41
+ reply.code(200).send(stream)
42
+ })
43
+
44
+ fastify.listen({ port: 0 }, err => {
45
+ t.error(err)
46
+ t.teardown(() => { fastify.close() })
47
+
48
+ sget(`http://localhost:${fastify.server.address().port}/error`, function (err, response) {
49
+ t.error(err)
50
+ t.equal(response.statusCode, 500)
51
+ })
52
+ })
53
+ })
54
+
55
+ test('should trigger the onSend hook', t => {
56
+ t.plan(4)
57
+ const fastify = Fastify()
58
+
59
+ fastify.get('/', (req, reply) => {
60
+ reply.send(fs.createReadStream(__filename, 'utf8'))
61
+ })
62
+
63
+ fastify.addHook('onSend', (req, reply, payload, done) => {
64
+ t.ok(payload._readableState)
65
+ reply.header('Content-Type', 'application/javascript')
66
+ done()
67
+ })
68
+
69
+ fastify.inject({
70
+ url: '/'
71
+ }, (err, res) => {
72
+ t.error(err)
73
+ t.equal(res.headers['content-type'], 'application/javascript')
74
+ t.equal(res.payload, fs.readFileSync(__filename, 'utf8'))
75
+ fastify.close()
76
+ })
77
+ })
78
+
79
+ test('should trigger the onSend hook only twice if pumping the stream fails, first with the stream, second with the serialized error', t => {
80
+ t.plan(5)
81
+ const fastify = Fastify()
82
+
83
+ fastify.get('/', (req, reply) => {
84
+ reply.send(fs.createReadStream('not-existing-file', 'utf8'))
85
+ })
86
+
87
+ let counter = 0
88
+ fastify.addHook('onSend', (req, reply, payload, done) => {
89
+ if (counter === 0) {
90
+ t.ok(payload._readableState)
91
+ } else if (counter === 1) {
92
+ const error = JSON.parse(payload)
93
+ t.equal(error.statusCode, 500)
94
+ }
95
+ counter++
96
+ done()
97
+ })
98
+
99
+ fastify.listen({ port: 0 }, err => {
100
+ t.error(err)
101
+ t.teardown(() => { fastify.close() })
102
+
103
+ sget(`http://localhost:${fastify.server.address().port}`, function (err, response) {
104
+ t.error(err)
105
+ t.equal(response.statusCode, 500)
106
+ })
107
+ })
108
+ })