fastify 5.6.2 → 5.7.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 (51) hide show
  1. package/README.md +34 -33
  2. package/SECURITY.md +45 -7
  3. package/SPONSORS.md +1 -1
  4. package/build/build-validation.js +1 -2
  5. package/build/sync-version.js +0 -1
  6. package/docs/Guides/Detecting-When-Clients-Abort.md +1 -1
  7. package/docs/Guides/Ecosystem.md +12 -15
  8. package/docs/Guides/Migration-Guide-V4.md +2 -1
  9. package/docs/Guides/Migration-Guide-V5.md +9 -0
  10. package/docs/Guides/Serverless.md +25 -7
  11. package/docs/Guides/Testing.md +2 -2
  12. package/docs/Reference/Decorators.md +4 -3
  13. package/docs/Reference/Encapsulation.md +2 -2
  14. package/docs/Reference/Logging.md +4 -0
  15. package/docs/Reference/Plugins.md +2 -2
  16. package/docs/Reference/Principles.md +2 -2
  17. package/docs/Reference/Reply.md +3 -2
  18. package/docs/Reference/Server.md +35 -4
  19. package/docs/Reference/TypeScript.md +2 -1
  20. package/docs/Reference/Validation-and-Serialization.md +7 -1
  21. package/examples/benchmark/webstream.js +27 -0
  22. package/fastify.d.ts +16 -21
  23. package/fastify.js +14 -9
  24. package/lib/config-validator.js +189 -223
  25. package/lib/error-handler.js +2 -5
  26. package/lib/error-status.js +14 -0
  27. package/lib/four-oh-four.js +2 -1
  28. package/lib/handle-request.js +6 -1
  29. package/lib/reply.js +53 -3
  30. package/lib/route.js +26 -12
  31. package/lib/schema-controller.js +2 -2
  32. package/lib/wrap-thenable.js +3 -0
  33. package/package.json +4 -4
  34. package/test/404s.test.js +69 -0
  35. package/test/diagnostics-channel/error-status.test.js +84 -0
  36. package/test/internals/schema-controller-perf.test.js +40 -0
  37. package/test/issue-4959.test.js +34 -9
  38. package/test/listen.1.test.js +9 -1
  39. package/test/logger/logging.test.js +38 -1
  40. package/test/router-options.test.js +169 -0
  41. package/test/server.test.js +4 -1
  42. package/test/types/fastify.test-d.ts +28 -7
  43. package/test/types/instance.test-d.ts +29 -21
  44. package/test/types/reply.test-d.ts +55 -4
  45. package/test/types/type-provider.test-d.ts +6 -6
  46. package/test/web-api.test.js +136 -0
  47. package/types/instance.d.ts +1 -1
  48. package/types/reply.d.ts +2 -2
  49. package/types/type-provider.d.ts +16 -0
  50. package/.vscode/settings.json +0 -22
  51. package/test/decorator-namespace.test._js_ +0 -30
@@ -2,11 +2,11 @@
2
2
 
3
3
  const statusCodes = require('node:http').STATUS_CODES
4
4
  const wrapThenable = require('./wrap-thenable.js')
5
+ const { setErrorStatusCode } = require('./error-status.js')
5
6
  const {
6
7
  kReplyHeaders,
7
8
  kReplyNextErrorHandler,
8
9
  kReplyIsRunningOnErrorHook,
9
- kReplyHasStatusCode,
10
10
  kRouteContext,
11
11
  kDisableRequestLogging
12
12
  } = require('./symbols.js')
