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,413 @@
1
+ 'use strict'
2
+
3
+ const { AsyncResource } = require('node:async_hooks')
4
+ const { FifoMap: Fifo } = require('toad-cache')
5
+ const { parse: secureJsonParse } = require('secure-json-parse')
6
+ const ContentType = require('./content-type')
7
+ const {
8
+ kDefaultJsonParse,
9
+ kContentTypeParser,
10
+ kBodyLimit,
11
+ kRequestPayloadStream,
12
+ kState,
13
+ kTestInternals,
14
+ kReplyIsError,
15
+ kRouteContext
16
+ } = require('./symbols')
17
+
18
+ const {
19
+ FST_ERR_CTP_INVALID_TYPE,
20
+ FST_ERR_CTP_EMPTY_TYPE,
21
+ FST_ERR_CTP_ALREADY_PRESENT,
22
+ FST_ERR_CTP_INVALID_HANDLER,
23
+ FST_ERR_CTP_INVALID_PARSE_TYPE,
24
+ FST_ERR_CTP_BODY_TOO_LARGE,
25
+ FST_ERR_CTP_INVALID_MEDIA_TYPE,
26
+ FST_ERR_CTP_INVALID_CONTENT_LENGTH,
27
+ FST_ERR_CTP_EMPTY_JSON_BODY,
28
+ FST_ERR_CTP_INSTANCE_ALREADY_STARTED,
29
+ FST_ERR_CTP_INVALID_JSON_BODY
30
+ } = require('./errors')
31
+ const { FSTSEC001 } = require('./warnings')
32
+
33
+ function ContentTypeParser (bodyLimit, onProtoPoisoning, onConstructorPoisoning) {
34
+ this[kDefaultJsonParse] = getDefaultJsonParser(onProtoPoisoning, onConstructorPoisoning)
35
+ // using a map instead of a plain object to avoid prototype hijack attacks
36
+ this.customParsers = new Map()
37
+ this.customParsers.set('application/json', new Parser(true, false, bodyLimit, this[kDefaultJsonParse]))
38
+ this.customParsers.set('text/plain', new Parser(true, false, bodyLimit, defaultPlainTextParser))
39
+ this.parserList = ['application/json', 'text/plain']
40
+ this.parserRegExpList = []
41
+ this.cache = new Fifo(100)
42
+ }
43
+
44
+ ContentTypeParser.prototype.add = function (contentType, opts, parserFn) {
45
+ const contentTypeIsString = typeof contentType === 'string'
46
+
47
+ if (contentTypeIsString) {
48
+ contentType = contentType.trim().toLowerCase()
49
+ if (contentType.length === 0) throw new FST_ERR_CTP_EMPTY_TYPE()
50
+ } else if (!(contentType instanceof RegExp)) {
51
+ throw new FST_ERR_CTP_INVALID_TYPE()
52
+ }
53
+
54
+ if (typeof parserFn !== 'function') {
55
+ throw new FST_ERR_CTP_INVALID_HANDLER()
56
+ }
57
+
58
+ if (this.existingParser(contentType)) {
59
+ throw new FST_ERR_CTP_ALREADY_PRESENT(contentType)
60
+ }
61
+
62
+ if (opts.parseAs !== undefined) {
63
+ if (opts.parseAs !== 'string' && opts.parseAs !== 'buffer') {
64
+ throw new FST_ERR_CTP_INVALID_PARSE_TYPE(opts.parseAs)
65
+ }
66
+ }
67
+
68
+ const parser = new Parser(
69
+ opts.parseAs === 'string',
70
+ opts.parseAs === 'buffer',
71
+ opts.bodyLimit,
72
+ parserFn
73
+ )
74
+
75
+ if (contentType === '*') {
76
+ this.customParsers.set('', parser)
77
+ } else {
78
+ if (contentTypeIsString) {
79
+ const ct = new ContentType(contentType)
80
+ if (ct.isValid === false) {
81
+ throw new FST_ERR_CTP_INVALID_TYPE()
82
+ }
83
+ const normalizedContentType = ct.toString()
84
+ this.parserList.unshift(normalizedContentType)
85
+ this.customParsers.set(normalizedContentType, parser)
86
+ } else {
87
+ validateRegExp(contentType)
88
+ this.parserRegExpList.unshift(contentType)
89
+ this.customParsers.set(contentType.toString(), parser)
90
+ }
91
+ }
92
+ }
93
+
94
+ ContentTypeParser.prototype.hasParser = function (contentType) {
95
+ if (typeof contentType === 'string') {
96
+ contentType = new ContentType(contentType).toString()
97
+ } else {
98
+ if (!(contentType instanceof RegExp)) throw new FST_ERR_CTP_INVALID_TYPE()
99
+ contentType = contentType.toString()
100
+ }
101
+
102
+ return this.customParsers.has(contentType)
103
+ }
104
+
105
+ ContentTypeParser.prototype.existingParser = function (contentType) {
106
+ if (typeof contentType === 'string') {
107
+ const ct = new ContentType(contentType).toString()
108
+ if (contentType === 'application/json' && this.customParsers.has(contentType)) {
109
+ return this.customParsers.get(ct).fn !== this[kDefaultJsonParse]
110
+ }
111
+ if (contentType === 'text/plain' && this.customParsers.has(contentType)) {
112
+ return this.customParsers.get(ct).fn !== defaultPlainTextParser
113
+ }
114
+ }
115
+
116
+ return this.hasParser(contentType)
117
+ }
118
+
119
+ ContentTypeParser.prototype.getParser = function (contentType) {
120
+ if (typeof contentType === 'string') {
121
+ contentType = new ContentType(contentType)
122
+ }
123
+ const ct = contentType.toString()
124
+
125
+ let parser = this.cache.get(ct)
126
+ if (parser !== undefined) return parser
127
+ parser = this.customParsers.get(ct)
128
+ if (parser !== undefined) {
129
+ this.cache.set(ct, parser)
130
+ return parser
131
+ }
132
+
133
+ // We have conflicting desires across our test suite. In some cases, we
134
+ // expect to get a parser by just passing the media-type. In others, we expect
135
+ // to get a parser registered under the media-type while also providing
136
+ // parameters. And in yet others, we expect to register a parser under the
137
+ // media-type and have it apply to any request with a header that starts
138
+ // with that type.
139
+ parser = this.customParsers.get(contentType.mediaType)
140
+ if (parser !== undefined) {
141
+ return parser
142
+ }
143
+
144
+ for (let j = 0; j !== this.parserRegExpList.length; ++j) {
145
+ const parserRegExp = this.parserRegExpList[j]
146
+ if (parserRegExp.test(ct)) {
147
+ parser = this.customParsers.get(parserRegExp.toString())
148
+ this.cache.set(ct, parser)
149
+ return parser
150
+ }
151
+ }
152
+
153
+ return this.customParsers.get('')
154
+ }
155
+
156
+ ContentTypeParser.prototype.removeAll = function () {
157
+ this.customParsers = new Map()
158
+ this.parserRegExpList = []
159
+ this.parserList = []
160
+ this.cache = new Fifo(100)
161
+ }
162
+
163
+ ContentTypeParser.prototype.remove = function (contentType) {
164
+ let parsers
165
+
166
+ if (typeof contentType === 'string') {
167
+ contentType = new ContentType(contentType).toString()
168
+ parsers = this.parserList
169
+ } else {
170
+ if (!(contentType instanceof RegExp)) throw new FST_ERR_CTP_INVALID_TYPE()
171
+ contentType = contentType.toString()
172
+ parsers = this.parserRegExpList
173
+ }
174
+
175
+ const removed = this.customParsers.delete(contentType)
176
+ const idx = parsers.findIndex(ct => ct.toString() === contentType)
177
+
178
+ if (idx > -1) {
179
+ parsers.splice(idx, 1)
180
+ }
181
+
182
+ return removed || idx > -1
183
+ }
184
+
185
+ ContentTypeParser.prototype.run = function (contentType, handler, request, reply) {
186
+ const parser = this.getParser(contentType)
187
+
188
+ if (parser === undefined) {
189
+ if (request.is404 === true) {
190
+ handler(request, reply)
191
+ return
192
+ }
193
+
194
+ reply[kReplyIsError] = true
195
+ reply.send(new FST_ERR_CTP_INVALID_MEDIA_TYPE())
196
+ return
197
+ }
198
+
199
+ const resource = new AsyncResource('content-type-parser:run', request)
200
+ const done = resource.bind(onDone)
201
+
202
+ if (parser.asString === true || parser.asBuffer === true) {
203
+ rawBody(
204
+ request,
205
+ reply,
206
+ reply[kRouteContext]._parserOptions,
207
+ parser,
208
+ done
209
+ )
210
+ return
211
+ }
212
+
213
+ const result = parser.fn(request, request[kRequestPayloadStream], done)
214
+ if (result && typeof result.then === 'function') {
215
+ result.then(body => { done(null, body) }, done)
216
+ }
217
+
218
+ function onDone (error, body) {
219
+ resource.emitDestroy()
220
+ if (error != null) {
221
+ // We must close the connection as the client may
222
+ // send more data
223
+ reply.header('connection', 'close')
224
+ reply[kReplyIsError] = true
225
+ reply.send(error)
226
+ return
227
+ }
228
+ request.body = body
229
+ handler(request, reply)
230
+ }
231
+ }
232
+
233
+ function rawBody (request, reply, options, parser, done) {
234
+ const asString = parser.asString === true
235
+ const limit = options.limit === null ? parser.bodyLimit : options.limit
236
+ const contentLength = Number(request.headers['content-length'])
237
+
238
+ if (contentLength > limit) {
239
+ done(new FST_ERR_CTP_BODY_TOO_LARGE(), undefined)
240
+ return
241
+ }
242
+
243
+ let receivedLength = 0
244
+ let body = asString ? '' : []
245
+ const payload = request[kRequestPayloadStream] || request.raw
246
+
247
+ if (asString) {
248
+ payload.setEncoding('utf8')
249
+ }
250
+
251
+ payload.on('data', onData)
252
+ payload.on('end', onEnd)
253
+ payload.on('error', onEnd)
254
+ payload.resume()
255
+
256
+ function onData (chunk) {
257
+ receivedLength += asString ? Buffer.byteLength(chunk) : chunk.length
258
+ const { receivedEncodedLength = 0 } = payload
259
+ // The resulting body length must not exceed bodyLimit (see "zip bomb").
260
+ // The case when encoded length is larger than received length is rather theoretical,
261
+ // unless the stream returned by preParsing hook is broken and reports wrong value.
262
+ if (receivedLength > limit || receivedEncodedLength > limit) {
263
+ payload.removeListener('data', onData)
264
+ payload.removeListener('end', onEnd)
265
+ payload.removeListener('error', onEnd)
266
+ done(new FST_ERR_CTP_BODY_TOO_LARGE(), undefined)
267
+ return
268
+ }
269
+
270
+ if (asString) {
271
+ body += chunk
272
+ } else {
273
+ body.push(chunk)
274
+ }
275
+ }
276
+
277
+ function onEnd (err) {
278
+ payload.removeListener('data', onData)
279
+ payload.removeListener('end', onEnd)
280
+ payload.removeListener('error', onEnd)
281
+
282
+ if (err != null) {
283
+ if (!(typeof err.statusCode === 'number' && err.statusCode >= 400)) {
284
+ err.statusCode = 400
285
+ }
286
+ done(err, undefined)
287
+ return
288
+ }
289
+
290
+ if (!Number.isNaN(contentLength) && (payload.receivedEncodedLength || receivedLength) !== contentLength) {
291
+ done(new FST_ERR_CTP_INVALID_CONTENT_LENGTH(), undefined)
292
+ return
293
+ }
294
+
295
+ if (!asString) {
296
+ body = Buffer.concat(body)
297
+ }
298
+
299
+ const result = parser.fn(request, body, done)
300
+ if (result && typeof result.then === 'function') {
301
+ result.then(body => { done(null, body) }, done)
302
+ }
303
+ }
304
+ }
305
+
306
+ function getDefaultJsonParser (onProtoPoisoning, onConstructorPoisoning) {
307
+ const parseOptions = { protoAction: onProtoPoisoning, constructorAction: onConstructorPoisoning }
308
+
309
+ return defaultJsonParser
310
+
311
+ function defaultJsonParser (req, body, done) {
312
+ if (body.length === 0) {
313
+ done(new FST_ERR_CTP_EMPTY_JSON_BODY(), undefined)
314
+ return
315
+ }
316
+ try {
317
+ done(null, secureJsonParse(body, parseOptions))
318
+ } catch {
319
+ done(new FST_ERR_CTP_INVALID_JSON_BODY(), undefined)
320
+ }
321
+ }
322
+ }
323
+
324
+ function defaultPlainTextParser (req, body, done) {
325
+ done(null, body)
326
+ }
327
+
328
+ function Parser (asString, asBuffer, bodyLimit, fn) {
329
+ this.asString = asString
330
+ this.asBuffer = asBuffer
331
+ this.bodyLimit = bodyLimit
332
+ this.fn = fn
333
+ }
334
+
335
+ function buildContentTypeParser (c) {
336
+ const contentTypeParser = new ContentTypeParser()
337
+ contentTypeParser[kDefaultJsonParse] = c[kDefaultJsonParse]
338
+ contentTypeParser.customParsers = new Map(c.customParsers.entries())
339
+ contentTypeParser.parserList = c.parserList.slice()
340
+ contentTypeParser.parserRegExpList = c.parserRegExpList.slice()
341
+ return contentTypeParser
342
+ }
343
+
344
+ function addContentTypeParser (contentType, opts, parser) {
345
+ if (this[kState].started) {
346
+ throw new FST_ERR_CTP_INSTANCE_ALREADY_STARTED('addContentTypeParser')
347
+ }
348
+
349
+ if (typeof opts === 'function') {
350
+ parser = opts
351
+ opts = {}
352
+ }
353
+
354
+ if (!opts) opts = {}
355
+ if (!opts.bodyLimit) opts.bodyLimit = this[kBodyLimit]
356
+
357
+ if (Array.isArray(contentType)) {
358
+ contentType.forEach((type) => this[kContentTypeParser].add(type, opts, parser))
359
+ } else {
360
+ this[kContentTypeParser].add(contentType, opts, parser)
361
+ }
362
+
363
+ return this
364
+ }
365
+
366
+ function hasContentTypeParser (contentType) {
367
+ return this[kContentTypeParser].hasParser(contentType)
368
+ }
369
+
370
+ function removeContentTypeParser (contentType) {
371
+ if (this[kState].started) {
372
+ throw new FST_ERR_CTP_INSTANCE_ALREADY_STARTED('removeContentTypeParser')
373
+ }
374
+
375
+ if (Array.isArray(contentType)) {
376
+ for (const type of contentType) {
377
+ this[kContentTypeParser].remove(type)
378
+ }
379
+ } else {
380
+ this[kContentTypeParser].remove(contentType)
381
+ }
382
+ }
383
+
384
+ function removeAllContentTypeParsers () {
385
+ if (this[kState].started) {
386
+ throw new FST_ERR_CTP_INSTANCE_ALREADY_STARTED('removeAllContentTypeParsers')
387
+ }
388
+
389
+ this[kContentTypeParser].removeAll()
390
+ }
391
+
392
+ function validateRegExp (regexp) {
393
+ // RegExp should either start with ^ or include ;?
394
+ // It can ensure the user is properly detect the essence
395
+ // MIME types.
396
+ if (regexp.source[0] !== '^' && regexp.source.includes(';?') === false) {
397
+ FSTSEC001(regexp.source)
398
+ }
399
+ }
400
+
401
+ module.exports = ContentTypeParser
402
+ module.exports.helpers = {
403
+ buildContentTypeParser,
404
+ addContentTypeParser,
405
+ hasContentTypeParser,
406
+ removeContentTypeParser,
407
+ removeAllContentTypeParsers
408
+ }
409
+ module.exports.defaultParsers = {
410
+ getDefaultJsonParser,
411
+ defaultTextParser: defaultPlainTextParser
412
+ }
413
+ module.exports[kTestInternals] = { rawBody }
@@ -0,0 +1,160 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * keyValuePairsReg is used to split the parameters list into associated
5
+ * key value pairings. The leading `(?:^|;)\s*` anchor ensures the regex
6
+ * only attempts matches at parameter boundaries, preventing quadratic
7
+ * backtracking on malformed input.
8
+ *
9
+ * @see https://httpwg.org/specs/rfc9110.html#parameter
10
+ * @type {RegExp}
11
+ */
12
+ const keyValuePairsReg = /(?:^|;)\s*([\w!#$%&'*+.^`|~-]+)=([^;]*)/gm
13
+
14
+ /**
15
+ * typeNameReg is used to validate that the first part of the media-type
16
+ * does not use disallowed characters. Types must consist solely of
17
+ * characters that match the specified character class. It must terminate
18
+ * with a matching character.
19
+ *
20
+ * @see https://httpwg.org/specs/rfc9110.html#rule.token.separators
21
+ * @type {RegExp}
22
+ */
23
+ const typeNameReg = /^[\w!#$%&'*+.^`|~-]+$/
24
+
25
+ /**
26
+ * subtypeNameReg is used to validate that the second part of the media-type
27
+ * does not use disallowed characters. Subtypes must consist solely of
28
+ * characters that match the specified character class, and optionally
29
+ * terminated with any amount of whitespace characters. Without the terminating
30
+ * anchor (`$`), the regular expression will match the leading portion of a
31
+ * string instead of the whole string.
32
+ *
33
+ * @see https://httpwg.org/specs/rfc9110.html#rule.token.separators
34
+ * @type {RegExp}
35
+ */
36
+ const subtypeNameReg = /^[\w!#$%&'*+.^`|~-]+\s*$/
37
+
38
+ /**
39
+ * ContentType parses and represents the value of the content-type header.
40
+ *
41
+ * @see https://httpwg.org/specs/rfc9110.html#media.type
42
+ * @see https://httpwg.org/specs/rfc9110.html#parameter
43
+ */
44
+ class ContentType {
45
+ #valid = false
46
+ #empty = true
47
+ #type = ''
48
+ #subtype = ''
49
+ #parameters = new Map()
50
+ #string
51
+
52
+ constructor (headerValue) {
53
+ if (headerValue == null || headerValue === '' || headerValue === 'undefined') {
54
+ return
55
+ }
56
+
57
+ let sepIdx = headerValue.indexOf(';')
58
+ if (sepIdx === -1) {
59
+ // The value is the simplest `type/subtype` variant.
60
+ sepIdx = headerValue.indexOf('/')
61
+ if (sepIdx === -1) {
62
+ // Got a string without the correct `type/subtype` format.
63
+ return
64
+ }
65
+
66
+ const type = headerValue.slice(0, sepIdx).trimStart().toLowerCase()
67
+ const subtype = headerValue.slice(sepIdx + 1).trimEnd().toLowerCase()
68
+
69
+ if (
70
+ typeNameReg.test(type) === true &&
71
+ subtypeNameReg.test(subtype) === true
72
+ ) {
73
+ this.#valid = true
74
+ this.#empty = false
75
+ this.#type = type
76
+ this.#subtype = subtype
77
+ }
78
+
79
+ return
80
+ }
81
+
82
+ // We have a `type/subtype; params=list...` header value.
83
+ const mediaType = headerValue.slice(0, sepIdx).toLowerCase()
84
+ const paramsList = headerValue.slice(sepIdx + 1).trim()
85
+
86
+ sepIdx = mediaType.indexOf('/')
87
+ if (sepIdx === -1) {
88
+ // We got an invalid string like `something; params=list...`.
89
+ return
90
+ }
91
+ const type = mediaType.slice(0, sepIdx).trimStart()
92
+ const subtype = mediaType.slice(sepIdx + 1).trimEnd()
93
+
94
+ if (
95
+ typeNameReg.test(type) === false ||
96
+ subtypeNameReg.test(subtype) === false
97
+ ) {
98
+ // Some portion of the media-type is using invalid characters. Therefore,
99
+ // the content-type header is invalid.
100
+ return
101
+ }
102
+ this.#type = type
103
+ this.#subtype = subtype
104
+ this.#valid = true
105
+ this.#empty = false
106
+
107
+ let matches = keyValuePairsReg.exec(paramsList)
108
+ while (matches) {
109
+ const key = matches[1]
110
+ const value = matches[2]
111
+ if (value[0] === '"') {
112
+ if (value.at(-1) !== '"') {
113
+ this.#parameters.set(key, 'invalid quoted string')
114
+ matches = keyValuePairsReg.exec(paramsList)
115
+ continue
116
+ }
117
+ // We should probably verify the value matches a quoted string
118
+ // (https://httpwg.org/specs/rfc9110.html#rule.quoted-string) value.
119
+ // But we are not really doing much with the parameter values, so we
120
+ // are omitting that at this time.
121
+ this.#parameters.set(key, value.slice(1, value.length - 1))
122
+ } else {
123
+ this.#parameters.set(key, value)
124
+ }
125
+ matches = keyValuePairsReg.exec(paramsList)
126
+ }
127
+ }
128
+
129
+ get [Symbol.toStringTag] () { return 'ContentType' }
130
+
131
+ get isEmpty () { return this.#empty }
132
+
133
+ get isValid () { return this.#valid }
134
+
135
+ get mediaType () { return `${this.#type}/${this.#subtype}` }
136
+
137
+ get type () { return this.#type }
138
+
139
+ get subtype () { return this.#subtype }
140
+
141
+ get parameters () { return this.#parameters }
142
+
143
+ toString () {
144
+ /* c8 ignore next: we don't need to verify the cache */
145
+ if (this.#string) return this.#string
146
+ const parameters = []
147
+ for (const [key, value] of this.#parameters.entries()) {
148
+ parameters.push(`${key}="${value}"`)
149
+ }
150
+ const result = [this.#type, '/', this.#subtype]
151
+ if (parameters.length > 0) {
152
+ result.push('; ')
153
+ result.push(parameters.join('; '))
154
+ }
155
+ this.#string = result.join('')
156
+ return this.#string
157
+ }
158
+ }
159
+
160
+ module.exports = ContentType
package/lib/context.js ADDED
@@ -0,0 +1,98 @@
1
+ 'use strict'
2
+
3
+ const {
4
+ kFourOhFourContext,
5
+ kReplySerializerDefault,
6
+ kSchemaErrorFormatter,
7
+ kErrorHandler,
8
+ kChildLoggerFactory,
9
+ kOptions,
10
+ kReply,
11
+ kRequest,
12
+ kBodyLimit,
13
+ kLogLevel,
14
+ kContentTypeParser,
15
+ kRouteByFastify,
16
+ kRequestCacheValidateFns,
17
+ kReplyCacheSerializeFns,
18
+ kHandlerTimeout
19
+ } = require('./symbols.js')
20
+
21
+ // Object that holds the context of every request
22
+ // Every route holds an instance of this object.
23
+ function Context ({
24
+ schema,
25
+ handler,
26
+ config,
27
+ requestIdLogLabel,
28
+ childLoggerFactory,
29
+ errorHandler,
30
+ bodyLimit,
31
+ logLevel,
32
+ logSerializers,
33
+ attachValidation,
34
+ validatorCompiler,
35
+ serializerCompiler,
36
+ replySerializer,
37
+ schemaErrorFormatter,
38
+ exposeHeadRoute,
39
+ prefixTrailingSlash,
40
+ server,
41
+ isFastify,
42
+ handlerTimeout
43
+ }) {
44
+ this.schema = schema
45
+ this.handler = handler
46
+ this.Reply = server[kReply]
47
+ this.Request = server[kRequest]
48
+ this.contentTypeParser = server[kContentTypeParser]
49
+ this.onRequest = null
50
+ this.onSend = null
51
+ this.onError = null
52
+ this.onTimeout = null
53
+ this.preHandler = null
54
+ this.onResponse = null
55
+ this.preSerialization = null
56
+ this.onRequestAbort = null
57
+ this.config = config
58
+ this.errorHandler = errorHandler || server[kErrorHandler]
59
+ this.requestIdLogLabel = requestIdLogLabel || server[kOptions].requestIdLogLabel
60
+ this.childLoggerFactory = childLoggerFactory || server[kChildLoggerFactory]
61
+ this._middie = null
62
+ this._parserOptions = {
63
+ limit: bodyLimit || server[kBodyLimit]
64
+ }
65
+ this.exposeHeadRoute = exposeHeadRoute
66
+ this.prefixTrailingSlash = prefixTrailingSlash
67
+ this.logLevel = logLevel || server[kLogLevel]
68
+ this.logSerializers = logSerializers
69
+ this[kFourOhFourContext] = null
70
+ this.attachValidation = attachValidation
71
+ this[kReplySerializerDefault] = replySerializer
72
+ this.schemaErrorFormatter =
73
+ schemaErrorFormatter ||
74
+ server[kSchemaErrorFormatter] ||
75
+ defaultSchemaErrorFormatter
76
+ this[kRouteByFastify] = isFastify
77
+
78
+ this[kRequestCacheValidateFns] = null
79
+ this[kReplyCacheSerializeFns] = null
80
+ this.validatorCompiler = validatorCompiler || null
81
+ this.serializerCompiler = serializerCompiler || null
82
+
83
+ this.handlerTimeout = handlerTimeout || server[kHandlerTimeout] || 0
84
+ this.server = server
85
+ }
86
+
87
+ function defaultSchemaErrorFormatter (errors, dataVar) {
88
+ let text = ''
89
+ const separator = ', '
90
+
91
+ for (let i = 0; i !== errors.length; ++i) {
92
+ const e = errors[i]
93
+ text += dataVar + (e.instancePath || '') + ' ' + e.message + separator
94
+ }
95
+ return new Error(text.slice(0, -separator.length))
96
+ }
97
+
98
+ module.exports = Context