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.
- package/.borp.yaml +3 -0
- package/.markdownlint-cli2.yaml +22 -0
- package/.prettierignore +1 -0
- package/GOVERNANCE.md +4 -0
- package/LICENSE +21 -0
- package/PROJECT_CHARTER.md +126 -0
- package/README.md +423 -0
- package/SECURITY.md +220 -0
- package/SPONSORS.md +24 -0
- package/build/build-error-serializer.js +35 -0
- package/build/build-validation.js +169 -0
- package/build/sync-version.js +11 -0
- package/docs/Guides/Benchmarking.md +60 -0
- package/docs/Guides/Database.md +321 -0
- package/docs/Guides/Delay-Accepting-Requests.md +608 -0
- package/docs/Guides/Detecting-When-Clients-Abort.md +172 -0
- package/docs/Guides/Ecosystem.md +726 -0
- package/docs/Guides/Fluent-Schema.md +127 -0
- package/docs/Guides/Getting-Started.md +620 -0
- package/docs/Guides/Index.md +43 -0
- package/docs/Guides/Migration-Guide-V3.md +287 -0
- package/docs/Guides/Migration-Guide-V4.md +267 -0
- package/docs/Guides/Migration-Guide-V5.md +727 -0
- package/docs/Guides/Plugins-Guide.md +520 -0
- package/docs/Guides/Prototype-Poisoning.md +383 -0
- package/docs/Guides/Recommendations.md +378 -0
- package/docs/Guides/Serverless.md +604 -0
- package/docs/Guides/Style-Guide.md +246 -0
- package/docs/Guides/Testing.md +481 -0
- package/docs/Guides/Write-Plugin.md +103 -0
- package/docs/Guides/Write-Type-Provider.md +34 -0
- package/docs/Reference/ContentTypeParser.md +271 -0
- package/docs/Reference/Decorators.md +436 -0
- package/docs/Reference/Encapsulation.md +194 -0
- package/docs/Reference/Errors.md +377 -0
- package/docs/Reference/HTTP2.md +94 -0
- package/docs/Reference/Hooks.md +958 -0
- package/docs/Reference/Index.md +73 -0
- package/docs/Reference/LTS.md +86 -0
- package/docs/Reference/Lifecycle.md +99 -0
- package/docs/Reference/Logging.md +268 -0
- package/docs/Reference/Middleware.md +79 -0
- package/docs/Reference/Plugins.md +245 -0
- package/docs/Reference/Principles.md +73 -0
- package/docs/Reference/Reply.md +1001 -0
- package/docs/Reference/Request.md +295 -0
- package/docs/Reference/Routes.md +802 -0
- package/docs/Reference/Server.md +2389 -0
- package/docs/Reference/Type-Providers.md +256 -0
- package/docs/Reference/TypeScript.md +1729 -0
- package/docs/Reference/Validation-and-Serialization.md +1130 -0
- package/docs/Reference/Warnings.md +58 -0
- package/docs/index.md +24 -0
- package/docs/resources/encapsulation_context.drawio +1 -0
- package/docs/resources/encapsulation_context.svg +3 -0
- package/eslint.config.js +35 -0
- package/examples/asyncawait.js +38 -0
- package/examples/benchmark/body.json +3 -0
- package/examples/benchmark/hooks-benchmark-async-await.js +44 -0
- package/examples/benchmark/hooks-benchmark.js +52 -0
- package/examples/benchmark/parser.js +47 -0
- package/examples/benchmark/simple.js +30 -0
- package/examples/benchmark/webstream.js +27 -0
- package/examples/hooks.js +91 -0
- package/examples/http2.js +39 -0
- package/examples/https.js +38 -0
- package/examples/parser.js +53 -0
- package/examples/plugin.js +12 -0
- package/examples/route-prefix.js +38 -0
- package/examples/shared-schema.js +38 -0
- package/examples/simple-stream.js +20 -0
- package/examples/simple.js +32 -0
- package/examples/simple.mjs +27 -0
- package/examples/typescript-server.ts +79 -0
- package/examples/use-plugin.js +29 -0
- package/fastify.d.ts +253 -0
- package/fastify.js +985 -0
- package/integration/server.js +29 -0
- package/integration/test.sh +23 -0
- package/lib/config-validator.js +1266 -0
- package/lib/content-type-parser.js +413 -0
- package/lib/content-type.js +160 -0
- package/lib/context.js +98 -0
- package/lib/decorate.js +152 -0
- package/lib/error-handler.js +173 -0
- package/lib/error-serializer.js +134 -0
- package/lib/error-status.js +14 -0
- package/lib/errors.js +516 -0
- package/lib/four-oh-four.js +190 -0
- package/lib/handle-request.js +195 -0
- package/lib/head-route.js +45 -0
- package/lib/hooks.js +429 -0
- package/lib/initial-config-validation.js +37 -0
- package/lib/logger-factory.js +136 -0
- package/lib/logger-pino.js +68 -0
- package/lib/noop-set.js +10 -0
- package/lib/plugin-override.js +90 -0
- package/lib/plugin-utils.js +169 -0
- package/lib/promise.js +23 -0
- package/lib/reply.js +1030 -0
- package/lib/req-id-gen-factory.js +52 -0
- package/lib/request.js +391 -0
- package/lib/route.js +686 -0
- package/lib/schema-controller.js +164 -0
- package/lib/schemas.js +207 -0
- package/lib/server.js +441 -0
- package/lib/symbols.js +71 -0
- package/lib/validation.js +280 -0
- package/lib/warnings.js +57 -0
- package/lib/wrap-thenable.js +84 -0
- package/package.json +225 -0
- package/scripts/validate-ecosystem-links.js +179 -0
- package/test/404s.test.js +2035 -0
- package/test/500s.test.js +422 -0
- package/test/allow-unsafe-regex.test.js +92 -0
- package/test/als.test.js +65 -0
- package/test/async-await.test.js +705 -0
- package/test/async-dispose.test.js +20 -0
- package/test/async_hooks.test.js +52 -0
- package/test/body-limit.test.js +224 -0
- package/test/buffer.test.js +74 -0
- package/test/build/error-serializer.test.js +36 -0
- package/test/build/version.test.js +14 -0
- package/test/build-certificate.js +109 -0
- package/test/bundler/README.md +29 -0
- package/test/bundler/esbuild/bundler-test.js +32 -0
- package/test/bundler/esbuild/package.json +10 -0
- package/test/bundler/esbuild/src/fail-plugin-version.js +14 -0
- package/test/bundler/esbuild/src/index.js +9 -0
- package/test/bundler/webpack/bundler-test.js +32 -0
- package/test/bundler/webpack/package.json +11 -0
- package/test/bundler/webpack/src/fail-plugin-version.js +14 -0
- package/test/bundler/webpack/src/index.js +9 -0
- package/test/bundler/webpack/webpack.config.js +15 -0
- package/test/case-insensitive.test.js +102 -0
- package/test/chainable.test.js +40 -0
- package/test/child-logger-factory.test.js +128 -0
- package/test/client-timeout.test.js +38 -0
- package/test/close-pipelining.test.js +78 -0
- package/test/close.test.js +706 -0
- package/test/conditional-pino.test.js +47 -0
- package/test/connection-timeout.test.js +42 -0
- package/test/constrained-routes.test.js +1138 -0
- package/test/content-length.test.js +174 -0
- package/test/content-parser.test.js +739 -0
- package/test/content-type.test.js +181 -0
- package/test/context-config.test.js +164 -0
- package/test/custom-http-server.test.js +118 -0
- package/test/custom-parser-async.test.js +59 -0
- package/test/custom-parser.0.test.js +701 -0
- package/test/custom-parser.1.test.js +266 -0
- package/test/custom-parser.2.test.js +91 -0
- package/test/custom-parser.3.test.js +208 -0
- package/test/custom-parser.4.test.js +218 -0
- package/test/custom-parser.5.test.js +130 -0
- package/test/custom-querystring-parser.test.js +129 -0
- package/test/decorator.test.js +1330 -0
- package/test/delete.test.js +344 -0
- package/test/diagnostics-channel/404.test.js +49 -0
- package/test/diagnostics-channel/async-delay-request.test.js +65 -0
- package/test/diagnostics-channel/async-request.test.js +64 -0
- package/test/diagnostics-channel/error-before-handler.test.js +35 -0
- package/test/diagnostics-channel/error-request.test.js +53 -0
- package/test/diagnostics-channel/error-status.test.js +123 -0
- package/test/diagnostics-channel/init.test.js +50 -0
- package/test/diagnostics-channel/sync-delay-request.test.js +49 -0
- package/test/diagnostics-channel/sync-request-reply.test.js +51 -0
- package/test/diagnostics-channel/sync-request.test.js +54 -0
- package/test/encapsulated-child-logger-factory.test.js +69 -0
- package/test/encapsulated-error-handler.test.js +237 -0
- package/test/esm/errorCodes.test.mjs +10 -0
- package/test/esm/esm.test.mjs +13 -0
- package/test/esm/index.test.js +8 -0
- package/test/esm/named-exports.mjs +14 -0
- package/test/esm/other.mjs +8 -0
- package/test/esm/plugin.mjs +8 -0
- package/test/fastify-instance.test.js +300 -0
- package/test/find-route.test.js +152 -0
- package/test/fluent-schema.test.js +209 -0
- package/test/genReqId.test.js +426 -0
- package/test/handler-context.test.js +45 -0
- package/test/handler-timeout.test.js +367 -0
- package/test/has-route.test.js +88 -0
- package/test/header-overflow.test.js +55 -0
- package/test/helper.js +496 -0
- package/test/hooks-async.test.js +1099 -0
- package/test/hooks.on-listen.test.js +1162 -0
- package/test/hooks.on-ready.test.js +421 -0
- package/test/hooks.test.js +3578 -0
- package/test/http-methods/copy.test.js +35 -0
- package/test/http-methods/custom-http-methods.test.js +114 -0
- package/test/http-methods/get.test.js +412 -0
- package/test/http-methods/head.test.js +263 -0
- package/test/http-methods/lock.test.js +108 -0
- package/test/http-methods/mkcalendar.test.js +143 -0
- package/test/http-methods/mkcol.test.js +35 -0
- package/test/http-methods/move.test.js +42 -0
- package/test/http-methods/propfind.test.js +136 -0
- package/test/http-methods/proppatch.test.js +105 -0
- package/test/http-methods/report.test.js +142 -0
- package/test/http-methods/search.test.js +233 -0
- package/test/http-methods/trace.test.js +21 -0
- package/test/http-methods/unlock.test.js +38 -0
- package/test/http2/closing.test.js +270 -0
- package/test/http2/constraint.test.js +109 -0
- package/test/http2/head.test.js +34 -0
- package/test/http2/plain.test.js +68 -0
- package/test/http2/secure-with-fallback.test.js +113 -0
- package/test/http2/secure.test.js +67 -0
- package/test/http2/unknown-http-method.test.js +34 -0
- package/test/https/custom-https-server.test.js +58 -0
- package/test/https/https.test.js +136 -0
- package/test/imports.test.js +17 -0
- package/test/inject.test.js +502 -0
- package/test/input-validation.js +335 -0
- package/test/internals/all.test.js +38 -0
- package/test/internals/content-type-parser.test.js +111 -0
- package/test/internals/context.test.js +31 -0
- package/test/internals/decorator.test.js +156 -0
- package/test/internals/errors.test.js +982 -0
- package/test/internals/handle-request.test.js +270 -0
- package/test/internals/hook-runner.test.js +449 -0
- package/test/internals/hooks.test.js +96 -0
- package/test/internals/initial-config.test.js +383 -0
- package/test/internals/logger.test.js +163 -0
- package/test/internals/plugin.test.js +170 -0
- package/test/internals/promise.test.js +63 -0
- package/test/internals/reply-serialize.test.js +714 -0
- package/test/internals/reply.test.js +1920 -0
- package/test/internals/req-id-gen-factory.test.js +133 -0
- package/test/internals/request-validate.test.js +1402 -0
- package/test/internals/request.test.js +506 -0
- package/test/internals/schema-controller-perf.test.js +40 -0
- package/test/internals/server.test.js +91 -0
- package/test/internals/validation.test.js +352 -0
- package/test/issue-4959.test.js +118 -0
- package/test/keep-alive-timeout.test.js +42 -0
- package/test/listen.1.test.js +154 -0
- package/test/listen.2.test.js +113 -0
- package/test/listen.3.test.js +83 -0
- package/test/listen.4.test.js +168 -0
- package/test/listen.5.test.js +122 -0
- package/test/logger/instantiation.test.js +341 -0
- package/test/logger/logger-test-utils.js +47 -0
- package/test/logger/logging.test.js +460 -0
- package/test/logger/options.test.js +579 -0
- package/test/logger/request.test.js +292 -0
- package/test/logger/response.test.js +183 -0
- package/test/logger/tap-parallel-not-ok +0 -0
- package/test/max-requests-per-socket.test.js +113 -0
- package/test/middleware.test.js +37 -0
- package/test/noop-set.test.js +19 -0
- package/test/nullable-validation.test.js +187 -0
- package/test/options.error-handler.test.js +5 -0
- package/test/options.test.js +5 -0
- package/test/output-validation.test.js +140 -0
- package/test/patch.error-handler.test.js +5 -0
- package/test/patch.test.js +5 -0
- package/test/plugin.1.test.js +230 -0
- package/test/plugin.2.test.js +314 -0
- package/test/plugin.3.test.js +287 -0
- package/test/plugin.4.test.js +504 -0
- package/test/plugin.helper.js +8 -0
- package/test/plugin.name.display.js +10 -0
- package/test/post-empty-body.test.js +38 -0
- package/test/pretty-print.test.js +366 -0
- package/test/promises.test.js +125 -0
- package/test/proto-poisoning.test.js +145 -0
- package/test/put.error-handler.test.js +5 -0
- package/test/put.test.js +5 -0
- package/test/register.test.js +184 -0
- package/test/reply-code.test.js +148 -0
- package/test/reply-early-hints.test.js +100 -0
- package/test/reply-error.test.js +815 -0
- package/test/reply-trailers.test.js +445 -0
- package/test/reply-web-stream-locked.test.js +37 -0
- package/test/request-error.test.js +624 -0
- package/test/request-header-host.test.js +339 -0
- package/test/request-id.test.js +118 -0
- package/test/request-timeout.test.js +53 -0
- package/test/route-hooks.test.js +635 -0
- package/test/route-prefix.test.js +904 -0
- package/test/route-shorthand.test.js +48 -0
- package/test/route.1.test.js +259 -0
- package/test/route.2.test.js +100 -0
- package/test/route.3.test.js +213 -0
- package/test/route.4.test.js +127 -0
- package/test/route.5.test.js +211 -0
- package/test/route.6.test.js +306 -0
- package/test/route.7.test.js +406 -0
- package/test/route.8.test.js +225 -0
- package/test/router-options.test.js +1108 -0
- package/test/same-shape.test.js +124 -0
- package/test/schema-examples.test.js +661 -0
- package/test/schema-feature.test.js +2198 -0
- package/test/schema-serialization.test.js +1171 -0
- package/test/schema-special-usage.test.js +1348 -0
- package/test/schema-validation.test.js +1572 -0
- package/test/scripts/validate-ecosystem-links.test.js +339 -0
- package/test/serialize-response.test.js +186 -0
- package/test/server.test.js +347 -0
- package/test/set-error-handler.test.js +69 -0
- package/test/skip-reply-send.test.js +317 -0
- package/test/stream-serializers.test.js +40 -0
- package/test/stream.1.test.js +94 -0
- package/test/stream.2.test.js +129 -0
- package/test/stream.3.test.js +198 -0
- package/test/stream.4.test.js +176 -0
- package/test/stream.5.test.js +188 -0
- package/test/sync-routes.test.js +32 -0
- package/test/throw.test.js +359 -0
- package/test/toolkit.js +63 -0
- package/test/trust-proxy.test.js +162 -0
- package/test/type-provider.test.js +22 -0
- package/test/types/content-type-parser.test-d.ts +72 -0
- package/test/types/decorate-request-reply.test-d.ts +18 -0
- package/test/types/dummy-plugin.ts +9 -0
- package/test/types/errors.test-d.ts +90 -0
- package/test/types/fastify.test-d.ts +352 -0
- package/test/types/hooks.test-d.ts +550 -0
- package/test/types/import.ts +2 -0
- package/test/types/instance.test-d.ts +588 -0
- package/test/types/logger.test-d.ts +277 -0
- package/test/types/plugin.test-d.ts +97 -0
- package/test/types/register.test-d.ts +237 -0
- package/test/types/reply.test-d.ts +254 -0
- package/test/types/request.test-d.ts +188 -0
- package/test/types/route.test-d.ts +553 -0
- package/test/types/schema.test-d.ts +135 -0
- package/test/types/serverFactory.test-d.ts +37 -0
- package/test/types/type-provider.test-d.ts +1213 -0
- package/test/types/using.test-d.ts +17 -0
- package/test/upgrade.test.js +52 -0
- package/test/url-rewriting.test.js +122 -0
- package/test/use-semicolon-delimiter.test.js +168 -0
- package/test/validation-error-handling.test.js +900 -0
- package/test/versioned-routes.test.js +603 -0
- package/test/web-api.test.js +616 -0
- package/test/wrap-thenable.test.js +30 -0
- package/types/content-type-parser.d.ts +75 -0
- package/types/context.d.ts +22 -0
- package/types/errors.d.ts +92 -0
- package/types/hooks.d.ts +875 -0
- package/types/instance.d.ts +609 -0
- package/types/logger.d.ts +107 -0
- package/types/plugin.d.ts +44 -0
- package/types/register.d.ts +42 -0
- package/types/reply.d.ts +81 -0
- package/types/request.d.ts +95 -0
- package/types/route.d.ts +199 -0
- package/types/schema.d.ts +61 -0
- package/types/server-factory.d.ts +19 -0
- package/types/type-provider.d.ts +130 -0
- 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
|
+
})
|