node-fastify 5.8.3

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 (354) hide show
  1. package/.borp.yaml +3 -0
  2. package/.markdownlint-cli2.yaml +22 -0
  3. package/.prettierignore +1 -0
  4. package/GOVERNANCE.md +4 -0
  5. package/LICENSE +21 -0
  6. package/PROJECT_CHARTER.md +126 -0
  7. package/README.md +423 -0
  8. package/SECURITY.md +220 -0
  9. package/SPONSORS.md +24 -0
  10. package/build/build-error-serializer.js +35 -0
  11. package/build/build-validation.js +169 -0
  12. package/build/sync-version.js +11 -0
  13. package/docs/Guides/Benchmarking.md +60 -0
  14. package/docs/Guides/Database.md +321 -0
  15. package/docs/Guides/Delay-Accepting-Requests.md +608 -0
  16. package/docs/Guides/Detecting-When-Clients-Abort.md +172 -0
  17. package/docs/Guides/Ecosystem.md +726 -0
  18. package/docs/Guides/Fluent-Schema.md +127 -0
  19. package/docs/Guides/Getting-Started.md +620 -0
  20. package/docs/Guides/Index.md +43 -0
  21. package/docs/Guides/Migration-Guide-V3.md +287 -0
  22. package/docs/Guides/Migration-Guide-V4.md +267 -0
  23. package/docs/Guides/Migration-Guide-V5.md +727 -0
  24. package/docs/Guides/Plugins-Guide.md +520 -0
  25. package/docs/Guides/Prototype-Poisoning.md +383 -0
  26. package/docs/Guides/Recommendations.md +378 -0
  27. package/docs/Guides/Serverless.md +604 -0
  28. package/docs/Guides/Style-Guide.md +246 -0
  29. package/docs/Guides/Testing.md +481 -0
  30. package/docs/Guides/Write-Plugin.md +103 -0
  31. package/docs/Guides/Write-Type-Provider.md +34 -0
  32. package/docs/Reference/ContentTypeParser.md +271 -0
  33. package/docs/Reference/Decorators.md +436 -0
  34. package/docs/Reference/Encapsulation.md +194 -0
  35. package/docs/Reference/Errors.md +377 -0
  36. package/docs/Reference/HTTP2.md +94 -0
  37. package/docs/Reference/Hooks.md +958 -0
  38. package/docs/Reference/Index.md +73 -0
  39. package/docs/Reference/LTS.md +86 -0
  40. package/docs/Reference/Lifecycle.md +99 -0
  41. package/docs/Reference/Logging.md +268 -0
  42. package/docs/Reference/Middleware.md +79 -0
  43. package/docs/Reference/Plugins.md +245 -0
  44. package/docs/Reference/Principles.md +73 -0
  45. package/docs/Reference/Reply.md +1001 -0
  46. package/docs/Reference/Request.md +295 -0
  47. package/docs/Reference/Routes.md +802 -0
  48. package/docs/Reference/Server.md +2389 -0
  49. package/docs/Reference/Type-Providers.md +256 -0
  50. package/docs/Reference/TypeScript.md +1729 -0
  51. package/docs/Reference/Validation-and-Serialization.md +1130 -0
  52. package/docs/Reference/Warnings.md +58 -0
  53. package/docs/index.md +24 -0
  54. package/docs/resources/encapsulation_context.drawio +1 -0
  55. package/docs/resources/encapsulation_context.svg +3 -0
  56. package/eslint.config.js +35 -0
  57. package/examples/asyncawait.js +38 -0
  58. package/examples/benchmark/body.json +3 -0
  59. package/examples/benchmark/hooks-benchmark-async-await.js +44 -0
  60. package/examples/benchmark/hooks-benchmark.js +52 -0
  61. package/examples/benchmark/parser.js +47 -0
  62. package/examples/benchmark/simple.js +30 -0
  63. package/examples/benchmark/webstream.js +27 -0
  64. package/examples/hooks.js +91 -0
  65. package/examples/http2.js +39 -0
  66. package/examples/https.js +38 -0
  67. package/examples/parser.js +53 -0
  68. package/examples/plugin.js +12 -0
  69. package/examples/route-prefix.js +38 -0
  70. package/examples/shared-schema.js +38 -0
  71. package/examples/simple-stream.js +20 -0
  72. package/examples/simple.js +32 -0
  73. package/examples/simple.mjs +27 -0
  74. package/examples/typescript-server.ts +79 -0
  75. package/examples/use-plugin.js +29 -0
  76. package/fastify.d.ts +253 -0
  77. package/fastify.js +985 -0
  78. package/integration/server.js +29 -0
  79. package/integration/test.sh +23 -0
  80. package/lib/config-validator.js +1266 -0
  81. package/lib/content-type-parser.js +413 -0
  82. package/lib/content-type.js +160 -0
  83. package/lib/context.js +98 -0
  84. package/lib/decorate.js +152 -0
  85. package/lib/error-handler.js +173 -0
  86. package/lib/error-serializer.js +134 -0
  87. package/lib/error-status.js +14 -0
  88. package/lib/errors.js +516 -0
  89. package/lib/four-oh-four.js +190 -0
  90. package/lib/handle-request.js +195 -0
  91. package/lib/head-route.js +45 -0
  92. package/lib/hooks.js +429 -0
  93. package/lib/initial-config-validation.js +37 -0
  94. package/lib/logger-factory.js +136 -0
  95. package/lib/logger-pino.js +68 -0
  96. package/lib/noop-set.js +10 -0
  97. package/lib/plugin-override.js +90 -0
  98. package/lib/plugin-utils.js +169 -0
  99. package/lib/promise.js +23 -0
  100. package/lib/reply.js +1030 -0
  101. package/lib/req-id-gen-factory.js +52 -0
  102. package/lib/request.js +391 -0
  103. package/lib/route.js +686 -0
  104. package/lib/schema-controller.js +164 -0
  105. package/lib/schemas.js +207 -0
  106. package/lib/server.js +441 -0
  107. package/lib/symbols.js +71 -0
  108. package/lib/validation.js +280 -0
  109. package/lib/warnings.js +57 -0
  110. package/lib/wrap-thenable.js +84 -0
  111. package/package.json +225 -0
  112. package/scripts/validate-ecosystem-links.js +179 -0
  113. package/test/404s.test.js +2035 -0
  114. package/test/500s.test.js +422 -0
  115. package/test/allow-unsafe-regex.test.js +92 -0
  116. package/test/als.test.js +65 -0
  117. package/test/async-await.test.js +705 -0
  118. package/test/async-dispose.test.js +20 -0
  119. package/test/async_hooks.test.js +52 -0
  120. package/test/body-limit.test.js +224 -0
  121. package/test/buffer.test.js +74 -0
  122. package/test/build/error-serializer.test.js +36 -0
  123. package/test/build/version.test.js +14 -0
  124. package/test/build-certificate.js +109 -0
  125. package/test/bundler/README.md +29 -0
  126. package/test/bundler/esbuild/bundler-test.js +32 -0
  127. package/test/bundler/esbuild/package.json +10 -0
  128. package/test/bundler/esbuild/src/fail-plugin-version.js +14 -0
  129. package/test/bundler/esbuild/src/index.js +9 -0
  130. package/test/bundler/webpack/bundler-test.js +32 -0
  131. package/test/bundler/webpack/package.json +11 -0
  132. package/test/bundler/webpack/src/fail-plugin-version.js +14 -0
  133. package/test/bundler/webpack/src/index.js +9 -0
  134. package/test/bundler/webpack/webpack.config.js +15 -0
  135. package/test/case-insensitive.test.js +102 -0
  136. package/test/chainable.test.js +40 -0
  137. package/test/child-logger-factory.test.js +128 -0
  138. package/test/client-timeout.test.js +38 -0
  139. package/test/close-pipelining.test.js +78 -0
  140. package/test/close.test.js +706 -0
  141. package/test/conditional-pino.test.js +47 -0
  142. package/test/connection-timeout.test.js +42 -0
  143. package/test/constrained-routes.test.js +1138 -0
  144. package/test/content-length.test.js +174 -0
  145. package/test/content-parser.test.js +739 -0
  146. package/test/content-type.test.js +181 -0
  147. package/test/context-config.test.js +164 -0
  148. package/test/custom-http-server.test.js +118 -0
  149. package/test/custom-parser-async.test.js +59 -0
  150. package/test/custom-parser.0.test.js +701 -0
  151. package/test/custom-parser.1.test.js +266 -0
  152. package/test/custom-parser.2.test.js +91 -0
  153. package/test/custom-parser.3.test.js +208 -0
  154. package/test/custom-parser.4.test.js +218 -0
  155. package/test/custom-parser.5.test.js +130 -0
  156. package/test/custom-querystring-parser.test.js +129 -0
  157. package/test/decorator.test.js +1330 -0
  158. package/test/delete.test.js +344 -0
  159. package/test/diagnostics-channel/404.test.js +49 -0
  160. package/test/diagnostics-channel/async-delay-request.test.js +65 -0
  161. package/test/diagnostics-channel/async-request.test.js +64 -0
  162. package/test/diagnostics-channel/error-before-handler.test.js +35 -0
  163. package/test/diagnostics-channel/error-request.test.js +53 -0
  164. package/test/diagnostics-channel/error-status.test.js +123 -0
  165. package/test/diagnostics-channel/init.test.js +50 -0
  166. package/test/diagnostics-channel/sync-delay-request.test.js +49 -0
  167. package/test/diagnostics-channel/sync-request-reply.test.js +51 -0
  168. package/test/diagnostics-channel/sync-request.test.js +54 -0
  169. package/test/encapsulated-child-logger-factory.test.js +69 -0
  170. package/test/encapsulated-error-handler.test.js +237 -0
  171. package/test/esm/errorCodes.test.mjs +10 -0
  172. package/test/esm/esm.test.mjs +13 -0
  173. package/test/esm/index.test.js +8 -0
  174. package/test/esm/named-exports.mjs +14 -0
  175. package/test/esm/other.mjs +8 -0
  176. package/test/esm/plugin.mjs +8 -0
  177. package/test/fastify-instance.test.js +300 -0
  178. package/test/find-route.test.js +152 -0
  179. package/test/fluent-schema.test.js +209 -0
  180. package/test/genReqId.test.js +426 -0
  181. package/test/handler-context.test.js +45 -0
  182. package/test/handler-timeout.test.js +367 -0
  183. package/test/has-route.test.js +88 -0
  184. package/test/header-overflow.test.js +55 -0
  185. package/test/helper.js +496 -0
  186. package/test/hooks-async.test.js +1099 -0
  187. package/test/hooks.on-listen.test.js +1162 -0
  188. package/test/hooks.on-ready.test.js +421 -0
  189. package/test/hooks.test.js +3578 -0
  190. package/test/http-methods/copy.test.js +35 -0
  191. package/test/http-methods/custom-http-methods.test.js +114 -0
  192. package/test/http-methods/get.test.js +412 -0
  193. package/test/http-methods/head.test.js +263 -0
  194. package/test/http-methods/lock.test.js +108 -0
  195. package/test/http-methods/mkcalendar.test.js +143 -0
  196. package/test/http-methods/mkcol.test.js +35 -0
  197. package/test/http-methods/move.test.js +42 -0
  198. package/test/http-methods/propfind.test.js +136 -0
  199. package/test/http-methods/proppatch.test.js +105 -0
  200. package/test/http-methods/report.test.js +142 -0
  201. package/test/http-methods/search.test.js +233 -0
  202. package/test/http-methods/trace.test.js +21 -0
  203. package/test/http-methods/unlock.test.js +38 -0
  204. package/test/http2/closing.test.js +270 -0
  205. package/test/http2/constraint.test.js +109 -0
  206. package/test/http2/head.test.js +34 -0
  207. package/test/http2/plain.test.js +68 -0
  208. package/test/http2/secure-with-fallback.test.js +113 -0
  209. package/test/http2/secure.test.js +67 -0
  210. package/test/http2/unknown-http-method.test.js +34 -0
  211. package/test/https/custom-https-server.test.js +58 -0
  212. package/test/https/https.test.js +136 -0
  213. package/test/imports.test.js +17 -0
  214. package/test/inject.test.js +502 -0
  215. package/test/input-validation.js +335 -0
  216. package/test/internals/all.test.js +38 -0
  217. package/test/internals/content-type-parser.test.js +111 -0
  218. package/test/internals/context.test.js +31 -0
  219. package/test/internals/decorator.test.js +156 -0
  220. package/test/internals/errors.test.js +982 -0
  221. package/test/internals/handle-request.test.js +270 -0
  222. package/test/internals/hook-runner.test.js +449 -0
  223. package/test/internals/hooks.test.js +96 -0
  224. package/test/internals/initial-config.test.js +383 -0
  225. package/test/internals/logger.test.js +163 -0
  226. package/test/internals/plugin.test.js +170 -0
  227. package/test/internals/promise.test.js +63 -0
  228. package/test/internals/reply-serialize.test.js +714 -0
  229. package/test/internals/reply.test.js +1920 -0
  230. package/test/internals/req-id-gen-factory.test.js +133 -0
  231. package/test/internals/request-validate.test.js +1402 -0
  232. package/test/internals/request.test.js +506 -0
  233. package/test/internals/schema-controller-perf.test.js +40 -0
  234. package/test/internals/server.test.js +91 -0
  235. package/test/internals/validation.test.js +352 -0
  236. package/test/issue-4959.test.js +118 -0
  237. package/test/keep-alive-timeout.test.js +42 -0
  238. package/test/listen.1.test.js +154 -0
  239. package/test/listen.2.test.js +113 -0
  240. package/test/listen.3.test.js +83 -0
  241. package/test/listen.4.test.js +168 -0
  242. package/test/listen.5.test.js +122 -0
  243. package/test/logger/instantiation.test.js +341 -0
  244. package/test/logger/logger-test-utils.js +47 -0
  245. package/test/logger/logging.test.js +460 -0
  246. package/test/logger/options.test.js +579 -0
  247. package/test/logger/request.test.js +292 -0
  248. package/test/logger/response.test.js +183 -0
  249. package/test/logger/tap-parallel-not-ok +0 -0
  250. package/test/max-requests-per-socket.test.js +113 -0
  251. package/test/middleware.test.js +37 -0
  252. package/test/noop-set.test.js +19 -0
  253. package/test/nullable-validation.test.js +187 -0
  254. package/test/options.error-handler.test.js +5 -0
  255. package/test/options.test.js +5 -0
  256. package/test/output-validation.test.js +140 -0
  257. package/test/patch.error-handler.test.js +5 -0
  258. package/test/patch.test.js +5 -0
  259. package/test/plugin.1.test.js +230 -0
  260. package/test/plugin.2.test.js +314 -0
  261. package/test/plugin.3.test.js +287 -0
  262. package/test/plugin.4.test.js +504 -0
  263. package/test/plugin.helper.js +8 -0
  264. package/test/plugin.name.display.js +10 -0
  265. package/test/post-empty-body.test.js +38 -0
  266. package/test/pretty-print.test.js +366 -0
  267. package/test/promises.test.js +125 -0
  268. package/test/proto-poisoning.test.js +145 -0
  269. package/test/put.error-handler.test.js +5 -0
  270. package/test/put.test.js +5 -0
  271. package/test/register.test.js +184 -0
  272. package/test/reply-code.test.js +148 -0
  273. package/test/reply-early-hints.test.js +100 -0
  274. package/test/reply-error.test.js +815 -0
  275. package/test/reply-trailers.test.js +445 -0
  276. package/test/reply-web-stream-locked.test.js +37 -0
  277. package/test/request-error.test.js +624 -0
  278. package/test/request-header-host.test.js +339 -0
  279. package/test/request-id.test.js +118 -0
  280. package/test/request-timeout.test.js +53 -0
  281. package/test/route-hooks.test.js +635 -0
  282. package/test/route-prefix.test.js +904 -0
  283. package/test/route-shorthand.test.js +48 -0
  284. package/test/route.1.test.js +259 -0
  285. package/test/route.2.test.js +100 -0
  286. package/test/route.3.test.js +213 -0
  287. package/test/route.4.test.js +127 -0
  288. package/test/route.5.test.js +211 -0
  289. package/test/route.6.test.js +306 -0
  290. package/test/route.7.test.js +406 -0
  291. package/test/route.8.test.js +225 -0
  292. package/test/router-options.test.js +1108 -0
  293. package/test/same-shape.test.js +124 -0
  294. package/test/schema-examples.test.js +661 -0
  295. package/test/schema-feature.test.js +2198 -0
  296. package/test/schema-serialization.test.js +1171 -0
  297. package/test/schema-special-usage.test.js +1348 -0
  298. package/test/schema-validation.test.js +1572 -0
  299. package/test/scripts/validate-ecosystem-links.test.js +339 -0
  300. package/test/serialize-response.test.js +186 -0
  301. package/test/server.test.js +347 -0
  302. package/test/set-error-handler.test.js +69 -0
  303. package/test/skip-reply-send.test.js +317 -0
  304. package/test/stream-serializers.test.js +40 -0
  305. package/test/stream.1.test.js +94 -0
  306. package/test/stream.2.test.js +129 -0
  307. package/test/stream.3.test.js +198 -0
  308. package/test/stream.4.test.js +176 -0
  309. package/test/stream.5.test.js +188 -0
  310. package/test/sync-routes.test.js +32 -0
  311. package/test/throw.test.js +359 -0
  312. package/test/toolkit.js +63 -0
  313. package/test/trust-proxy.test.js +162 -0
  314. package/test/type-provider.test.js +22 -0
  315. package/test/types/content-type-parser.test-d.ts +72 -0
  316. package/test/types/decorate-request-reply.test-d.ts +18 -0
  317. package/test/types/dummy-plugin.ts +9 -0
  318. package/test/types/errors.test-d.ts +90 -0
  319. package/test/types/fastify.test-d.ts +352 -0
  320. package/test/types/hooks.test-d.ts +550 -0
  321. package/test/types/import.ts +2 -0
  322. package/test/types/instance.test-d.ts +588 -0
  323. package/test/types/logger.test-d.ts +277 -0
  324. package/test/types/plugin.test-d.ts +97 -0
  325. package/test/types/register.test-d.ts +237 -0
  326. package/test/types/reply.test-d.ts +254 -0
  327. package/test/types/request.test-d.ts +188 -0
  328. package/test/types/route.test-d.ts +553 -0
  329. package/test/types/schema.test-d.ts +135 -0
  330. package/test/types/serverFactory.test-d.ts +37 -0
  331. package/test/types/type-provider.test-d.ts +1213 -0
  332. package/test/types/using.test-d.ts +17 -0
  333. package/test/upgrade.test.js +52 -0
  334. package/test/url-rewriting.test.js +122 -0
  335. package/test/use-semicolon-delimiter.test.js +168 -0
  336. package/test/validation-error-handling.test.js +900 -0
  337. package/test/versioned-routes.test.js +603 -0
  338. package/test/web-api.test.js +616 -0
  339. package/test/wrap-thenable.test.js +30 -0
  340. package/types/content-type-parser.d.ts +75 -0
  341. package/types/context.d.ts +22 -0
  342. package/types/errors.d.ts +92 -0
  343. package/types/hooks.d.ts +875 -0
  344. package/types/instance.d.ts +609 -0
  345. package/types/logger.d.ts +107 -0
  346. package/types/plugin.d.ts +44 -0
  347. package/types/register.d.ts +42 -0
  348. package/types/reply.d.ts +81 -0
  349. package/types/request.d.ts +95 -0
  350. package/types/route.d.ts +199 -0
  351. package/types/schema.d.ts +61 -0
  352. package/types/server-factory.d.ts +19 -0
  353. package/types/type-provider.d.ts +130 -0
  354. package/types/utils.d.ts +98 -0
