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,608 @@
1
+ <h1 align="center">Fastify</h1>
2
+
3
+ # Delay Accepting Requests
4
+
5
+ ## Introduction
6
+
7
+ Fastify provides several [hooks](../Reference/Hooks.md) useful for a variety of
8
+ situations. One of them is the [`onReady`](../Reference/Hooks.md#onready) hook,
9
+ which is useful for executing tasks *right before* the server starts accepting
10
+ new requests. There isn't, though, a direct mechanism to handle scenarios in
11
+ which you'd like the server to start accepting **specific** requests and denying
12
+ all others, at least up to some point.
13
+
14
+ Say, for instance, your server needs to authenticate with an OAuth provider to
15
+ start serving requests. To do that it'd need to engage in the [OAuth
16
+ Authorization Code
17
+ Flow](https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow),
18
+ which would require it to listen to two requests from the authentication
19
+ provider:
20
+
21
+ 1. the Authorization Code webhook
22
+ 2. the tokens webhook
23
+
24
+ Until the authorization flow is done you wouldn't be able to serve customer
25
+ requests. What to do then?
26
+
27
+ There are several solutions for achieving that kind of behavior. Here we'll
28
+ introduce one of such techniques and, hopefully, you'll be able to get things
29
+ rolling asap!
30
+
31
+ ## Solution
32
+
33
+ ### Overview
34
+
35
+ The proposed solution is one of many possible ways of dealing with this scenario
36
+ and many similar to it. It relies solely on Fastify, so no fancy infrastructure
37
+ tricks or third-party libraries will be necessary.
38
+
39
+ To simplify things we won't be dealing with a precise OAuth flow but, instead,
40
+ simulate a scenario in which some key is needed to serve a request and that key
41
+ can only be retrieved in runtime by authenticating with an external provider.
42
+
43
+ The main goal here is to deny requests that would otherwise fail **as early as
44
+ possible** and with some **meaningful context**. That's both useful for the
45
+ server (fewer resources allocated to a bound-to-fail task) and for the client
46
+ (they get some meaningful information and don't need to wait long for it).
47
+
48
+ That will be achieved by wrapping into a custom plugin two main features:
49
+
50
+ 1. the mechanism for authenticating with the provider
51
+ [decorating](../Reference/Decorators.md) the `fastify` object with the
52
+ authentication key (`magicKey` from here onward)
53
+ 1. the mechanism for denying requests that would, otherwise, fail
54
+
55
+ ### Hands-on
56
+
57
+ For this sample solution we'll be using the following:
58
+
59
+ - `node.js v16.14.2`
60
+ - `npm 8.5.0`
61
+ - `fastify 4.0.0-rc.1`
62
+ - `fastify-plugin 3.0.1`
63
+ - `undici 5.0.0`
64
+
65
+ Say we have the following base server set up at first:
66
+
67
+ ```js
68
+ const Fastify = require('fastify')
69
+
70
+ const provider = require('./provider')
71
+
72
+ const server = Fastify({ logger: true })
73
+ const USUAL_WAIT_TIME_MS = 5000
74
+
75
+ server.get('/ping', function (request, reply) {
76
+ reply.send({ error: false, ready: request.server.magicKey !== null })
77
+ })
78
+
79
+ server.post('/webhook', function (request, reply) {
80
+ // It's good practice to validate webhook requests come from
81
+ // who you expect. This is skipped in this sample for the sake
82
+ // of simplicity
83
+
84
+ const { magicKey } = request.body
85
+ request.server.magicKey = magicKey
86
+ request.log.info('Ready for customer requests!')
87
+
88
+ reply.send({ error: false })
89
+ })
90
+
91
+ server.get('/v1*', async function (request, reply) {
92
+ try {
93
+ const data = await provider.fetchSensitiveData(request.server.magicKey)
94
+ return { customer: true, error: false }
95
+ } catch (error) {
96
+ request.log.error({
97
+ error,
98
+ message: 'Failed at fetching sensitive data from provider',
99
+ })
100
+
101
+ reply.statusCode = 500
102
+ return { customer: null, error: true }
103
+ }
104
+ })
105
+
106
+ server.decorate('magicKey')
107
+
108
+ server.listen({ port: '1234' }, () => {
109
+ provider.thirdPartyMagicKeyGenerator(USUAL_WAIT_TIME_MS)
110
+ .catch((error) => {
111
+ server.log.error({
112
+ error,
113
+ message: 'Got an error while trying to get the magic key!'
114
+ })
115
+
116
+ // Since we won't be able to serve requests, might as well wrap
117
+ // things up
118
+ server.close(() => process.exit(1))
119
+ })
120
+ })
121
+ ```
122
+
123
+ Our code is simply setting up a Fastify server with a few routes:
124
+
125
+ - a `/ping` route that specifies whether the service is ready or not to serve
126
+ requests by checking if the `magicKey` has been set up
127
+ - a `/webhook` endpoint for our provider to reach back to us when they're ready
128
+ to share the `magicKey`. The `magicKey` is, then, saved into the previously set
129
+ decorator on the `fastify` object
130
+ - a catchall `/v1*` route to simulate what would have been customer-initiated
131
+ requests. These requests rely on us having a valid `magicKey`
132
+
133
+ The `provider.js` file, simulating actions of an external provider, is as
134
+ follows:
135
+
136
+ ```js
137
+ const { fetch } = require('undici')
138
+ const { setTimeout } = require('node:timers/promises')
139
+
140
+ const MAGIC_KEY = '12345'
141
+
142
+ const delay = setTimeout
143
+
144
+ exports.thirdPartyMagicKeyGenerator = async (ms) => {
145
+ // Simulate processing delay
146
+ await delay(ms)
147
+
148
+ // Simulate webhook request to our server
149
+ const { status } = await fetch(
150
+ 'http://localhost:1234/webhook',
151
+ {
152
+ body: JSON.stringify({ magicKey: MAGIC_KEY }),
153
+ method: 'POST',
154
+ headers: {
155
+ 'content-type': 'application/json',
156
+ },
157
+ },
158
+ )
159
+
160
+ if (status !== 200) {
161
+ throw new Error('Failed to fetch magic key')
162
+ }
163
+ }
164
+
165
+ exports.fetchSensitiveData = async (key) => {
166
+ // Simulate processing delay
167
+ await delay(700)
168
+ const data = { sensitive: true }
169
+
170
+ if (key === MAGIC_KEY) {
171
+ return data
172
+ }
173
+
174
+ throw new Error('Invalid key')
175
+ }
176
+ ```
177
+
178
+ The most important snippet here is the `thirdPartyMagicKeyGenerator` function,
179
+ which will wait for 5 seconds and, then, make the POST request to our `/webhook`
180
+ endpoint.
181
+
182
+ When our server spins up we start listening to new connections without having
183
+ our `magicKey` set up. Until we receive the webhook request from our external
184
+ provider (in this example we're simulating a 5 second delay) all our requests
185
+ under the `/v1*` path (customer requests) will fail. Worse than that: they'll
186
+ fail after we've reached out to our provider with an invalid key and got an
187
+ error from them. That wasted time and resources for us and our customers.
188
+ Depending on the kind of application we're running and on the request rate we're
189
+ expecting this delay is not acceptable or, at least, very annoying.
190
+
191
+ Of course, that could be simply mitigated by checking whether or not the
192
+ `magicKey` has been set up before hitting the provider in the `/v1*` handler.
193
+ Sure, but that would lead to bloat in the code. And imagine we have dozens of
194
+ different routes, with different controllers, that require that key. Should we
195
+ repeatedly add that check to all of them? That's error-prone and there are more
196
+ elegant solutions.
197
+
198
+ What we'll do to improve this setup overall is create a
199
+ [`Plugin`](../Reference/Plugins.md) that'll be solely responsible for making
200
+ sure we both:
201
+
202
+ - do not accept requests that would otherwise fail until we're ready for them
203
+ - make sure we reach out to our provider as soon as possible
204
+
205
+ This way we'll make sure all our setup regarding this specific _business rule_
206
+ is placed on a single entity, instead of scattered all across our code base.
207
+
208
+ With the changes to improve this behavior, the code will look like this:
209
+
210
+ ##### index.js
211
+
212
+ ```js
213
+ const Fastify = require('fastify')
214
+
215
+ const customerRoutes = require('./customer-routes')
216
+ const { setup, delay } = require('./delay-incoming-requests')
217
+
218
+ const server = new Fastify({ logger: true })
219
+
220
+ server.register(setup)
221
+
222
+ // Non-blocked URL
223
+ server.get('/ping', function (request, reply) {
224
+ reply.send({ error: false, ready: request.server.magicKey !== null })
225
+ })
226
+
227
+ // Webhook to handle the provider's response - also non-blocked
228
+ server.post('/webhook', function (request, reply) {
229
+ // It's good practice to validate webhook requests really come from
230
+ // whoever you expect. This is skipped in this sample for the sake
231
+ // of simplicity
232
+
233
+ const { magicKey } = request.body
234
+ request.server.magicKey = magicKey
235
+ request.log.info('Ready for customer requests!')
236
+
237
+ reply.send({ error: false })
238
+ })
239
+
240
+ // Blocked URLs
241
+ // Mind we're building a new plugin by calling the `delay` factory with our
242
+ // customerRoutes plugin
243
+ server.register(delay(customerRoutes), { prefix: '/v1' })
244
+
245
+ server.listen({ port: '1234' })
246
+ ```
247
+
248
+ ##### provider.js
249
+
250
+ ```js
251
+ const { fetch } = require('undici')
252
+ const { setTimeout } = require('node:timers/promises')
253
+
254
+ const MAGIC_KEY = '12345'
255
+
256
+ const delay = setTimeout
257
+
258
+ exports.thirdPartyMagicKeyGenerator = async (ms) => {
259
+ // Simulate processing delay
260
+ await delay(ms)
261
+
262
+ // Simulate webhook request to our server
263
+ const { status } = await fetch(
264
+ 'http://localhost:1234/webhook',
265
+ {
266
+ body: JSON.stringify({ magicKey: MAGIC_KEY }),
267
+ method: 'POST',
268
+ headers: {
269
+ 'content-type': 'application/json',
270
+ },
271
+ },
272
+ )
273
+
274
+ if (status !== 200) {
275
+ throw new Error('Failed to fetch magic key')
276
+ }
277
+ }
278
+
279
+ exports.fetchSensitiveData = async (key) => {
280
+ // Simulate processing delay
281
+ await delay(700)
282
+ const data = { sensitive: true }
283
+
284
+ if (key === MAGIC_KEY) {
285
+ return data
286
+ }
287
+
288
+ throw new Error('Invalid key')
289
+ }
290
+ ```
291
+
292
+ ##### delay-incoming-requests.js
293
+
294
+ ```js
295
+ const fp = require('fastify-plugin')
296
+
297
+ const provider = require('./provider')
298
+
299
+ const USUAL_WAIT_TIME_MS = 5000
300
+
301
+ async function setup(fastify) {
302
+ // As soon as we're listening for requests, let's work our magic
303
+ fastify.server.on('listening', doMagic)
304
+
305
+ // Set up the placeholder for the magicKey
306
+ fastify.decorate('magicKey')
307
+
308
+ // Our magic -- important to make sure errors are handled. Beware of async
309
+ // functions outside `try/catch` blocks
310
+ // If an error is thrown at this point and not captured it'll crash the
311
+ // application
312
+ function doMagic() {
313
+ fastify.log.info('Doing magic!')
314
+
315
+ provider.thirdPartyMagicKeyGenerator(USUAL_WAIT_TIME_MS)
316
+ .catch((error) => {
317
+ fastify.log.error({
318
+ error,
319
+ message: 'Got an error while trying to get the magic key!'
320
+ })
321
+
322
+ // Since we won't be able to serve requests, might as well wrap
323
+ // things up
324
+ fastify.close(() => process.exit(1))
325
+ })
326
+ }
327
+ }
328
+
329
+ const delay = (routes) =>
330
+ function (fastify, opts, done) {
331
+ // Make sure customer requests won't be accepted if the magicKey is not
332
+ // available
333
+ fastify.addHook('onRequest', function (request, reply, next) {
334
+ if (!request.server.magicKey) {
335
+ reply.statusCode = 503
336
+ reply.header('Retry-After', USUAL_WAIT_TIME_MS)
337
+ reply.send({ error: true, retryInMs: USUAL_WAIT_TIME_MS })
338
+ }
339
+
340
+ next()
341
+ })
342
+
343
+ // Register to-be-delayed routes
344
+ fastify.register(routes, opts)
345
+
346
+ done()
347
+ }
348
+
349
+ module.exports = {
350
+ setup: fp(setup),
351
+ delay,
352
+ }
353
+ ```
354
+
355
+ ##### customer-routes.js
356
+
357
+ ```js
358
+ const fp = require('fastify-plugin')
359
+
360
+ const provider = require('./provider')
361
+
362
+ module.exports = fp(async function (fastify) {
363
+ fastify.get('*', async function (request ,reply) {
364
+ try {
365
+ const data = await provider.fetchSensitiveData(request.server.magicKey)
366
+ return { customer: true, error: false }
367
+ } catch (error) {
368
+ request.log.error({
369
+ error,
370
+ message: 'Failed at fetching sensitive data from provider',
371
+ })
372
+
373
+ reply.statusCode = 500
374
+ return { customer: null, error: true }
375
+ }
376
+ })
377
+ })
378
+ ```
379
+
380
+ There is a very specific change on the previously existing files that is worth
381
+ mentioning: Beforehand we were using the `server.listen` callback to start the
382
+ authentication process with the external provider and we were decorating the
383
+ `server` object right before initializing the server. That was bloating our
384
+ server initialization setup with unnecessary code and didn't have much to do
385
+ with starting the Fastify server. It was a business logic that didn't have its
386
+ specific place in the code base.
387
+
388
+ Now we've implemented the `delayIncomingRequests` plugin in the
389
+ `delay-incoming-requests.js` file. That's, in truth, a module split into two
390
+ different plugins that will build up to a single use-case. That's the brains of
391
+ our operation. Let's walk through what the plugins do:
392
+
393
+ ##### setup
394
+
395
+ The `setup` plugin is responsible for making sure we reach out to our provider
396
+ asap and store the `magicKey` somewhere available to all our handlers.
397
+
398
+ ```js
399
+ fastify.server.on('listening', doMagic)
400
+ ```
401
+
402
+ As soon as the server starts listening (very similar behavior to adding a piece
403
+ of code to the `server.listen`'s callback function) a `listening` event is
404
+ emitted (for more info refer to
405
+ https://nodejs.org/api/net.html#event-listening). We use that to reach out to
406
+ our provider as soon as possible, with the `doMagic` function.
407
+
408
+ ```js
409
+ fastify.decorate('magicKey')
410
+ ```
411
+
412
+ The `magicKey` decoration is also part of the plugin now. We initialize it with
413
+ a placeholder, waiting for the valid value to be retrieved.
414
+
415
+ ##### delay
416
+
417
+ `delay` is not a plugin itself. It's actually a plugin *factory*. It expects a
418
+ Fastify plugin with `routes` and exports the actual plugin that'll handle
419
+ enveloping those routes with an `onRequest` hook that will make sure no requests
420
+ are handled until we're ready for them.
421
+
422
+ ```js
423
+ const delay = (routes) =>
424
+ function (fastify, opts, done) {
425
+ // Make sure customer requests won't be accepted if the magicKey is not
426
+ // available
427
+ fastify.addHook('onRequest', function (request, reply, next) {
428
+ if (!request.server.magicKey) {
429
+ reply.statusCode = 503
430
+ reply.header('Retry-After', USUAL_WAIT_TIME_MS)
431
+ reply.send({ error: true, retryInMs: USUAL_WAIT_TIME_MS })
432
+ }
433
+
434
+ next()
435
+ })
436
+
437
+ // Register to-be-delayed routes
438
+ fastify.register(routes, opts)
439
+
440
+ done()
441
+ }
442
+ ```
443
+
444
+ Instead of updating every single controller that might use the `magicKey`, we
445
+ simply make sure that no route that's related to customer requests will be
446
+ served until we have everything ready. And there's more: we fail **FAST** and
447
+ have the possibility of giving the customer meaningful information, like how
448
+ long they should wait before retrying the request. Going even further, by
449
+ issuing a [`503` status
450
+ code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/503) we're
451
+ signaling to our infrastructure components (namely load balancers) that we're
452
+ still not ready to take incoming requests and they should redirect traffic to
453
+ other instances, if available. Additionally, we are providing a `Retry-After`
454
+ header with the time in milliseconds the client should wait before retrying.
455
+
456
+ It's noteworthy that we didn't use the `fastify-plugin` wrapper in the `delay`
457
+ factory. That's because we wanted the `onRequest` hook to only be set within
458
+ that specific scope and not to the scope that called it (in our case, the main
459
+ `server` object defined in `index.js`). `fastify-plugin` sets the
460
+ `skip-override` hidden property, which has a practical effect of making whatever
461
+ changes we make to our `fastify` object available to the upper scope. That's
462
+ also why we used it with the `customerRoutes` plugin: we wanted those routes to
463
+ be available to its calling scope, the `delay` plugin. For more info on that
464
+ subject refer to [Plugins](../Reference/Plugins.md#handle-the-scope).
465
+
466
+ Let's see how that behaves in action. If we fired our server up with `node
467
+ index.js` and made a few requests to test things out. These were the logs we'd
468
+ see (some bloat was removed to ease things up):
469
+
470
+ <!-- markdownlint-disable -->
471
+ ```sh
472
+ {"time":1650063793316,"msg":"Doing magic!"}
473
+ {"time":1650063793316,"msg":"Server listening at http://127.0.0.1:1234"}
474
+ {"time":1650063795030,"reqId":"req-1","req":{"method":"GET","url":"/v1","hostname":"localhost:1234","remoteAddress":"127.0.0.1","remotePort":51928},"msg":"incoming request"}
475
+ {"time":1650063795033,"reqId":"req-1","res":{"statusCode":503},"responseTime":2.5721680000424385,"msg":"request completed"}
476
+ {"time":1650063796248,"reqId":"req-2","req":{"method":"GET","url":"/ping","hostname":"localhost:1234","remoteAddress":"127.0.0.1","remotePort":51930},"msg":"incoming request"}
477
+ {"time":1650063796248,"reqId":"req-2","res":{"statusCode":200},"responseTime":0.4802369996905327,"msg":"request completed"}
478
+ {"time":1650063798377,"reqId":"req-3","req":{"method":"POST","url":"/webhook","hostname":"localhost:1234","remoteAddress":"127.0.0.1","remotePort":51932},"msg":"incoming request"}
479
+ {"time":1650063798379,"reqId":"req-3","msg":"Ready for customer requests!"}
480
+ {"time":1650063798379,"reqId":"req-3","res":{"statusCode":200},"responseTime":1.3567829988896847,"msg":"request completed"}
481
+ {"time":1650063799858,"reqId":"req-4","req":{"method":"GET","url":"/v1","hostname":"localhost:1234","remoteAddress":"127.0.0.1","remotePort":51934},"msg":"incoming request"}
482
+ {"time":1650063800561,"reqId":"req-4","res":{"statusCode":200},"responseTime":702.4662979990244,"msg":"request completed"}
483
+ ```
484
+ <!-- markdownlint-enable -->
485
+
486
+ Let's focus on a few parts:
487
+
488
+ ```sh
489
+ {"time":1650063793316,"msg":"Doing magic!"}
490
+ {"time":1650063793316,"msg":"Server listening at http://127.0.0.1:1234"}
491
+ ```
492
+
493
+ These are the initial logs we'd see as soon as the server started. We reach out
494
+ to the external provider as early as possible within a valid time window (we
495
+ couldn't do that before the server was ready to receive connections).
496
+
497
+ While the server is still not ready, a few requests are attempted:
498
+
499
+ <!-- markdownlint-disable -->
500
+ ```sh
501
+ {"time":1650063795030,"reqId":"req-1","req":{"method":"GET","url":"/v1","hostname":"localhost:1234","remoteAddress":"127.0.0.1","remotePort":51928},"msg":"incoming request"}
502
+ {"time":1650063795033,"reqId":"req-1","res":{"statusCode":503},"responseTime":2.5721680000424385,"msg":"request completed"}
503
+ {"time":1650063796248,"reqId":"req-2","req":{"method":"GET","url":"/ping","hostname":"localhost:1234","remoteAddress":"127.0.0.1","remotePort":51930},"msg":"incoming request"}
504
+ {"time":1650063796248,"reqId":"req-2","res":{"statusCode":200},"responseTime":0.4802369996905327,"msg":"request completed"}
505
+ ```
506
+ <!-- markdownlint-enable -->
507
+
508
+ The first one (`req-1`) was a `GET /v1`, that failed (**FAST** - `responseTime`
509
+ is in `ms`) with our `503` status code and the meaningful information in the
510
+ response. Below is the response for that request:
511
+
512
+ ```sh
513
+ HTTP/1.1 503 Service Unavailable
514
+ Connection: keep-alive
515
+ Content-Length: 31
516
+ Content-Type: application/json; charset=utf-8
517
+ Date: Fri, 15 Apr 2022 23:03:15 GMT
518
+ Keep-Alive: timeout=5
519
+ Retry-After: 5000
520
+
521
+ {
522
+ "error": true,
523
+ "retryInMs": 5000
524
+ }
525
+ ```
526
+
527
+ Then we attempted a new request (`req-2`), which was a `GET /ping`. As expected,
528
+ since that was not one of the requests we asked our plugin to filter, it
529
+ succeeded. That could also be used as a means of informing an interested party
530
+ whether or not we were ready to serve requests with the `ready` field. Although
531
+ `/ping` is more commonly associated with *liveness* checks and that would be
532
+ the responsibility of a *readiness* check. The curious reader can get more info
533
+ on these terms in the article
534
+ ["Kubernetes best practices: Setting up health checks with readiness and liveness probes"](
535
+ https://cloud.google.com/blog/products/containers-kubernetes/kubernetes-best-practices-setting-up-health-checks-with-readiness-and-liveness-probes).
536
+
537
+ Below is the response to that request:
538
+
539
+ ```sh
540
+ HTTP/1.1 200 OK
541
+ Connection: keep-alive
542
+ Content-Length: 29
543
+ Content-Type: application/json; charset=utf-8
544
+ Date: Fri, 15 Apr 2022 23:03:16 GMT
545
+ Keep-Alive: timeout=5
546
+
547
+ {
548
+ "error": false,
549
+ "ready": false
550
+ }
551
+ ```
552
+
553
+ After that, there were more interesting log messages:
554
+
555
+ <!-- markdownlint-disable -->
556
+ ```sh
557
+ {"time":1650063798377,"reqId":"req-3","req":{"method":"POST","url":"/webhook","hostname":"localhost:1234","remoteAddress":"127.0.0.1","remotePort":51932},"msg":"incoming request"}
558
+ {"time":1650063798379,"reqId":"req-3","msg":"Ready for customer requests!"}
559
+ {"time":1650063798379,"reqId":"req-3","res":{"statusCode":200},"responseTime":1.3567829988896847,"msg":"request completed"}
560
+ ```
561
+ <!-- markdownlint-enable -->
562
+
563
+ This time it was our simulated external provider hitting us to let us know
564
+ authentication had gone well and telling us what our `magicKey` was. We saved
565
+ that into our `magicKey` decorator and celebrated with a log message saying we
566
+ were now ready for customers to hit us!
567
+
568
+ <!-- markdownlint-disable -->
569
+ ```sh
570
+ {"time":1650063799858,"reqId":"req-4","req":{"method":"GET","url":"/v1","hostname":"localhost:1234","remoteAddress":"127.0.0.1","remotePort":51934},"msg":"incoming request"}
571
+ {"time":1650063800561,"reqId":"req-4","res":{"statusCode":200},"responseTime":702.4662979990244,"msg":"request completed"}
572
+ ```
573
+ <!-- markdownlint-enable -->
574
+
575
+ Finally, a final `GET /v1` request was made and, this time, it succeeded. Its
576
+ response was the following:
577
+
578
+ ```sh
579
+ HTTP/1.1 200 OK
580
+ Connection: keep-alive
581
+ Content-Length: 31
582
+ Content-Type: application/json; charset=utf-8
583
+ Date: Fri, 15 Apr 2022 23:03:20 GMT
584
+ Keep-Alive: timeout=5
585
+
586
+ {
587
+ "customer": true,
588
+ "error": false
589
+ }
590
+ ```
591
+
592
+ ## Conclusion
593
+
594
+ Specifics of the implementation will vary from one problem to another, but the
595
+ main goal of this guide was to show a very specific use case of an issue that
596
+ could be solved within Fastify's ecosystem.
597
+
598
+ This guide is a tutorial on the use of plugins, decorators, and hooks to solve
599
+ the problem of delaying serving specific requests on our application. It's not
600
+ production-ready, as it keeps local state (the `magicKey`) and it's not
601
+ horizontally scalable (we don't want to flood our provider, right?). One way of
602
+ improving it would be storing the `magicKey` somewhere else (perhaps a cache
603
+ database?).
604
+
605
+ The keywords here were [Decorators](../Reference/Decorators.md),
606
+ [Hooks](../Reference/Hooks.md), and [Plugins](../Reference/Plugins.md).
607
+ Combining what Fastify has to offer can lead to very ingenious and creative
608
+ solutions to a wide variety of problems. Let's be creative! :)