@@ -81,10 +81,7 @@ function handleError (reply, error, cb) {
81
81
 
82
82
  function defaultErrorHandler (error, request, reply) {
83
83
  setErrorHeaders(error, reply)
84
- if (!reply[kReplyHasStatusCode] || reply.statusCode === 200) {
85
- const statusCode = error && (error.statusCode || error.status)
86
- reply.code(statusCode >= 400 ? statusCode : 500)
87
- }
84
+ setErrorStatusCode(reply, error)
88
85
  if (reply.statusCode < 500) {
89
86
  if (!reply.log[kDisableRequestLogging]) {
90
87
  reply.log.info(
@@ -0,0 +1,14 @@
1
+ 'use strict'
2
+
3
+ const {
4
+ kReplyHasStatusCode
5
+ } = require('./symbols')
6
+
7
+ function setErrorStatusCode (reply, err) {
8
+ if (!reply[kReplyHasStatusCode] || reply.statusCode === 200) {
9
+ const statusCode = err && (err.statusCode || err.status)
10
+ reply.code(statusCode >= 400 ? statusCode : 500)
11
+ }
12
+ }
13
+
14
+ module.exports = { setErrorStatusCode }
@@ -49,7 +49,8 @@ function fourOhFour (options) {
49
49
  function basic404 (request, reply) {
50
50
  const { url, method } = request.raw
51
51
  const message = `Route ${method}:${url} not found`
52
- if (!disableRequestLogging) {
52
+ const resolvedDisableRequestLogging = typeof disableRequestLogging === 'function' ? disableRequestLogging(request.raw) : disableRequestLogging
53
+ if (!resolvedDisableRequestLogging) {
53
54
  request.log.info(message)
54
55
  }
55
56
  reply.code(404).send({
@@ -4,6 +4,7 @@ const diagnostics = require('node:diagnostics_channel')
4
4
  const { validate: validateSchema } = require('./validation')
5
5
  const { preValidationHookRunner, preHandlerHookRunner } = require('./hooks')
6
6
  const wrapThenable = require('./wrap-thenable')
7
+ const { setErrorStatusCode } = require('./error-status')
7
8
  const {
8
9
  kReplyIsError,
9
10
  kRouteContext,
@@ -143,11 +144,13 @@ function preHandlerCallbackInner (err, request, reply, store) {
143
144
  try {
144
145
  if (err != null) {
145
146
  reply[kReplyIsError] = true
146
- reply.send(err)
147
147
  if (store) {
148
148
  store.error = err
149
+ // Set status code before publishing so subscribers see the correct value
150
+ setErrorStatusCode(reply, err)
149
151
  channels.error.publish(store)
150
152
  }
153
+ reply.send(err)
151
154
  return
152
155
  }
153
156
 
@@ -158,6 +161,8 @@ function preHandlerCallbackInner (err, request, reply, store) {
158
161
  } catch (err) {
159
162
  if (store) {
160
163
  store.error = err
164
+ // Set status code before publishing so subscribers see the correct value
165
+ setErrorStatusCode(reply, err)
161
166
  channels.error.publish(store)
162
167
  }
163
168
 
package/lib/reply.js CHANGED
@@ -1,7 +1,6 @@
1
1
  'use strict'
2
2
 
3
3
  const eos = require('node:stream').finished
4
- const Readable = require('node:stream').Readable
5
4
 
6
5
  const {
7
6
  kFourOhFourContext,
@@ -685,8 +684,59 @@ function sendWebStream (payload, res, reply) {
685
684
  if (payload.locked) {
686
685
  throw new FST_ERR_REP_READABLE_STREAM_LOCKED()
687
686
  }
688
- const nodeStream = Readable.fromWeb(payload)
689
- sendStream(nodeStream, res, reply)
687
+
688
+ let sourceOpen = true
689
+ let errorLogged = false
690
+ const reader = payload.getReader()
691
+
692
+ eos(res, function (err) {
693
+ if (sourceOpen) {
694
+ if (err != null && res.headersSent && !errorLogged) {
695
+ errorLogged = true
696
+ logStreamError(reply.log, err, res)
697
+ }
698
+ reader.cancel().catch(noop)
699
+ }
700
+ })
701
+
702
+ if (!res.headersSent) {
703
+ for (const key in reply[kReplyHeaders]) {
704
+ res.setHeader(key, reply[kReplyHeaders][key])
705
+ }
706
+ } else {
707
+ reply.log.warn('response will send, but you shouldn\'t use res.writeHead in stream mode')
708
+ }
709
+
710
+ function onRead (result) {
711
+ if (result.done) {
712
+ sourceOpen = false
713
+ sendTrailer(null, res, reply)
714
+ return
715
+ }
716
+ /* c8 ignore next 5 - race condition: eos handler typically fires first */
717
+ if (res.destroyed) {
718
+ sourceOpen = false
719
+ reader.cancel().catch(noop)
720
+ return
721
+ }
722
+ res.write(result.value)
723
+ reader.read().then(onRead, onReadError)
724
+ }
725
+
726
+ function onReadError (err) {
727
+ sourceOpen = false
728
+ if (res.headersSent || reply.request.raw.aborted === true) {
729
+ if (!errorLogged) {
730
+ errorLogged = true
731
+ logStreamError(reply.log, err, reply)
732
+ }
733
+ res.destroy()
734
+ } else {
735
+ onErrorHook(reply, err)
736
+ }
737
+ }
738
+
739
+ reader.read().then(onRead, onReadError)
690
740
  }
691
741
 
692
742
  function sendStream (payload, res, reply) {
package/lib/route.js CHANGED
@@ -29,8 +29,6 @@ const {
29
29
  FST_ERR_HOOK_INVALID_ASYNC_HANDLER
30
30
  } = require('./errors')
31
31
 
32
- const { FSTDEP022 } = require('./warnings')
33
-
34
32
  const {
35
33
  kRoutePrefix,
36
34
  kSupportedHTTPMethods,
@@ -53,6 +51,7 @@ const {
53
51
  const { buildErrorHandler } = require('./error-handler')
54
52
  const { createChildLogger } = require('./logger-factory.js')
55
53
  const { getGenReqId } = require('./req-id-gen-factory.js')
54
+ const { FSTDEP022 } = require('./warnings')
56
55
 
57
56
  const routerKeys = [
58
57
  'allowUnsafeRegex',
@@ -69,7 +68,7 @@ const routerKeys = [
69
68
  ]
70
69
 
71
70
  function buildRouting (options) {
72
- const router = FindMyWay(options.config)
71
+ const router = FindMyWay(options)
73
72
 
74
73
  let avvio
75
74
  let fourOhFour
@@ -78,6 +77,7 @@ function buildRouting (options) {
78
77
  let setupResponseListeners
79
78
  let throwIfAlreadyStarted
80
79
  let disableRequestLogging
80
+ let disableRequestLoggingFn
81
81
  let ignoreTrailingSlash
82
82
  let ignoreDuplicateSlashes
83
83
  let return503OnClosing
@@ -94,13 +94,17 @@ function buildRouting (options) {
94
94
  setup (options, fastifyArgs) {
95
95
  avvio = fastifyArgs.avvio
96
96
  fourOhFour = fastifyArgs.fourOhFour
97
+ logger = options.logger
97
98
  hasLogger = fastifyArgs.hasLogger
98
99
  setupResponseListeners = fastifyArgs.setupResponseListeners
99
100
  throwIfAlreadyStarted = fastifyArgs.throwIfAlreadyStarted
100
101
 
101
- logger = options.logger
102
102
  globalExposeHeadRoutes = options.exposeHeadRoutes
103
103
  disableRequestLogging = options.disableRequestLogging
104
+ if (typeof disableRequestLogging === 'function') {
105
+ disableRequestLoggingFn = options.disableRequestLogging
106
+ }
107
+
104
108
  ignoreTrailingSlash = options.routerOptions.ignoreTrailingSlash
105
109
  ignoreDuplicateSlashes = options.routerOptions.ignoreDuplicateSlashes
106
110
  return503OnClosing = Object.hasOwn(options, 'return503OnClosing') ? options.return503OnClosing : true
@@ -373,9 +377,8 @@ function buildRouting (options) {
373
377
  context.logSerializers = opts.logSerializers
374
378
  context.attachValidation = opts.attachValidation
375
379
  context[kReplySerializerDefault] = this[kReplySerializerDefault]
376
- context.schemaErrorFormatter = opts.schemaErrorFormatter ||
377
- this[kSchemaErrorFormatter] ||
378
- context.schemaErrorFormatter
380
+ context.schemaErrorFormatter =
381
+ opts.schemaErrorFormatter || this[kSchemaErrorFormatter] || context.schemaErrorFormatter
379
382
 
380
383
  // Run hooks and more
381
384
  avvio.once('preReady', () => {
@@ -402,9 +405,11 @@ function buildRouting (options) {
402
405
  context.schema = normalizeSchema(context.schema, this.initialConfig)
403
406
 
404
407
  const schemaController = this[kSchemaController]
405
- if (!opts.validatorCompiler && (
406
- opts.schema.body || opts.schema.headers || opts.schema.querystring || opts.schema.params
407
- )) {
408
+ const hasValidationSchema = opts.schema.body ||
409
+ opts.schema.headers ||
410
+ opts.schema.querystring ||
411
+ opts.schema.params
412
+ if (!opts.validatorCompiler && hasValidationSchema) {
408
413
  schemaController.setupValidator(this[kOptions])
409
414
  }
410
415
  try {
@@ -455,7 +460,8 @@ function buildRouting (options) {
455
460
  loggerOpts.serializers = context.logSerializers
456
461
  }
457
462
  const childLogger = createChildLogger(context, logger, req, id, loggerOpts)
458
- childLogger[kDisableRequestLogging] = disableRequestLogging
463
+ // Set initial value; will be re-evaluated after FastifyRequest is constructed if it's a function
464
+ childLogger[kDisableRequestLogging] = disableRequestLoggingFn ? false : disableRequestLogging
459
465
 
460
466
  if (closing === true) {
461
467
  /* istanbul ignore next mac, windows */
@@ -499,7 +505,15 @@ function buildRouting (options) {
499
505
 
500
506
  const request = new context.Request(id, params, req, query, childLogger, context)
501
507
  const reply = new context.Reply(res, request, childLogger)
502
- if (disableRequestLogging === false) {
508
+
509
+ // Evaluate disableRequestLogging after FastifyRequest is constructed
510
+ // so the caller has access to decorations and customizations
511
+ const resolvedDisableRequestLogging = disableRequestLoggingFn
512
+ ? disableRequestLoggingFn(request)
513
+ : disableRequestLogging
514
+ childLogger[kDisableRequestLogging] = resolvedDisableRequestLogging
515
+
516
+ if (resolvedDisableRequestLogging === false) {
503
517
  childLogger.info({ req: request }, 'incoming request')
504
518
  }
505
519
 
@@ -1,8 +1,6 @@
1
1
  'use strict'
2
2
 
3
3
  const { buildSchemas } = require('./schemas')
4
- const SerializerSelector = require('@fastify/fast-json-stringify-compiler')
5
- const ValidatorSelector = require('@fastify/ajv-compiler')
6
4
 
7
5
  /**
8
6
  * Called at every fastify context that is being created.
@@ -21,9 +19,11 @@ function buildSchemaController (parentSchemaCtrl, opts) {
21
19
  }, opts?.compilersFactory)
22
20
 
23
21
  if (!compilersFactory.buildValidator) {
22
+ const ValidatorSelector = require('@fastify/ajv-compiler')
24
23
  compilersFactory.buildValidator = ValidatorSelector()
25
24
  }
26
25
  if (!compilersFactory.buildSerializer) {
26
+ const SerializerSelector = require('@fastify/fast-json-stringify-compiler')
27
27
  compilersFactory.buildSerializer = SerializerSelector()
28
28
  }
29
29
 
@@ -4,6 +4,7 @@ const {
4
4
  kReplyIsError,
5
5
  kReplyHijacked
6
6
  } = require('./symbols')
7
+ const { setErrorStatusCode } = require('./error-status')
7
8
 
8
9
  const diagnostics = require('node:diagnostics_channel')
9
10
  const channels = diagnostics.tracingChannel('fastify.request.handler')
@@ -52,6 +53,8 @@ function wrapThenable (thenable, reply, store) {
52
53
  }, function (err) {
53
54
  if (store) {
54
55
  store.error = err
56
+ // Set status code before publishing so subscribers see the correct value
57
+ setErrorStatusCode(reply, err)
55
58
  channels.error.publish(store) // note that error happens before asyncStart
56
59
  channels.asyncStart.publish(store)
57
60
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fastify",
3
- "version": "5.6.2",
3
+ "version": "5.7.1",
4
4
  "description": "Fast and low overhead web framework, for Node.js",
5
5
  "main": "fastify.js",
6
6
  "type": "commonjs",
@@ -171,7 +171,7 @@
171
171
  "@sinonjs/fake-timers": "^11.2.2",
172
172
  "@stylistic/eslint-plugin": "^5.1.0",
173
173
  "@stylistic/eslint-plugin-js": "^4.1.0",
174
- "@types/node": "^24.0.12",
174
+ "@types/node": "^25.0.3",
175
175
  "ajv": "^8.12.0",
176
176
  "ajv-errors": "^3.0.0",
177
177
  "ajv-formats": "^3.0.1",
@@ -191,7 +191,7 @@
191
191
  "joi": "^18.0.1",
192
192
  "json-schema-to-ts": "^3.0.1",
193
193
  "JSONStream": "^1.3.5",
194
- "markdownlint-cli2": "^0.18.1",
194
+ "markdownlint-cli2": "^0.20.0",
195
195
  "neostandard": "^0.12.0",
196
196
  "node-forge": "^1.3.1",
197
197
  "proxyquire": "^2.1.3",
@@ -203,7 +203,7 @@
203
203
  "yup": "^1.4.0"
204
204
  },
205
205
  "dependencies": {
206
- "@fastify/ajv-compiler": "^4.0.0",
206
+ "@fastify/ajv-compiler": "^4.0.5",
207
207
  "@fastify/error": "^4.0.0",
208
208
  "@fastify/fast-json-stringify-compiler": "^5.0.0",
209
209
  "@fastify/proxy-addr": "^5.0.0",
package/test/404s.test.js CHANGED
@@ -1845,6 +1845,26 @@ test('400 in case of bad url (pre find-my-way v2.2.0 was a 404)', async t => {
1845
1845
  done()
1846
1846
  })
1847
1847
  })
1848
+
1849
+ await t.test('Bad URL with special characters should be properly JSON escaped', (t, done) => {
1850
+ t.plan(3)
1851
+ const fastify = Fastify()
1852
+ fastify.get('/hello/:id', () => t.assert.fail('we should not be here'))
1853
+ fastify.inject({
1854
+ url: '/hello/%world%22test',
1855
+ method: 'GET'
1856
+ }, (err, response) => {
1857
+ t.assert.ifError(err)
1858
+ t.assert.strictEqual(response.statusCode, 400)
1859
+ t.assert.deepStrictEqual(JSON.parse(response.payload), {
1860
+ error: 'Bad Request',
1861
+ message: '\'/hello/%world%22test\' is not a valid url component',
1862
+ statusCode: 400,
1863
+ code: 'FST_ERR_BAD_URL'
1864
+ })
1865
+ done()
1866
+ })
1867
+ })
1848
1868
  })
1849
1869
 
1850
1870
  test('setNotFoundHandler should be chaining fastify instance', async t => {
@@ -1964,3 +1984,52 @@ test('hooks are applied to not found handlers /3', async t => {
1964
1984
  const { statusCode } = await fastify.inject('/')
1965
1985
  t.assert.strictEqual(statusCode, 401)
1966
1986
  })
1987
+
1988
+ test('should honor disableRequestLogging function for 404', async t => {
1989
+ t.plan(3)
1990
+
1991
+ const Writable = require('node:stream').Writable
1992
+
1993
+ const logStream = new Writable()
1994
+ logStream.logs = []
1995
+ logStream._write = function (chunk, encoding, callback) {
1996
+ this.logs.push(JSON.parse(chunk.toString()))
1997
+ callback()
1998
+ }
1999
+
2000
+ const fastify = Fastify({
2001
+ logger: {
2002
+ level: 'info',
2003
+ stream: logStream
2004
+ },
2005
+ disableRequestLogging: (req) => {
2006
+ // Disable logging for URLs containing 'silent'
2007
+ return req.url.includes('silent')
2008
+ }
2009
+ })
2010
+
2011
+ fastify.get('/', function (req, reply) {
2012
+ reply.send({ hello: 'world' })
2013
+ })
2014
+
2015
+ t.after(() => { fastify.close() })
2016
+
2017
+ // First request to a non-existent route (no 'silent' in URL) - should log
2018
+ const response1 = await fastify.inject({
2019
+ method: 'GET',
2020
+ url: '/not-found'
2021
+ })
2022
+ t.assert.strictEqual(response1.statusCode, 404)
2023
+
2024
+ // Second request to a non-existent route with 'silent' in URL - should not log
2025
+ const response2 = await fastify.inject({
2026
+ method: 'GET',
2027
+ url: '/silent-route'
2028
+ })
2029
+ t.assert.strictEqual(response2.statusCode, 404)
2030
+
2031
+ // Check logs: first request should have logged, second should not
2032
+ // We expect: incoming request, Route not found info, request completed (for first request only)
2033
+ const infoLogs = logStream.logs.filter(log => log.msg && log.msg.includes('Route GET:/not-found not found'))
2034
+ t.assert.strictEqual(infoLogs.length, 1, 'Should log 404 info only for non-silent route')
2035
+ })
@@ -5,6 +5,90 @@ const Fastify = require('../..')
5
5
  const statusCodes = require('node:http').STATUS_CODES
6
6
  const diagnostics = require('node:diagnostics_channel')
7
7
 
8
+ test('diagnostics channel error event should report correct status code', async (t) => {
9
+ t.plan(3)
10
+ const fastify = Fastify()
11
+ t.after(() => fastify.close())
12
+
13
+ let diagnosticsStatusCode
14
+
15
+ const channel = diagnostics.channel('tracing:fastify.request.handler:error')
16
+ const handler = (msg) => {
17
+ diagnosticsStatusCode = msg.reply.statusCode
18
+ }
19
+ channel.subscribe(handler)
20
+ t.after(() => channel.unsubscribe(handler))
21
+
22
+ fastify.get('/', async () => {
23
+ const err = new Error('test error')
24
+ err.statusCode = 503
25
+ throw err
26
+ })
27
+
28
+ const res = await fastify.inject('/')
29
+
30
+ t.assert.strictEqual(res.statusCode, 503)
31
+ t.assert.strictEqual(diagnosticsStatusCode, 503, 'diagnostics channel should report correct status code')
32
+ t.assert.strictEqual(diagnosticsStatusCode, res.statusCode, 'diagnostics status should match response status')
33
+ })
34
+
35
+ test('diagnostics channel error event should report 500 for errors without status', async (t) => {
36
+ t.plan(3)
37
+ const fastify = Fastify()
38
+ t.after(() => fastify.close())
39
+
40
+ let diagnosticsStatusCode
41
+
42
+ const channel = diagnostics.channel('tracing:fastify.request.handler:error')
43
+ const handler = (msg) => {
44
+ diagnosticsStatusCode = msg.reply.statusCode
45
+ }
46
+ channel.subscribe(handler)
47
+ t.after(() => channel.unsubscribe(handler))
48
+
49
+ fastify.get('/', async () => {
50
+ throw new Error('plain error without status')
51
+ })
52
+
53
+ const res = await fastify.inject('/')
54
+
55
+ t.assert.strictEqual(res.statusCode, 500)
56
+ t.assert.strictEqual(diagnosticsStatusCode, 500, 'diagnostics channel should report 500 for plain errors')
57
+ t.assert.strictEqual(diagnosticsStatusCode, res.statusCode, 'diagnostics status should match response status')
58
+ })
59
+
60
+ test('diagnostics channel error event should report correct status with custom error handler', async (t) => {
61
+ t.plan(3)
62
+ const fastify = Fastify()
63
+ t.after(() => fastify.close())
64
+
65
+ let diagnosticsStatusCode
66
+
67
+ const channel = diagnostics.channel('tracing:fastify.request.handler:error')
68
+ const handler = (msg) => {
69
+ diagnosticsStatusCode = msg.reply.statusCode
70
+ }
71
+ channel.subscribe(handler)
72
+ t.after(() => channel.unsubscribe(handler))
73
+
74
+ fastify.setErrorHandler((error, request, reply) => {
75
+ reply.status(503).send({ error: error.message })
76
+ })
77
+
78
+ fastify.get('/', async () => {
79
+ throw new Error('handler error')
80
+ })
81
+
82
+ const res = await fastify.inject('/')
83
+
84
+ // Note: The diagnostics channel fires before the custom error handler runs,
85
+ // so it reports 500 (default) rather than 503 (set by custom handler).
86
+ // This is expected behavior - the error channel reports the initial error state.
87
+ t.assert.strictEqual(res.statusCode, 503)
88
+ t.assert.strictEqual(diagnosticsStatusCode, 500, 'diagnostics channel reports status before custom handler')
89
+ t.assert.notStrictEqual(diagnosticsStatusCode, res.statusCode, 'custom handler can change status after diagnostics')
90
+ })
91
+
8
92
  test('Error.status property support', (t, done) => {
9
93
  t.plan(4)
10
94
  const fastify = Fastify()
@@ -0,0 +1,40 @@
1
+ const { sep } = require('node:path')
2
+ const { test } = require('node:test')
3
+ const Fastify = require('../../fastify')
4
+
5
+ test('SchemaController are NOT loaded when the controllers are custom', async t => {
6
+ const app = Fastify({
7
+ schemaController: {
8
+ compilersFactory: {
9
+ buildValidator: () => () => { },
10
+ buildSerializer: () => () => { }
11
+ }
12
+ }
13
+ })
14
+
15
+ await app.ready()
16
+
17
+ const loaded = Object.keys(require.cache)
18
+ const ajvModule = loaded.find((path) => path.includes(`@fastify${sep}ajv-compiler`))
19
+ const stringifyModule = loaded.find((path) => path.includes(`@fastify${sep}fast-json-stringify-compiler`))
20
+
21
+ t.assert.equal(ajvModule, undefined, 'Ajv compiler is loaded')
22
+ t.assert.equal(stringifyModule, undefined, 'Stringify compiler is loaded')
23
+ })
24
+
25
+ test('SchemaController are loaded when the controllers are not custom', async t => {
26
+ const app = Fastify()
27
+ await app.ready()
28
+
29
+ const loaded = Object.keys(require.cache)
30
+ const ajvModule = loaded.find((path) => path.includes(`@fastify${sep}ajv-compiler`))
31
+ const stringifyModule = loaded.find((path) => path.includes(`@fastify${sep}fast-json-stringify-compiler`))
32
+
33
+ t.after(() => {
34
+ delete require.cache[ajvModule]
35
+ delete require.cache[stringifyModule]
36
+ })
37
+
38
+ t.assert.ok(ajvModule, 'Ajv compiler is loaded')
39
+ t.assert.ok(stringifyModule, 'Stringify compiler is loaded')
40
+ })
@@ -11,7 +11,7 @@ const { setTimeout } = require('node:timers')
11
11
  *
12
12
  * @see https://github.com/fastify/fastify/issues/4959
13
13
  */
14
- function runBadClientCall (reqOptions, payload) {
14
+ function runBadClientCall (reqOptions, payload, waitBeforeDestroy) {
15
15
  let innerResolve, innerReject
16
16
  const promise = new Promise((resolve, reject) => {
17
17
  innerResolve = resolve
@@ -30,11 +30,25 @@ function runBadClientCall (reqOptions, payload) {
30
30
  innerReject(new Error('Request should have failed'))
31
31
  })
32
32
 
33
- // Kill the socket immediately (before sending data)
34
- req.on('socket', (socket) => {
35
- socket.on('connect', () => {
36
- setTimeout(() => { socket.destroy() }, 0)
37
- })
33
+ // Kill the socket after the request has been fully written.
34
+ // Destroying it on `connect` can race before any bytes are sent, making the
35
+ // server-side assertions (hooks/handler) non-deterministic.
36
+ //
37
+ // To keep the test deterministic, we optionally wait for a server-side signal
38
+ // (e.g. onSend entered) before aborting the client.
39
+ let socket
40
+ req.on('socket', (s) => { socket = s })
41
+ req.on('finish', () => {
42
+ if (waitBeforeDestroy && typeof waitBeforeDestroy.then === 'function') {
43
+ Promise.race([
44
+ waitBeforeDestroy,
45
+ new Promise(resolve => setTimeout(resolve, 200))
46
+ ]).then(() => {
47
+ if (socket) socket.destroy()
48
+ }, innerResolve)
49
+ return
50
+ }
51
+ setTimeout(() => { socket.destroy() }, 0)
38
52
  })
39
53
  req.on('error', innerResolve)
40
54
  req.write(postData)
@@ -47,6 +61,11 @@ test('should handle a socket error', async (t) => {
47
61
  t.plan(4)
48
62
  const fastify = Fastify()
49
63
 
64
+ let resolveOnSendEntered
65
+ const onSendEntered = new Promise((resolve) => {
66
+ resolveOnSendEntered = resolve
67
+ })
68
+
50
69
  function shouldNotHappen () {
51
70
  t.assert.fail('This should not happen')
52
71
  }
@@ -70,8 +89,14 @@ test('should handle a socket error', async (t) => {
70
89
  t.assert.ok('onSend hook called')
71
90
  request.onSendCalled = true
72
91
 
73
- // Introduce a delay
74
- await new Promise(resolve => setTimeout(resolve, 5))
92
+ if (resolveOnSendEntered) {
93
+ resolveOnSendEntered()
94
+ resolveOnSendEntered = null
95
+ }
96
+
97
+ // Introduce a delay (gives time for client-side abort to happen while the
98
+ // request has already been processed, exercising the original issue).
99
+ await new Promise(resolve => setTimeout(resolve, 50))
75
100
  return payload
76
101
  })
77
102
 
@@ -88,6 +113,6 @@ test('should handle a socket error', async (t) => {
88
113
  port: fastify.server.address().port,
89
114
  path: '/',
90
115
  method: 'PUT'
91
- }, { test: 'me' })
116
+ }, { test: 'me' }, onSendEntered)
92
117
  t.assert.equal(err.code, 'ECONNRESET')
93
118
  })
@@ -1,5 +1,6 @@
1
1
  'use strict'
2
2
 
3
+ const { networkInterfaces } = require('node:os')
3
4
  const { test, before } = require('node:test')
4
5
  const Fastify = require('..')
5
6
  const helper = require('./helper')
@@ -41,7 +42,14 @@ test('Async/await listen with arguments', async t => {
41
42
  })
42
43
  const addr = await fastify.listen({ port: 0, host: '0.0.0.0' })
43
44
  const address = fastify.server.address()
44
- t.assert.strictEqual(addr, `http://127.0.0.1:${address.port}`)
45
+ const { protocol, hostname, port, pathname } = new URL(addr)
46
+ t.assert.strictEqual(protocol, 'http:')
47
+ t.assert.ok(Object.values(networkInterfaces())
48
+ .flat()
49
+ .filter(({ internal }) => internal)
50
+ .some(({ address }) => address === hostname))
51
+ t.assert.strictEqual(pathname, '/')
52
+ t.assert.strictEqual(Number(port), address.port)
45
53
  t.assert.deepEqual(address, {
46
54
  address: '0.0.0.0',
47
55
  family: 'IPv4',