fastify 5.11.3 → 5.12.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.
package/SPONSORS.md CHANGED
@@ -19,6 +19,7 @@ or [GitHub Sponsors](https://github.com/sponsors/fastify)!
19
19
  - [atagon GmbH](https://github.com/atagon-GmbH)
20
20
  - [Photon](https://github.com/photon-hq)
21
21
  - [N-iX](https://www.n-ix.com/)
22
+ - [TestMu AI](https://www.testmuai.com/?utm_medium=sponsor&utm_source=fastify)
22
23
 
23
24
  ## Tier 2
24
25
 
@@ -256,9 +256,41 @@ curl -X POST https://$GOOGLE_REGION-$GOOGLE_PROJECT.cloudfunctions.net/me \
256
256
  {"message":"Hello Fastify!"}
257
257
  ```
258
258
 
259
+ #### Per-route logging
260
+
261
+ Google Cloud Functions exposes one entry point for your Fastify instance, so
262
+ the dashboard may show all HTTP traffic under that function. Instead of
263
+ deploying one function per endpoint, keep the single Fastify instance and emit
264
+ a structured log line after each request with the matched route:
265
+
266
+ ```js
267
+ fastify.addHook('onResponse', async (request, reply) => {
268
+ request.log.info({
269
+ route: request.routeOptions.url,
270
+ method: request.method,
271
+ statusCode: reply.statusCode,
272
+ responseTime: reply.elapsedTime
273
+ }, 'request completed')
274
+ })
275
+ ```
276
+
277
+ `request.routeOptions.url` is the route pattern, so `/users/123` and
278
+ `/users/456` are grouped under `/users/:id`. With Fastify's logger enabled, the
279
+ fields are emitted as structured log fields, as described in the [Cloud
280
+ Logging structured logging documentation](https://cloud.google.com/logging/docs/structured-logging)
281
+ and the [Cloud Functions logging guide](https://cloud.google.com/functions/docs/monitoring/logging).
282
+ In Cloud Logging, create a [log-based
283
+ metric](https://cloud.google.com/logging/docs/logs-based-metrics) or filter on
284
+ `jsonPayload.route` to get per-route request counts, latency, and error rates.
285
+
286
+ The same hook works when deploying through Firebase Functions with `onRequest`.
287
+
259
288
  ### References
260
289
  - [Google Cloud Functions - Node.js Quickstart
261
- ](https://docs.cloud.google.com/run/docs/quickstarts/functions/deploy-functions-gcloud)
290
+ ](https://cloud.google.com/run/docs/quickstarts/functions/deploy-functions-gcloud)
291
+ - [Cloud Logging - Structured Logging](https://cloud.google.com/logging/docs/structured-logging)
292
+ - [Cloud Logging - Log-based Metrics](https://cloud.google.com/logging/docs/logs-based-metrics)
293
+ - [Cloud Functions - Logging](https://cloud.google.com/functions/docs/monitoring/logging)
262
294
 
263
295
  ## Google Firebase Functions
264
296
 
@@ -594,4 +626,4 @@ https://vercel.com/templates/backend/fastify-on-vercel).
594
626
  [Fluid compute](https://vercel.com/docs/fluid-compute) currently
595
627
  requires an explicit opt-in. Learn more about enabling Fluid compute
596
628
  [here](
597
- https://vercel.com/docs/fluid-compute#enabling-fluid-compute).
629
+ https://vercel.com/docs/fluid-compute#enabling-fluid-compute).
@@ -6,6 +6,7 @@
6
6
  - [.code(statusCode)](#codestatuscode)
7
7
  - [.elapsedTime](#elapsedtime)
8
8
  - [.statusCode](#statuscode)
9
+ - [.mediaType](#mediatype)
9
10
  - [.server](#server)
10
11
  - [.header(key, value)](#headerkey-value)
11
12
  - [.headers(object)](#headersobject)
@@ -51,6 +52,7 @@ object that exposes the following functions and properties:
51
52
  - `.statusCode` - Read and set the HTTP status code.
52
53
  - `.elapsedTime` - Returns the amount of time passed
53
54
  since the request was received by Fastify.
55
+ - `.mediaType` - The media type extracted from `Content-Type` header.
54
56
  - `.server` - A reference to the fastify instance object.
55
57
  - `.header(name, value)` - Sets a response header.
56
58
  - `.headers(object)` - Sets all the keys of the object as response headers.
@@ -129,6 +131,18 @@ if (reply.statusCode >= 299) {
129
131
  reply.statusCode = 500
130
132
  }
131
133
  ```
134
+ ### .mediaType
135
+ <a id="mediatype"></a>
136
+
137
+ Returns the media type extracted from `Content-Type` header. When `Content-Type`
138
+ header is missing, it will return `undefined`.
139
+
140
+ ```js
141
+ if (reply.mediaType === 'image/gif') {
142
+ const versionBuffer = new Uint8Array(reply.payload, 3, 3)
143
+ reply.header('x-gif-version', new TextDecoder().decode(versionBuffer))
144
+ }
145
+ ```
132
146
 
133
147
  ### .server
134
148
  <a id="server"></a>
package/fastify.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const VERSION = '5.11.3'
3
+ const VERSION = '5.12.0'
4
4
 
5
5
  const Avvio = require('avvio')
6
6
  const http = require('node:http')
@@ -183,7 +183,10 @@ class ContentType {
183
183
 
184
184
  get isValid () { return this.#valid }
185
185
 
186
- get mediaType () { return `${this.#type}/${this.#subtype}` }
186
+ get mediaType () {
187
+ if (this.#valid === false) return undefined
188
+ return `${this.#type}/${this.#subtype}`
189
+ }
187
190
 
188
191
  get type () { return this.#type }
189
192
 
package/lib/reply.js CHANGED
@@ -93,6 +93,12 @@ Object.defineProperties(Reply.prototype, {
93
93
  return (this[kReplyEndTime] || now()) - this[kReplyStartTime]
94
94
  }
95
95
  },
96
+ mediaType: {
97
+ get () {
98
+ const contentTypeHeader = this[kReplyHeaders]['content-type']
99
+ return ContentType.from(contentTypeHeader).mediaType
100
+ }
101
+ },
96
102
  server: {
97
103
  get () {
98
104
  return this.request[kRouteContext].server
@@ -254,9 +260,13 @@ Reply.prototype.hasHeader = function (key) {
254
260
  }
255
261
 
256
262
  Reply.prototype.removeHeader = function (key) {
263
+ key = key.toLowerCase()
257
264
  // Node.js does not like headers with keys set to undefined,
258
265
  // so we have to delete the key.
259
- delete this[kReplyHeaders][key.toLowerCase()]
266
+ delete this[kReplyHeaders][key]
267
+ if (!this.raw.headersSent) {
268
+ this.raw.removeHeader(key)
269
+ }
260
270
  return this
261
271
  }
262
272
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fastify",
3
- "version": "5.11.3",
3
+ "version": "5.12.0",
4
4
  "description": "Fast and low overhead web framework, for Node.js",
5
5
  "main": "fastify.js",
6
6
  "type": "commonjs",
@@ -167,7 +167,6 @@
167
167
  ],
168
168
  "devDependencies": {
169
169
  "@jsumners/line-reporter": "^1.0.1",
170
- "@sinonjs/fake-timers": "^11.2.2",
171
170
  "@stylistic/eslint-plugin": "^5.1.0",
172
171
  "@stylistic/eslint-plugin-js": "^4.1.0",
173
172
  "@types/node": "^26.0.1",
@@ -214,7 +213,7 @@
214
213
  "find-my-way": "^9.6.0",
215
214
  "light-my-request": "^6.0.0",
216
215
  "pino": "^9.14.0 || ^10.1.0",
217
- "process-warning": "^5.0.0",
216
+ "process-warning": "^5.1.0",
218
217
  "rfdc": "^1.3.1",
219
218
  "secure-json-parse": "^4.0.0",
220
219
  "semver": "^7.6.0",
@@ -1,9 +1,11 @@
1
1
  'use strict'
2
2
 
3
3
  const { test } = require('node:test')
4
+ const { spyWarning } = require('process-warning')
4
5
  const Fastify = require('..')
5
6
  const keys = require('../lib/symbols')
6
7
  const { FST_ERR_CTP_ALREADY_PRESENT, FST_ERR_CTP_INVALID_TYPE, FST_ERR_CTP_INVALID_MEDIA_TYPE } = require('../lib/errors')
8
+ const { FSTSEC001 } = require('../lib/warnings')
7
9
 
8
10
  const first = function (req, payload, done) {}
9
11
  const second = function (req, payload, done) {}
@@ -488,41 +490,37 @@ test('Safeguard against content-type spoofing - string', async t => {
488
490
  })
489
491
 
490
492
  test('Warning against improper content-type - regexp', async t => {
491
- await t.test('improper regex - text plain', (t, done) => {
493
+ await t.test('improper regex - text plain', async (t) => {
492
494
  t.plan(2)
493
- const fastify = Fastify()
495
+ const spyData = spyWarning(FSTSEC001)
496
+ t.after(spyData.restore)
494
497
 
495
- process.on('warning', onWarning)
496
- function onWarning (warning) {
497
- t.assert.strictEqual(warning.name, 'FastifySecurity')
498
- t.assert.strictEqual(warning.code, 'FSTSEC001')
499
- done()
500
- }
501
- t.after(() => process.removeListener('warning', onWarning))
498
+ const fastify = Fastify()
502
499
 
503
500
  fastify.removeAllContentTypeParsers()
504
501
  fastify.addContentTypeParser(/text\/plain/, function (request, body, done) {
505
502
  done(null, body)
506
503
  })
504
+
505
+ await fastify.ready()
506
+ t.assert.deepStrictEqual(spyData.calls, [{ arguments: ['text\\/plain'], result: true }])
507
+ t.assert.strictEqual(spyData.callCount(), 1)
507
508
  })
508
509
 
509
- await t.test('improper regex - application json', (t, done) => {
510
+ await t.test('improper regex - application json', async (t) => {
510
511
  t.plan(2)
512
+ const spyData = spyWarning(FSTSEC001)
513
+ t.after(spyData.restore)
511
514
  const fastify = Fastify()
512
515
 
513
- process.on('warning', onWarning)
514
- function onWarning (warning) {
515
- t.assert.strictEqual(warning.name, 'FastifySecurity')
516
- t.assert.strictEqual(warning.code, 'FSTSEC001')
517
- done()
518
- }
519
- t.after(() => process.removeListener('warning', onWarning))
520
-
521
516
  fastify.removeAllContentTypeParsers()
522
517
 
523
518
  fastify.addContentTypeParser(/application\/json/, function (request, body, done) {
524
519
  done(null, body)
525
520
  })
521
+
522
+ t.assert.deepStrictEqual(spyData.calls, [{ arguments: ['application\\/json'], result: true }])
523
+ t.assert.deepEqual(spyData.callCount(), 1)
526
524
  })
527
525
  })
528
526
 
@@ -46,18 +46,22 @@ describe('ContentType class', () => {
46
46
  test('returns empty instance for empty value', (t) => {
47
47
  let found = new ContentType('')
48
48
  t.assert.equal(found.isEmpty, true)
49
+ t.assert.equal(found.mediaType, undefined)
49
50
 
50
51
  found = new ContentType('undefined')
51
52
  t.assert.equal(found.isEmpty, true)
53
+ t.assert.equal(found.mediaType, undefined)
52
54
 
53
55
  found = new ContentType()
54
56
  t.assert.equal(found.isEmpty, true)
57
+ t.assert.equal(found.mediaType, undefined)
55
58
  })
56
59
 
57
60
  test('indicates media type is not correct format', (t) => {
58
61
  let found = new ContentType('foo')
59
62
  t.assert.equal(found.isEmpty, true)
60
63
  t.assert.equal(found.isValid, false)
64
+ t.assert.equal(found.mediaType, undefined)
61
65
 
62
66
  found = new ContentType('foo /bar')
63
67
  t.assert.equal(found.isEmpty, true)
package/test/helper.js CHANGED
@@ -4,11 +4,42 @@ const dns = require('node:dns').promises
4
4
  const stream = require('node:stream')
5
5
  const { promisify } = require('node:util')
6
6
  const symbols = require('../lib/symbols')
7
- const { waitForCb } = require('./toolkit')
8
7
  const assert = require('node:assert')
9
8
 
10
9
  module.exports.sleep = promisify(setTimeout)
11
10
 
11
+ function waitForCb (options) {
12
+ let count = null
13
+ let done = false
14
+ let iResolve
15
+ let iReject
16
+
17
+ function stepIn () {
18
+ if (done) {
19
+ iReject(new Error('Unexpected done call'))
20
+ return
21
+ }
22
+
23
+ if (--count) {
24
+ return
25
+ }
26
+
27
+ done = true
28
+ iResolve()
29
+ }
30
+
31
+ const patience = new Promise((resolve, reject) => {
32
+ iResolve = resolve
33
+ iReject = reject
34
+ })
35
+
36
+ count = options.steps || 1
37
+ done = false
38
+
39
+ return { stepIn, patience }
40
+ }
41
+ module.exports.waitForCb = waitForCb
42
+
12
43
  /**
13
44
  * @param method HTTP request method
14
45
  * @param t node:test instance
@@ -494,3 +525,44 @@ module.exports.getServerUrl = function (app) {
494
525
  ? `http://[${address}]:${port}`
495
526
  : `http://${address}:${port}`
496
527
  }
528
+
529
+ module.exports.partialDeepStrictEqual = function partialDeepStrictEqual (actual, expected) {
530
+ if (typeof expected !== 'object' || expected === null) {
531
+ return actual === expected
532
+ }
533
+
534
+ if (typeof actual !== 'object' || actual === null) {
535
+ return false
536
+ }
537
+
538
+ if (Array.isArray(expected)) {
539
+ if (!Array.isArray(actual)) return false
540
+ if (expected.length > actual.length) return false
541
+
542
+ for (let i = 0; i < expected.length; i++) {
543
+ if (!partialDeepStrictEqual(actual[i], expected[i])) {
544
+ return false
545
+ }
546
+ }
547
+ return true
548
+ }
549
+
550
+ for (const key of Object.keys(expected)) {
551
+ if (!(key in actual)) return false
552
+ if (!partialDeepStrictEqual(actual[key], expected[key])) {
553
+ return false
554
+ }
555
+ }
556
+
557
+ return true
558
+ }
559
+
560
+ module.exports.assertNoWarning = function (t) {
561
+ function doNotWarn () {
562
+ t.assert.fail('no warning')
563
+ }
564
+ process.on('warning', doNotWarn)
565
+ t.after(() => {
566
+ process.off('warning', doNotWarn)
567
+ })
568
+ }
@@ -4,8 +4,7 @@ const { Readable } = require('node:stream')
4
4
  const { test, describe } = require('node:test')
5
5
  const Fastify = require('../fastify')
6
6
  const fs = require('node:fs')
7
- const { sleep } = require('./helper')
8
- const { waitForCb } = require('./toolkit')
7
+ const { sleep, waitForCb } = require('./helper')
9
8
 
10
9
  process.removeAllListeners('warning')
11
10
 
@@ -10,8 +10,7 @@ const symbols = require('../lib/symbols.js')
10
10
  const payload = { hello: 'world' }
11
11
  const proxyquire = require('proxyquire')
12
12
  const { connect } = require('node:net')
13
- const { sleep } = require('./helper')
14
- const { waitForCb } = require('./toolkit.js')
13
+ const { sleep, waitForCb } = require('./helper')
15
14
  const { fetch } = require('undici')
16
15
 
17
16
  process.removeAllListeners('warning')
@@ -34,7 +34,7 @@ const doGet = async function (url) {
34
34
  }
35
35
 
36
36
  test('Once called, Reply should return an object with methods', t => {
37
- t.plan(15)
37
+ t.plan(16)
38
38
  const response = { res: 'res' }
39
39
  const context = {
40
40
  config: { onSend: [] },
@@ -49,6 +49,7 @@ test('Once called, Reply should return an object with methods', t => {
49
49
  t.assert.strictEqual(typeof reply[kReplyErrorHandlerCalled], 'boolean')
50
50
  t.assert.strictEqual(typeof reply.send, 'function')
51
51
  t.assert.strictEqual(typeof reply.code, 'function')
52
+ t.assert.strictEqual(typeof reply.mediaType, 'string')
52
53
  t.assert.strictEqual(typeof reply.status, 'function')
53
54
  t.assert.strictEqual(typeof reply.header, 'function')
54
55
  t.assert.strictEqual(typeof reply.serialize, 'function')
@@ -1114,6 +1115,80 @@ test('reply.removeHeader can remove the value', async t => {
1114
1115
  await fetch(`${fastifyServer}/headers`)
1115
1116
  })
1116
1117
 
1118
+ test('reply.removeHeader removes raw headers', async t => {
1119
+ t.plan(9)
1120
+
1121
+ const fastify = require('../../')()
1122
+
1123
+ t.after(() => fastify.close())
1124
+
1125
+ fastify.get('/headers', function (req, reply) {
1126
+ reply.raw.setHeader('X-Foo', 'raw')
1127
+ t.assert.strictEqual(reply.getHeader('x-foo'), 'raw')
1128
+ t.assert.strictEqual(reply.getHeaders()['x-foo'], 'raw')
1129
+ t.assert.strictEqual(reply.hasHeader('x-foo'), true)
1130
+
1131
+ t.assert.strictEqual(reply.removeHeader('x-FoO'), reply)
1132
+ t.assert.strictEqual(reply.getHeader('x-foo'), undefined)
1133
+ t.assert.strictEqual(Object.hasOwn(reply.getHeaders(), 'x-foo'), false)
1134
+ t.assert.strictEqual(reply.hasHeader('x-foo'), false)
1135
+ t.assert.strictEqual(reply.removeHeader('X-FOO'), reply)
1136
+
1137
+ reply.send()
1138
+ })
1139
+
1140
+ const fastifyServer = await fastify.listen({ port: 0 })
1141
+ const response = await fetch(`${fastifyServer}/headers`)
1142
+ t.assert.strictEqual(response.headers.get('x-foo'), null)
1143
+ })
1144
+
1145
+ test('reply.removeHeader removes layered headers', async t => {
1146
+ t.plan(7)
1147
+
1148
+ const fastify = require('../../')()
1149
+
1150
+ t.after(() => fastify.close())
1151
+
1152
+ fastify.get('/headers', function (req, reply) {
1153
+ reply.raw.setHeader('x-foo', 'raw')
1154
+ reply.header('x-foo', 'fastify')
1155
+ t.assert.strictEqual(reply.getHeader('x-foo'), 'fastify')
1156
+ t.assert.strictEqual(reply.getHeaders()['x-foo'], 'fastify')
1157
+
1158
+ t.assert.strictEqual(reply.removeHeader('x-foo'), reply)
1159
+ t.assert.strictEqual(reply.getHeader('x-foo'), undefined)
1160
+ t.assert.strictEqual(Object.hasOwn(reply.getHeaders(), 'x-foo'), false)
1161
+ t.assert.strictEqual(reply.hasHeader('x-foo'), false)
1162
+
1163
+ reply.send()
1164
+ })
1165
+
1166
+ const fastifyServer = await fastify.listen({ port: 0 })
1167
+ const response = await fetch(`${fastifyServer}/headers`)
1168
+ t.assert.strictEqual(response.headers.get('x-foo'), null)
1169
+ })
1170
+
1171
+ test('reply.removeHeader does not throw after headers are sent', async t => {
1172
+ t.plan(3)
1173
+
1174
+ const fastify = require('../../')()
1175
+
1176
+ t.after(() => fastify.close())
1177
+
1178
+ fastify.get('/headers', function (req, reply) {
1179
+ reply.hijack()
1180
+ reply.raw.setHeader('x-foo', 'raw')
1181
+ reply.raw.flushHeaders()
1182
+ t.assert.strictEqual(reply.raw.headersSent, true)
1183
+ t.assert.doesNotThrow(() => reply.removeHeader('x-foo'))
1184
+ reply.raw.end()
1185
+ })
1186
+
1187
+ const fastifyServer = await fastify.listen({ port: 0 })
1188
+ const response = await fetch(`${fastifyServer}/headers`)
1189
+ t.assert.strictEqual(response.headers.get('x-foo'), 'raw')
1190
+ })
1191
+
1117
1192
  test('reply.header can reset the value', async t => {
1118
1193
  t.plan(1)
1119
1194
 
@@ -4,6 +4,7 @@ const { networkInterfaces } = require('node:os')
4
4
  const { test, before } = require('node:test')
5
5
  const Fastify = require('..')
6
6
  const helper = require('./helper')
7
+ const { assertNoWarning } = require('./helper')
7
8
 
8
9
  let localhost
9
10
  let localhostForURL
@@ -13,33 +14,22 @@ before(async function () {
13
14
  })
14
15
 
15
16
  test('listen works without arguments', async t => {
16
- const doNotWarn = () => {
17
- t.assert.fail('should not be deprecated')
18
- }
19
- process.on('warning', doNotWarn)
17
+ assertNoWarning(t)
20
18
 
21
19
  const fastify = Fastify()
22
- t.after(() => {
23
- fastify.close()
24
- process.removeListener('warning', doNotWarn)
25
- })
20
+
26
21
  await fastify.listen()
27
22
  const address = fastify.server.address()
28
23
  t.assert.strictEqual(address.address, localhost)
29
24
  t.assert.ok(address.port > 0)
25
+ await fastify.close()
30
26
  })
31
27
 
32
28
  test('Async/await listen with arguments', async t => {
33
- const doNotWarn = () => {
34
- t.assert.fail('should not be deprecated')
35
- }
36
- process.on('warning', doNotWarn)
29
+ assertNoWarning(t)
37
30
 
38
31
  const fastify = Fastify()
39
- t.after(() => {
40
- fastify.close()
41
- process.removeListener('warning', doNotWarn)
42
- })
32
+
43
33
  const addr = await fastify.listen({ port: 0, host: '0.0.0.0' })
44
34
  const address = fastify.server.address()
45
35
  const { protocol, hostname, port, pathname } = new URL(addr)
@@ -55,39 +45,27 @@ test('Async/await listen with arguments', async t => {
55
45
  family: 'IPv4',
56
46
  port: address.port
57
47
  })
48
+ await fastify.close()
58
49
  })
59
50
 
60
51
  test('listen accepts a callback', (t, done) => {
61
52
  t.plan(2)
62
- const doNotWarn = () => {
63
- t.assert.fail('should not be deprecated')
64
- }
65
- process.on('warning', doNotWarn)
53
+ assertNoWarning(t)
66
54
 
67
55
  const fastify = Fastify()
68
- t.after(() => {
69
- fastify.close()
70
- process.removeListener('warning', doNotWarn)
71
- })
56
+
72
57
  fastify.listen({ port: 0 }, (err) => {
73
58
  t.assert.ifError(err)
74
59
  t.assert.strictEqual(fastify.server.address().address, localhost)
75
- done()
60
+ fastify.close(done)
76
61
  })
77
62
  })
78
63
 
79
64
  test('listen accepts options and a callback', (t, done) => {
80
65
  t.plan(1)
81
- const doNotWarn = () => {
82
- t.assert.fail('should not be deprecated')
83
- }
84
- process.on('warning', doNotWarn)
66
+ assertNoWarning(t)
85
67
 
86
68
  const fastify = Fastify()
87
- t.after(() => {
88
- fastify.close()
89
- process.removeListener('warning', doNotWarn)
90
- })
91
69
  fastify.listen({
92
70
  port: 0,
93
71
  host: 'localhost',
@@ -98,7 +76,7 @@ test('listen accepts options and a callback', (t, done) => {
98
76
  ipv6Only: false
99
77
  }, (err) => {
100
78
  t.assert.ifError(err)
101
- done()
79
+ fastify.close(done)
102
80
  })
103
81
  })
104
82
 
@@ -118,37 +96,24 @@ test('listen after Promise.resolve()', (t, done) => {
118
96
  })
119
97
 
120
98
  test('listen works with undefined host', async t => {
121
- const doNotWarn = () => {
122
- t.assert.fail('should not be deprecated')
123
- }
124
- process.on('warning', doNotWarn)
99
+ assertNoWarning(t)
125
100
 
126
101
  const fastify = Fastify()
127
- t.after(() => fastify.close())
128
- t.after(() => {
129
- fastify.close()
130
- process.removeListener('warning', doNotWarn)
131
- })
132
102
  await fastify.listen({ host: undefined, port: 0 })
133
103
  const address = fastify.server.address()
134
104
  t.assert.strictEqual(address.address, localhost)
135
105
  t.assert.ok(address.port > 0)
106
+ await fastify.close()
136
107
  })
137
108
 
138
109
  test('listen works with null host', async t => {
139
- const doNotWarn = () => {
140
- t.fail('should not be deprecated')
141
- }
142
- process.on('warning', doNotWarn)
110
+ assertNoWarning(t)
143
111
 
144
112
  const fastify = Fastify()
145
- t.after(() => fastify.close())
146
- t.after(() => {
147
- fastify.close()
148
- process.removeListener('warning', doNotWarn)
149
- })
113
+
150
114
  await fastify.listen({ host: null, port: 0 })
151
115
  const address = fastify.server.address()
152
116
  t.assert.strictEqual(address.address, localhost)
153
117
  t.assert.ok(address.port > 0)
118
+ await fastify.close()
154
119
  })
@@ -2,8 +2,9 @@
2
2
 
3
3
  const { test } = require('node:test')
4
4
  const net = require('node:net')
5
- const Fastify = require('../fastify')
6
5
  const { once } = require('node:events')
6
+ const { spyWarning } = require('process-warning')
7
+ const Fastify = require('../fastify')
7
8
  const { FSTWRN003 } = require('../lib/warnings.js')
8
9
 
9
10
  function createDeferredPromise () {
@@ -102,21 +103,17 @@ test('same port conflict and success should not fire callback multiple times - p
102
103
  test('should emit a warning when using async callback', (t, done) => {
103
104
  t.plan(2)
104
105
 
105
- process.on('warning', onWarning)
106
- function onWarning (warning) {
107
- t.assert.strictEqual(warning.name, 'FastifyWarning')
108
- t.assert.strictEqual(warning.code, FSTWRN003.code)
109
- }
110
-
106
+ const spyData = spyWarning(FSTWRN003)
111
107
  const fastify = Fastify()
112
108
 
113
109
  t.after(async () => {
114
110
  await fastify.close()
115
- process.removeListener('warning', onWarning)
116
- FSTWRN003.emitted = false
111
+ spyData.restore()
117
112
  })
118
113
 
119
114
  fastify.listen({ port: 0 }, async function doNotUseAsyncCallback () {
115
+ t.assert.deepStrictEqual(spyData.calls, [{ arguments: ['listen method'], result: true }])
116
+ t.assert.strictEqual(spyData.callCount(), 1)
120
117
  done()
121
118
  })
122
119
  })
@@ -14,7 +14,7 @@ const helper = require('../helper')
14
14
  const { FST_ERR_LOG_INVALID_LOGGER } = require('../../lib/errors')
15
15
  const { once, on } = stream
16
16
  const { createTempFile, request } = require('./logger-test-utils')
17
- const { partialDeepStrictEqual } = require('../toolkit')
17
+ const { partialDeepStrictEqual } = require('../helper')
18
18
 
19
19
  t.test('logger instantiation', { timeout: 60000 }, async (t) => {
20
20
  let localhost
@@ -11,7 +11,7 @@ const { LogController } = require('../../lib/log-controller')
11
11
  const helper = require('../helper')
12
12
  const { once, on } = stream
13
13
  const { request } = require('./logger-test-utils')
14
- const { partialDeepStrictEqual } = require('../toolkit')
14
+ const { partialDeepStrictEqual } = require('../helper')
15
15
 
16
16
  t.test('logging', { timeout: 60000 }, async (t) => {
17
17
  let localhost
@@ -9,7 +9,7 @@ const Fastify = require('../../fastify')
9
9
  const helper = require('../helper')
10
10
  const { on } = stream
11
11
  const { request } = require('./logger-test-utils')
12
- const { partialDeepStrictEqual } = require('../toolkit')
12
+ const { partialDeepStrictEqual } = require('../helper')
13
13
 
14
14
  t.test('request', { timeout: 60000 }, async (t) => {
15
15
  let localhost
@@ -7,7 +7,7 @@ const split = require('split2')
7
7
  const pino = require('pino')
8
8
 
9
9
  const Fastify = require('../../fastify')
10
- const { partialDeepStrictEqual } = require('../toolkit')
10
+ const { partialDeepStrictEqual } = require('../helper')
11
11
  const { on } = stream
12
12
 
13
13
  t.test('response serialization', { timeout: 60000 }, async (t) => {
@@ -3,7 +3,6 @@
3
3
  const { test, describe } = require('node:test')
4
4
  const Fastify = require('../fastify')
5
5
  const fp = require('fastify-plugin')
6
- const fakeTimer = require('@sinonjs/fake-timers')
7
6
  const { FST_ERR_PLUGIN_INVALID_ASYNC_HANDLER } = require('../lib/errors')
8
7
 
9
8
  test('pluginTimeout', (t, testDone) => {
@@ -46,12 +45,14 @@ test('pluginTimeout - named function', (t, testDone) => {
46
45
 
47
46
  test('pluginTimeout default', (t, testDone) => {
48
47
  t.plan(5)
49
- const clock = fakeTimer.install({ shouldClearNativeTimers: true })
48
+ t.mock.timers.enable({
49
+ apis: ['setTimeout', 'Date']
50
+ })
50
51
 
51
52
  const fastify = Fastify()
52
53
  fastify.register(function (app, opts, done) {
53
54
  // default time elapsed without calling done
54
- clock.tick(10000)
55
+ t.mock.timers.tick(10000)
55
56
  })
56
57
 
57
58
  fastify.ready((err) => {
@@ -64,7 +65,7 @@ test('pluginTimeout default', (t, testDone) => {
64
65
  testDone()
65
66
  })
66
67
 
67
- t.after(clock.uninstall)
68
+ t.after(() => t.mock.timers.reset())
68
69
  })
69
70
 
70
71
  test('plugin metadata - version', (t, testDone) => {
@@ -0,0 +1,131 @@
1
+ 'use strict'
2
+
3
+ const { test } = require('node:test')
4
+ const Fastify = require('..')
5
+
6
+ test('reply.mediaType should match the content-type header', async (t) => {
7
+ t.plan(2)
8
+
9
+ const fastify = Fastify()
10
+
11
+ fastify.get('/', (request, reply) => {
12
+ reply.type('application/json')
13
+ t.assert.strictEqual(reply.mediaType, 'application/json')
14
+ reply.send({ mediaType: reply.mediaType })
15
+ })
16
+
17
+ const response = await fastify.inject({
18
+ method: 'GET',
19
+ url: '/'
20
+ })
21
+ const body = await response.json()
22
+ t.assert.strictEqual(body.mediaType, 'application/json')
23
+ })
24
+
25
+ test('reply.mediaType should strip the charset parameter', async (t) => {
26
+ t.plan(2)
27
+
28
+ const fastify = Fastify()
29
+
30
+ fastify.get('/', (request, reply) => {
31
+ reply.header('content-type', 'application/json; charset=utf-8')
32
+ t.assert.strictEqual(reply.mediaType, 'application/json')
33
+ reply.send({ mediaType: reply.mediaType })
34
+ })
35
+
36
+ const response = await fastify.inject({
37
+ method: 'GET',
38
+ url: '/'
39
+ })
40
+ const body = await response.json()
41
+ t.assert.strictEqual(body.mediaType, 'application/json')
42
+ })
43
+
44
+ test('reply.mediaType should strip the space', async (t) => {
45
+ t.plan(2)
46
+
47
+ const fastify = Fastify()
48
+
49
+ fastify.get('/', (request, reply) => {
50
+ reply.header('content-type', ' application/json ; charset=utf-8')
51
+ t.assert.strictEqual(reply.mediaType, 'application/json')
52
+ reply.send({ mediaType: reply.mediaType })
53
+ })
54
+
55
+ const response = await fastify.inject({
56
+ method: 'GET',
57
+ url: '/'
58
+ })
59
+ const body = await response.json()
60
+ t.assert.strictEqual(body.mediaType, 'application/json')
61
+ })
62
+
63
+ test('reply.mediaType is undefined when content-type is not set', async (t) => {
64
+ t.plan(2)
65
+
66
+ const fastify = Fastify()
67
+
68
+ fastify.get('/', (request, reply) => {
69
+ t.assert.strictEqual(reply.mediaType, undefined)
70
+ reply.send({ mediaType: reply.mediaType })
71
+ })
72
+
73
+ const response = await fastify.inject({
74
+ method: 'GET',
75
+ url: '/'
76
+ })
77
+ const body = await response.json()
78
+ t.assert.strictEqual(body.mediaType, undefined)
79
+ })
80
+
81
+ test('reply.mediaType supported in hooks', async (t) => {
82
+ t.plan(5)
83
+
84
+ const fastify = Fastify()
85
+
86
+ fastify.get('/', {
87
+ preHandler: (request, reply, done) => {
88
+ reply.type('application/json')
89
+ t.assert.strictEqual(reply.mediaType, 'application/json')
90
+ done()
91
+ },
92
+ preSerialization: (request, reply, payload, done) => {
93
+ t.assert.strictEqual(reply.mediaType, 'application/json')
94
+ done(null, payload)
95
+ },
96
+ onSend: (request, reply, payload, done) => {
97
+ t.assert.strictEqual(reply.mediaType, 'application/json')
98
+ done(null, payload)
99
+ }
100
+ }, (request, reply) => {
101
+ t.assert.strictEqual(reply.mediaType, 'application/json')
102
+ reply.send({ mediaType: reply.mediaType })
103
+ })
104
+
105
+ const response = await fastify.inject({
106
+ method: 'GET',
107
+ url: '/'
108
+ })
109
+ const body = await response.json()
110
+ t.assert.strictEqual(body.mediaType, 'application/json')
111
+ })
112
+
113
+ test('reply.mediaType should reflect the last type set', async (t) => {
114
+ t.plan(2)
115
+
116
+ const fastify = Fastify()
117
+
118
+ fastify.get('/', (request, reply) => {
119
+ reply.type('application/json')
120
+ reply.type('text/plain')
121
+ t.assert.strictEqual(reply.mediaType, 'text/plain')
122
+ reply.send(`mediaType = ${reply.mediaType}`)
123
+ })
124
+
125
+ const response = await fastify.inject({
126
+ method: 'GET',
127
+ url: '/'
128
+ })
129
+ const body = response.body
130
+ t.assert.strictEqual(body, 'mediaType = text/plain')
131
+ })
@@ -2,7 +2,7 @@
2
2
 
3
3
  const { test } = require('node:test')
4
4
  const Fastify = require('..')
5
- const { waitForCb } = require('./toolkit')
5
+ const { waitForCb } = require('./helper')
6
6
 
7
7
  test('Prefix options should add a prefix for all the routes inside a register / 1', (t, testDone) => {
8
8
  t.plan(6)
@@ -4,6 +4,7 @@ const stream = require('node:stream')
4
4
  const { ReadableStream } = require('node:stream/web')
5
5
  const { test } = require('node:test')
6
6
  const Fastify = require('..')
7
+ const { assertNoWarning } = require('./helper')
7
8
 
8
9
  test('Creates a HEAD route for a GET one with prefixTrailingSlash', async (t) => {
9
10
  t.plan(1)
@@ -281,6 +282,8 @@ test('route onSend can be function or array of functions', async t => {
281
282
  })
282
283
 
283
284
  test('no warning for exposeHeadRoute', async t => {
285
+ assertNoWarning(t)
286
+
284
287
  const fastify = Fastify()
285
288
 
286
289
  fastify.route({
@@ -292,15 +295,5 @@ test('no warning for exposeHeadRoute', async t => {
292
295
  }
293
296
  })
294
297
 
295
- const listener = (w) => {
296
- t.assert.fail('no warning')
297
- }
298
-
299
- process.on('warning', listener)
300
-
301
- await fastify.listen({ port: 0 })
302
-
303
- process.removeListener('warning', listener)
304
-
305
- await fastify.close()
298
+ await fastify.ready()
306
299
  })
@@ -1,13 +1,14 @@
1
1
  'use strict'
2
2
 
3
3
  const { test } = require('node:test')
4
- const Fastify = require('..')
5
4
  const fp = require('fastify-plugin')
5
+ const { spyWarning } = require('process-warning')
6
+ const Fastify = require('..')
6
7
  const deepClone = require('rfdc')({ circles: true, proto: false })
7
8
  const Ajv = require('ajv')
8
9
  const { kSchemaController } = require('../lib/symbols.js')
9
10
  const { FSTWRN001 } = require('../lib/warnings')
10
- const { waitForCb } = require('./toolkit')
11
+ const { waitForCb } = require('./helper')
11
12
 
12
13
  const echoParams = (req, reply) => { reply.send(req.params) }
13
14
  const echoBody = (req, reply) => { reply.send(req.body) }
@@ -320,18 +321,10 @@ test('Should not change the input schemas', (t, testDone) => {
320
321
 
321
322
  test('Should emit warning if the schema headers is undefined', (t, testDone) => {
322
323
  t.plan(4)
323
- const fastify = Fastify()
324
+ const spyData = spyWarning(FSTWRN001)
325
+ t.after(spyData.restore)
324
326
 
325
- process.on('warning', onWarning)
326
- function onWarning (warning) {
327
- t.assert.strictEqual(warning.name, 'FastifyWarning')
328
- t.assert.strictEqual(warning.code, FSTWRN001.code)
329
- }
330
-
331
- t.after(() => {
332
- process.removeListener('warning', onWarning)
333
- FSTWRN001.emitted = false
334
- })
327
+ const fastify = Fastify()
335
328
 
336
329
  fastify.post('/:id', {
337
330
  handler: echoParams,
@@ -346,24 +339,18 @@ test('Should emit warning if the schema headers is undefined', (t, testDone) =>
346
339
  }, (error, res) => {
347
340
  t.assert.ifError(error)
348
341
  t.assert.strictEqual(res.statusCode, 200)
342
+ t.assert.deepStrictEqual(spyData.calls, [{ arguments: ['headers', 'POST', '/:id'], result: true }])
343
+ t.assert.strictEqual(spyData.callCount(), 1)
349
344
  testDone()
350
345
  })
351
346
  })
352
347
 
353
348
  test('Should emit warning if the schema body is undefined', (t, testDone) => {
354
349
  t.plan(4)
355
- const fastify = Fastify()
356
-
357
- process.on('warning', onWarning)
358
- function onWarning (warning) {
359
- t.assert.strictEqual(warning.name, 'FastifyWarning')
360
- t.assert.strictEqual(warning.code, FSTWRN001.code)
361
- }
350
+ const spyData = spyWarning(FSTWRN001)
351
+ t.after(spyData.restore)
362
352
 
363
- t.after(() => {
364
- process.removeListener('warning', onWarning)
365
- FSTWRN001.emitted = false
366
- })
353
+ const fastify = Fastify()
367
354
 
368
355
  fastify.post('/:id', {
369
356
  handler: echoParams,
@@ -378,24 +365,18 @@ test('Should emit warning if the schema body is undefined', (t, testDone) => {
378
365
  }, (error, res) => {
379
366
  t.assert.ifError(error)
380
367
  t.assert.strictEqual(res.statusCode, 200)
368
+ t.assert.deepStrictEqual(spyData.calls, [{ arguments: ['body', 'POST', '/:id'], result: true }])
369
+ t.assert.strictEqual(spyData.callCount(), 1)
381
370
  testDone()
382
371
  })
383
372
  })
384
373
 
385
374
  test('Should emit warning if the schema query is undefined', (t, testDone) => {
386
375
  t.plan(4)
387
- const fastify = Fastify()
376
+ const spyData = spyWarning(FSTWRN001)
377
+ t.after(spyData.restore)
388
378
 
389
- process.on('warning', onWarning)
390
- function onWarning (warning) {
391
- t.assert.strictEqual(warning.name, 'FastifyWarning')
392
- t.assert.strictEqual(warning.code, FSTWRN001.code)
393
- }
394
-
395
- t.after(() => {
396
- process.removeListener('warning', onWarning)
397
- FSTWRN001.emitted = false
398
- })
379
+ const fastify = Fastify()
399
380
 
400
381
  fastify.post('/:id', {
401
382
  handler: echoParams,
@@ -410,24 +391,18 @@ test('Should emit warning if the schema query is undefined', (t, testDone) => {
410
391
  }, (error, res) => {
411
392
  t.assert.ifError(error)
412
393
  t.assert.strictEqual(res.statusCode, 200)
394
+ t.assert.deepStrictEqual(spyData.calls, [{ arguments: ['querystring', 'POST', '/:id'], result: true }])
395
+ t.assert.strictEqual(spyData.callCount(), 1)
413
396
  testDone()
414
397
  })
415
398
  })
416
399
 
417
400
  test('Should emit warning if the schema params is undefined', (t, testDone) => {
418
401
  t.plan(4)
419
- const fastify = Fastify()
402
+ const spyData = spyWarning(FSTWRN001)
403
+ t.after(spyData.restore)
420
404
 
421
- process.on('warning', onWarning)
422
- function onWarning (warning) {
423
- t.assert.strictEqual(warning.name, 'FastifyWarning')
424
- t.assert.strictEqual(warning.code, FSTWRN001.code)
425
- }
426
-
427
- t.after(() => {
428
- process.removeListener('warning', onWarning)
429
- FSTWRN001.emitted = false
430
- })
405
+ const fastify = Fastify()
431
406
 
432
407
  fastify.post('/:id', {
433
408
  handler: echoParams,
@@ -442,31 +417,18 @@ test('Should emit warning if the schema params is undefined', (t, testDone) => {
442
417
  }, (error, res) => {
443
418
  t.assert.ifError(error)
444
419
  t.assert.strictEqual(res.statusCode, 200)
420
+ t.assert.deepStrictEqual(spyData.calls, [{ arguments: ['params', 'POST', '/:id'], result: true }])
421
+ t.assert.strictEqual(spyData.callCount(), 1)
445
422
  testDone()
446
423
  })
447
424
  })
448
425
 
449
426
  test('Should emit a warning for every route with undefined schema', (t, testDone) => {
450
- t.plan(16)
451
- const fastify = Fastify()
452
-
453
- let runs = 0
454
- const expectedWarningEmitted = [0, 1, 2, 3]
455
- // It emits 4 warnings:
456
- // - 2 - GET and HEAD for /undefinedParams/:id
457
- // - 2 - GET and HEAD for /undefinedBody/:id
458
- // => 3 x 4 assertions = 12 assertions
459
- function onWarning (warning) {
460
- t.assert.strictEqual(warning.name, 'FastifyWarning')
461
- t.assert.strictEqual(warning.code, FSTWRN001.code)
462
- t.assert.strictEqual(runs++, expectedWarningEmitted.shift())
463
- }
427
+ t.plan(9)
428
+ const spyData = spyWarning(FSTWRN001)
429
+ t.after(spyData.restore)
464
430
 
465
- process.on('warning', onWarning)
466
- t.after(() => {
467
- process.removeListener('warning', onWarning)
468
- FSTWRN001.emitted = false
469
- })
431
+ const fastify = Fastify()
470
432
 
471
433
  fastify.get('/undefinedParams/:id', {
472
434
  handler: echoParams,
@@ -496,6 +458,15 @@ test('Should emit a warning for every route with undefined schema', (t, testDone
496
458
  }, (error, res) => {
497
459
  t.assert.ifError(error)
498
460
  t.assert.strictEqual(res.statusCode, 200)
461
+ // fastify.inject run in series
462
+ // last callback recieve all warnings at once
463
+ // GET /undefinedParams/123
464
+ t.assert.deepStrictEqual(spyData.calls[0], { arguments: ['params', 'GET', '/undefinedParams/:id'], result: true })
465
+ t.assert.deepStrictEqual(spyData.calls[1], { arguments: ['params', 'HEAD', '/undefinedParams/:id'], result: true })
466
+ // GET /undefinedBody/123
467
+ t.assert.deepStrictEqual(spyData.calls[2], { arguments: ['body', 'GET', '/undefinedBody/:id'], result: true })
468
+ t.assert.deepStrictEqual(spyData.calls[3], { arguments: ['body', 'HEAD', '/undefinedBody/:id'], result: true })
469
+ t.assert.strictEqual(spyData.callCount(), 4)
499
470
  testDone()
500
471
  })
501
472
  })
@@ -3,7 +3,7 @@
3
3
  const { test } = require('node:test')
4
4
  const Fastify = require('..')
5
5
  const ContentType = require('../lib/content-type')
6
- const { waitForCb } = require('./toolkit')
6
+ const { waitForCb } = require('./helper')
7
7
 
8
8
  const echoBody = (req, reply) => { reply.send(req.body) }
9
9
 
@@ -9,7 +9,7 @@ const Fastify = require('..')
9
9
  const ajvMergePatch = require('ajv-merge-patch')
10
10
  const ajvErrors = require('ajv-errors')
11
11
  const proxyquire = require('proxyquire')
12
- const { waitForCb } = require('./toolkit')
12
+ const { waitForCb } = require('./helper')
13
13
 
14
14
  test('Ajv plugins array parameter', (t, testDone) => {
15
15
  t.plan(3)
@@ -5,7 +5,7 @@ const Fastify = require('..')
5
5
 
6
6
  const AJV = require('ajv')
7
7
  const Schema = require('fluent-json-schema')
8
- const { waitForCb } = require('./toolkit')
8
+ const { waitForCb } = require('./helper')
9
9
  const { kRequestContentType } = require('../lib/symbols')
10
10
 
11
11
  const customSchemaCompilers = {
@@ -7,7 +7,7 @@ const resolve = require('node:path').resolve
7
7
  const zlib = require('node:zlib')
8
8
  const pipeline = require('node:stream').pipeline
9
9
  const Fastify = require('..')
10
- const { waitForCb } = require('./toolkit')
10
+ const { waitForCb } = require('./helper')
11
11
 
12
12
  test('onSend hook stream', t => {
13
13
  t.plan(4)
package/test/toolkit.js DELETED
@@ -1,63 +0,0 @@
1
- 'use strict'
2
-
3
- exports.waitForCb = function (options) {
4
- let count = null
5
- let done = false
6
- let iResolve
7
- let iReject
8
-
9
- function stepIn () {
10
- if (done) {
11
- iReject(new Error('Unexpected done call'))
12
- return
13
- }
14
-
15
- if (--count) {
16
- return
17
- }
18
-
19
- done = true
20
- iResolve()
21
- }
22
-
23
- const patience = new Promise((resolve, reject) => {
24
- iResolve = resolve
25
- iReject = reject
26
- })
27
-
28
- count = options.steps || 1
29
- done = false
30
-
31
- return { stepIn, patience }
32
- }
33
-
34
- exports.partialDeepStrictEqual = function partialDeepStrictEqual (actual, expected) {
35
- if (typeof expected !== 'object' || expected === null) {
36
- return actual === expected
37
- }
38
-
39
- if (typeof actual !== 'object' || actual === null) {
40
- return false
41
- }
42
-
43
- if (Array.isArray(expected)) {
44
- if (!Array.isArray(actual)) return false
45
- if (expected.length > actual.length) return false
46
-
47
- for (let i = 0; i < expected.length; i++) {
48
- if (!partialDeepStrictEqual(actual[i], expected[i])) {
49
- return false
50
- }
51
- }
52
- return true
53
- }
54
-
55
- for (const key of Object.keys(expected)) {
56
- if (!(key in actual)) return false
57
- if (!partialDeepStrictEqual(actual[key], expected[key])) {
58
- return false
59
- }
60
- }
61
-
62
- return true
63
- }