@@ -0,0 +1,367 @@
1
+ 'use strict'
2
+
3
+ const { test } = require('node:test')
4
+ const net = require('node:net')
5
+ const Fastify = require('..')
6
+ const { Readable } = require('node:stream')
7
+ const { kTimeoutTimer, kOnAbort } = require('../lib/symbols')
8
+
9
+ // --- Option validation ---
10
+
11
+ test('server-level handlerTimeout defaults to 0 in initialConfig', t => {
12
+ t.plan(1)
13
+ const fastify = Fastify()
14
+ t.assert.strictEqual(fastify.initialConfig.handlerTimeout, 0)
15
+ })
16
+
17
+ test('server-level handlerTimeout: 5000 is accepted and exposed in initialConfig', t => {
18
+ t.plan(1)
19
+ const fastify = Fastify({ handlerTimeout: 5000 })
20
+ t.assert.strictEqual(fastify.initialConfig.handlerTimeout, 5000)
21
+ })
22
+
23
+ test('route-level handlerTimeout rejects invalid values', async t => {
24
+ const fastify = Fastify()
25
+
26
+ t.assert.throws(() => {
27
+ fastify.get('/a', { handlerTimeout: 'fast' }, async () => 'ok')
28
+ }, { code: 'FST_ERR_ROUTE_HANDLER_TIMEOUT_OPTION_NOT_INT' })
29
+
30
+ t.assert.throws(() => {
31
+ fastify.get('/b', { handlerTimeout: -1 }, async () => 'ok')
32
+ }, { code: 'FST_ERR_ROUTE_HANDLER_TIMEOUT_OPTION_NOT_INT' })
33
+
34
+ t.assert.throws(() => {
35
+ fastify.get('/c', { handlerTimeout: 1.5 }, async () => 'ok')
36
+ }, { code: 'FST_ERR_ROUTE_HANDLER_TIMEOUT_OPTION_NOT_INT' })
37
+
38
+ t.assert.throws(() => {
39
+ fastify.get('/d', { handlerTimeout: 0 }, async () => 'ok')
40
+ }, { code: 'FST_ERR_ROUTE_HANDLER_TIMEOUT_OPTION_NOT_INT' })
41
+ })
42
+
43
+ // --- Lazy signal without handlerTimeout ---
44
+
45
+ test('when handlerTimeout is 0 (default), request.signal is lazily created', async t => {
46
+ t.plan(3)
47
+ const fastify = Fastify()
48
+
49
+ fastify.get('/', async (request) => {
50
+ const signal = request.signal
51
+ t.assert.ok(signal instanceof AbortSignal)
52
+ t.assert.strictEqual(signal.aborted, false)
53
+ return { ok: true }
54
+ })
55
+
56
+ const res = await fastify.inject({ method: 'GET', url: '/' })
57
+ t.assert.strictEqual(res.statusCode, 200)
58
+ })
59
+
60
+ test('client disconnect aborts lazily created signal (no handlerTimeout)', async t => {
61
+ t.plan(1)
62
+
63
+ const fastify = Fastify()
64
+ let signalAborted = false
65
+
66
+ fastify.get('/', async (request) => {
67
+ await new Promise((resolve) => {
68
+ request.signal.addEventListener('abort', () => {
69
+ signalAborted = true
70
+ resolve()
71
+ })
72
+ })
73
+ return 'should not reach'
74
+ })
75
+
76
+ await fastify.listen({ port: 0 })
77
+ t.after(() => fastify.close())
78
+
79
+ const address = fastify.server.address()
80
+ await new Promise((resolve) => {
81
+ const client = net.connect(address.port, () => {
82
+ client.write('GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
83
+ setTimeout(() => {
84
+ client.destroy()
85
+ setTimeout(resolve, 100)
86
+ }, 50)
87
+ })
88
+ })
89
+
90
+ t.assert.strictEqual(signalAborted, true)
91
+ })
92
+
93
+ // --- Basic timeout behavior ---
94
+
95
+ test('slow handler returns 503 with FST_ERR_HANDLER_TIMEOUT', async t => {
96
+ t.plan(2)
97
+ const fastify = Fastify()
98
+
99
+ fastify.get('/', { handlerTimeout: 50 }, async () => {
100
+ await new Promise(resolve => setTimeout(resolve, 500))
101
+ return 'too late'
102
+ })
103
+
104
+ const res = await fastify.inject({ method: 'GET', url: '/' })
105
+ t.assert.strictEqual(res.statusCode, 503)
106
+ t.assert.strictEqual(JSON.parse(res.payload).code, 'FST_ERR_HANDLER_TIMEOUT')
107
+ })
108
+
109
+ test('fast handler completes normally with 200', async t => {
110
+ t.plan(2)
111
+ const fastify = Fastify()
112
+
113
+ fastify.get('/', { handlerTimeout: 5000 }, async () => {
114
+ return { hello: 'world' }
115
+ })
116
+
117
+ const res = await fastify.inject({ method: 'GET', url: '/' })
118
+ t.assert.strictEqual(res.statusCode, 200)
119
+ t.assert.deepStrictEqual(JSON.parse(res.payload), { hello: 'world' })
120
+ })
121
+
122
+ // --- Per-route override ---
123
+
124
+ test('route-level handlerTimeout overrides server default', async t => {
125
+ t.plan(4)
126
+ const fastify = Fastify({ handlerTimeout: 5000 })
127
+
128
+ fastify.get('/slow', { handlerTimeout: 50 }, async () => {
129
+ await new Promise(resolve => setTimeout(resolve, 500))
130
+ return 'too late'
131
+ })
132
+
133
+ fastify.get('/fast', async () => {
134
+ return { ok: true }
135
+ })
136
+
137
+ const resSlow = await fastify.inject({ method: 'GET', url: '/slow' })
138
+ t.assert.strictEqual(resSlow.statusCode, 503)
139
+ t.assert.strictEqual(JSON.parse(resSlow.payload).code, 'FST_ERR_HANDLER_TIMEOUT')
140
+
141
+ const resFast = await fastify.inject({ method: 'GET', url: '/fast' })
142
+ t.assert.strictEqual(resFast.statusCode, 200)
143
+ t.assert.deepStrictEqual(JSON.parse(resFast.payload), { ok: true })
144
+ })
145
+
146
+ // --- request.signal behavior ---
147
+
148
+ test('request.signal is an AbortSignal when handlerTimeout > 0', async t => {
149
+ t.plan(2)
150
+ const fastify = Fastify()
151
+
152
+ fastify.get('/', { handlerTimeout: 5000 }, async (request) => {
153
+ t.assert.ok(request.signal instanceof AbortSignal)
154
+ t.assert.strictEqual(request.signal.aborted, false)
155
+ return 'ok'
156
+ })
157
+
158
+ await fastify.inject({ method: 'GET', url: '/' })
159
+ })
160
+
161
+ test('request.signal aborts when timeout fires with reason', async t => {
162
+ t.plan(2)
163
+ const fastify = Fastify()
164
+
165
+ let signalReason = null
166
+ fastify.get('/', { handlerTimeout: 50 }, async (request) => {
167
+ request.signal.addEventListener('abort', () => {
168
+ signalReason = request.signal.reason
169
+ })
170
+ await new Promise(resolve => setTimeout(resolve, 500))
171
+ return 'too late'
172
+ })
173
+
174
+ await fastify.inject({ method: 'GET', url: '/' })
175
+ t.assert.ok(signalReason !== null)
176
+ t.assert.strictEqual(signalReason.code, 'FST_ERR_HANDLER_TIMEOUT')
177
+ })
178
+
179
+ // --- Streaming response ---
180
+
181
+ test('streaming response: timer clears when response finishes', async t => {
182
+ t.plan(1)
183
+
184
+ const fastify = Fastify()
185
+ fastify.get('/', { handlerTimeout: 5000 }, async (request, reply) => {
186
+ const stream = new Readable({
187
+ read () {
188
+ this.push('hello')
189
+ this.push(null)
190
+ }
191
+ })
192
+ reply.type('text/plain').send(stream)
193
+ return reply
194
+ })
195
+
196
+ await fastify.listen({ port: 0 })
197
+ t.after(() => fastify.close())
198
+
199
+ const address = fastify.server.address()
200
+ const res = await fetch(`http://localhost:${address.port}/`)
201
+ t.assert.strictEqual(res.status, 200)
202
+ })
203
+
204
+ // --- SSE with reply.hijack() ---
205
+
206
+ test('reply.hijack() clears timeout timer', async t => {
207
+ t.plan(1)
208
+
209
+ const fastify = Fastify()
210
+ fastify.get('/', { handlerTimeout: 100 }, async (request, reply) => {
211
+ reply.hijack()
212
+ // Write after the original timeout would have fired
213
+ await new Promise(resolve => setTimeout(resolve, 200))
214
+ reply.raw.writeHead(200, { 'Content-Type': 'text/plain' })
215
+ reply.raw.end('hijacked response')
216
+ })
217
+
218
+ await fastify.listen({ port: 0 })
219
+ t.after(() => fastify.close())
220
+
221
+ const address = fastify.server.address()
222
+ const res = await fetch(`http://localhost:${address.port}/`)
223
+ t.assert.strictEqual(res.status, 200)
224
+ })
225
+
226
+ // --- Error handler integration ---
227
+
228
+ test('route-level errorHandler receives FST_ERR_HANDLER_TIMEOUT', async t => {
229
+ t.plan(3)
230
+ const fastify = Fastify()
231
+
232
+ fastify.get('/', {
233
+ handlerTimeout: 50,
234
+ errorHandler: (error, request, reply) => {
235
+ t.assert.strictEqual(error.code, 'FST_ERR_HANDLER_TIMEOUT')
236
+ reply.code(504).send({ custom: 'timeout' })
237
+ }
238
+ }, async () => {
239
+ await new Promise(resolve => setTimeout(resolve, 500))
240
+ return 'too late'
241
+ })
242
+
243
+ const res = await fastify.inject({ method: 'GET', url: '/' })
244
+ t.assert.strictEqual(res.statusCode, 504)
245
+ t.assert.deepStrictEqual(JSON.parse(res.payload), { custom: 'timeout' })
246
+ })
247
+
248
+ // --- Timer cleanup / no leaks ---
249
+
250
+ test('timer is cleaned up after fast response (no leak)', async t => {
251
+ t.plan(3)
252
+ const fastify = Fastify()
253
+
254
+ let capturedRequest
255
+ fastify.get('/', { handlerTimeout: 60000 }, async (request) => {
256
+ capturedRequest = request
257
+ return 'fast'
258
+ })
259
+
260
+ const res = await fastify.inject({ method: 'GET', url: '/' })
261
+ t.assert.strictEqual(res.statusCode, 200)
262
+ // Timer and listener should be cleaned up
263
+ t.assert.strictEqual(capturedRequest[kTimeoutTimer], null)
264
+ t.assert.strictEqual(capturedRequest[kOnAbort], null)
265
+ })
266
+
267
+ // --- routeOptions exposure ---
268
+
269
+ test('request.routeOptions.handlerTimeout reflects configured value', async t => {
270
+ t.plan(2)
271
+ const fastify = Fastify()
272
+
273
+ fastify.get('/', { handlerTimeout: 3000 }, async (request) => {
274
+ t.assert.strictEqual(request.routeOptions.handlerTimeout, 3000)
275
+ return 'ok'
276
+ })
277
+
278
+ const res = await fastify.inject({ method: 'GET', url: '/' })
279
+ t.assert.strictEqual(res.statusCode, 200)
280
+ })
281
+
282
+ test('request.routeOptions.handlerTimeout reflects server default', async t => {
283
+ t.plan(2)
284
+ const fastify = Fastify({ handlerTimeout: 7000 })
285
+
286
+ fastify.get('/', async (request) => {
287
+ t.assert.strictEqual(request.routeOptions.handlerTimeout, 7000)
288
+ return 'ok'
289
+ })
290
+
291
+ const res = await fastify.inject({ method: 'GET', url: '/' })
292
+ t.assert.strictEqual(res.statusCode, 200)
293
+ })
294
+
295
+ // --- Client disconnect aborts signal ---
296
+
297
+ test('client disconnect aborts request.signal', async t => {
298
+ t.plan(1)
299
+
300
+ const fastify = Fastify()
301
+ let signalAborted = false
302
+
303
+ fastify.get('/', { handlerTimeout: 5000 }, async (request) => {
304
+ await new Promise((resolve) => {
305
+ request.signal.addEventListener('abort', () => {
306
+ signalAborted = true
307
+ resolve()
308
+ })
309
+ })
310
+ return 'should not reach'
311
+ })
312
+
313
+ await fastify.listen({ port: 0 })
314
+ t.after(() => fastify.close())
315
+
316
+ const address = fastify.server.address()
317
+ await new Promise((resolve) => {
318
+ const client = net.connect(address.port, () => {
319
+ client.write('GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
320
+ setTimeout(() => {
321
+ client.destroy()
322
+ // Give the server time to process the close event
323
+ setTimeout(resolve, 100)
324
+ }, 50)
325
+ })
326
+ })
327
+
328
+ t.assert.strictEqual(signalAborted, true)
329
+ })
330
+
331
+ // --- Race: handler completes just as timeout fires ---
332
+
333
+ test('no double-send when handler completes near timeout boundary', async t => {
334
+ t.plan(2)
335
+ const fastify = Fastify()
336
+
337
+ fastify.get('/', { handlerTimeout: 50 }, async (request, reply) => {
338
+ // Respond just before timeout
339
+ await new Promise(resolve => setTimeout(resolve, 40))
340
+ reply.send({ ok: true })
341
+ return reply
342
+ })
343
+
344
+ const res = await fastify.inject({ method: 'GET', url: '/' })
345
+ // Should get either 200 or 503 depending on race, but never crash
346
+ t.assert.ok(res.statusCode === 200 || res.statusCode === 503)
347
+ // Verify response is valid JSON regardless of which won the race
348
+ t.assert.ok(JSON.parse(res.payload))
349
+ })
350
+
351
+ // --- Server default inherited by routes ---
352
+
353
+ test('routes inherit server-level handlerTimeout', async t => {
354
+ t.plan(3)
355
+ const fastify = Fastify({ handlerTimeout: 50 })
356
+
357
+ fastify.get('/', async (request) => {
358
+ // Verify the signal is present (inherited from server default)
359
+ t.assert.ok(request.signal instanceof AbortSignal)
360
+ await new Promise(resolve => setTimeout(resolve, 500))
361
+ return 'too late'
362
+ })
363
+
364
+ const res = await fastify.inject({ method: 'GET', url: '/' })
365
+ t.assert.strictEqual(res.statusCode, 503)
366
+ t.assert.strictEqual(JSON.parse(res.payload).code, 'FST_ERR_HANDLER_TIMEOUT')
367
+ })
@@ -0,0 +1,88 @@
1
+ 'use strict'
2
+
3
+ const { test, describe } = require('node:test')
4
+ const Fastify = require('..')
5
+
6
+ const fastify = Fastify()
7
+
8
+ describe('hasRoute', async t => {
9
+ test('hasRoute - invalid options', t => {
10
+ t.plan(3)
11
+
12
+ t.assert.strictEqual(fastify.hasRoute({ }), false)
13
+ t.assert.strictEqual(fastify.hasRoute({ method: 'GET' }), false)
14
+ t.assert.strictEqual(fastify.hasRoute({ constraints: [] }), false)
15
+ })
16
+
17
+ test('hasRoute - primitive method', t => {
18
+ t.plan(2)
19
+ fastify.route({
20
+ method: 'GET',
21
+ url: '/',
22
+ handler: function (req, reply) {
23
+ reply.send({ hello: 'world' })
24
+ }
25
+ })
26
+
27
+ t.assert.strictEqual(fastify.hasRoute({
28
+ method: 'GET',
29
+ url: '/'
30
+ }), true)
31
+
32
+ t.assert.strictEqual(fastify.hasRoute({
33
+ method: 'POST',
34
+ url: '/'
35
+ }), false)
36
+ })
37
+
38
+ test('hasRoute - with constraints', t => {
39
+ t.plan(2)
40
+ fastify.route({
41
+ method: 'GET',
42
+ url: '/',
43
+ constraints: { version: '1.2.0' },
44
+ handler: (req, reply) => {
45
+ reply.send({ hello: 'world' })
46
+ }
47
+ })
48
+
49
+ t.assert.strictEqual(fastify.hasRoute({
50
+ method: 'GET',
51
+ url: '/',
52
+ constraints: { version: '1.2.0' }
53
+ }), true)
54
+
55
+ t.assert.strictEqual(fastify.hasRoute({
56
+ method: 'GET',
57
+ url: '/',
58
+ constraints: { version: '1.3.0' }
59
+ }), false)
60
+ })
61
+
62
+ test('hasRoute - parametric route regexp with constraints', t => {
63
+ t.plan(1)
64
+ // parametric with regexp
65
+ fastify.get('/example/:file(^\\d+).png', function (request, reply) { })
66
+
67
+ t.assert.strictEqual(fastify.hasRoute({
68
+ method: 'GET',
69
+ url: '/example/:file(^\\d+).png'
70
+ }), true)
71
+ })
72
+
73
+ test('hasRoute - finds a route even if method is not uppercased', t => {
74
+ t.plan(1)
75
+ fastify.route({
76
+ method: 'GET',
77
+ url: '/equal',
78
+ handler: function (req, reply) {
79
+ reply.send({ hello: 'world' })
80
+ }
81
+ })
82
+
83
+ t.assert.strictEqual(fastify.hasRoute({
84
+ method: 'get',
85
+ url: '/equal'
86
+ }), true)
87
+ })
88
+ })
@@ -0,0 +1,55 @@
1
+ 'use strict'
2
+
3
+ const { test } = require('node:test')
4
+ const Fastify = require('..')
5
+
6
+ const maxHeaderSize = 1024
7
+
8
+ test('Should return 431 if request header fields are too large', async (t) => {
9
+ t.plan(2)
10
+
11
+ const fastify = Fastify({ http: { maxHeaderSize } })
12
+ fastify.route({
13
+ method: 'GET',
14
+ url: '/',
15
+ handler: (_req, reply) => {
16
+ reply.send({ hello: 'world' })
17
+ }
18
+ })
19
+
20
+ const fastifyServer = await fastify.listen({ port: 0 })
21
+
22
+ const result = await fetch(fastifyServer, {
23
+ method: 'GET',
24
+ headers: {
25
+ 'Large-Header': 'a'.repeat(maxHeaderSize)
26
+ }
27
+ })
28
+
29
+ t.assert.ok(!result.ok)
30
+ t.assert.strictEqual(result.status, 431)
31
+
32
+ t.after(() => fastify.close())
33
+ })
34
+
35
+ test('Should return 431 if URI is too long', async (t) => {
36
+ t.plan(2)
37
+
38
+ const fastify = Fastify({ http: { maxHeaderSize } })
39
+ fastify.route({
40
+ method: 'GET',
41
+ url: '/',
42
+ handler: (_req, reply) => {
43
+ reply.send({ hello: 'world' })
44
+ }
45
+ })
46
+
47
+ const fastifyServer = await fastify.listen({ port: 0 })
48
+
49
+ const result = await fetch(`${fastifyServer}/${'a'.repeat(maxHeaderSize)}`)
50
+
51
+ t.assert.ok(!result.ok)
52
+ t.assert.strictEqual(result.status, 431)
53
+
54
+ t.after(() => fastify.close())
55
+ })