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
package/lib/reply.js ADDED
@@ -0,0 +1,1030 @@
1
+ 'use strict'
2
+
3
+ const eos = require('node:stream').finished
4
+
5
+ const {
6
+ kFourOhFourContext,
7
+ kReplyErrorHandlerCalled,
8
+ kReplyHijacked,
9
+ kReplyStartTime,
10
+ kReplyEndTime,
11
+ kReplySerializer,
12
+ kReplySerializerDefault,
13
+ kReplyIsError,
14
+ kReplyHeaders,
15
+ kReplyTrailers,
16
+ kReplyHasStatusCode,
17
+ kReplyIsRunningOnErrorHook,
18
+ kReplyNextErrorHandler,
19
+ kDisableRequestLogging,
20
+ kSchemaResponse,
21
+ kReplyCacheSerializeFns,
22
+ kSchemaController,
23
+ kOptions,
24
+ kRouteContext,
25
+ kTimeoutTimer,
26
+ kOnAbort,
27
+ kRequestSignal
28
+ } = require('./symbols.js')
29
+ const {
30
+ onSendHookRunner,
31
+ onResponseHookRunner,
32
+ preHandlerHookRunner,
33
+ preSerializationHookRunner
34
+ } = require('./hooks')
35
+
36
+ const internals = require('./handle-request.js')[Symbol.for('internals')]
37
+ const loggerUtils = require('./logger-factory')
38
+ const now = loggerUtils.now
39
+ const { handleError } = require('./error-handler')
40
+ const { getSchemaSerializer } = require('./schemas')
41
+
42
+ const CONTENT_TYPE = {
43
+ JSON: 'application/json; charset=utf-8',
44
+ PLAIN: 'text/plain; charset=utf-8',
45
+ OCTET: 'application/octet-stream'
46
+ }
47
+ const {
48
+ FST_ERR_REP_INVALID_PAYLOAD_TYPE,
49
+ FST_ERR_REP_RESPONSE_BODY_CONSUMED,
50
+ FST_ERR_REP_READABLE_STREAM_LOCKED,
51
+ FST_ERR_REP_ALREADY_SENT,
52
+ FST_ERR_SEND_INSIDE_ONERR,
53
+ FST_ERR_BAD_STATUS_CODE,
54
+ FST_ERR_BAD_TRAILER_NAME,
55
+ FST_ERR_BAD_TRAILER_VALUE,
56
+ FST_ERR_MISSING_SERIALIZATION_FN,
57
+ FST_ERR_MISSING_CONTENTTYPE_SERIALIZATION_FN,
58
+ FST_ERR_DEC_UNDECLARED
59
+ } = require('./errors')
60
+ const decorators = require('./decorate')
61
+
62
+ const toString = Object.prototype.toString
63
+
64
+ function Reply (res, request, log) {
65
+ this.raw = res
66
+ this[kReplySerializer] = null
67
+ this[kReplyErrorHandlerCalled] = false
68
+ this[kReplyIsError] = false
69
+ this[kReplyIsRunningOnErrorHook] = false
70
+ this.request = request
71
+ this[kReplyHeaders] = {}
72
+ this[kReplyTrailers] = null
73
+ this[kReplyHasStatusCode] = false
74
+ this[kReplyStartTime] = undefined
75
+ this.log = log
76
+ }
77
+ Reply.props = []
78
+
79
+ Object.defineProperties(Reply.prototype, {
80
+ [kRouteContext]: {
81
+ get () {
82
+ return this.request[kRouteContext]
83
+ }
84
+ },
85
+ elapsedTime: {
86
+ get () {
87
+ if (this[kReplyStartTime] === undefined) {
88
+ return 0
89
+ }
90
+ return (this[kReplyEndTime] || now()) - this[kReplyStartTime]
91
+ }
92
+ },
93
+ server: {
94
+ get () {
95
+ return this.request[kRouteContext].server
96
+ }
97
+ },
98
+ sent: {
99
+ enumerable: true,
100
+ get () {
101
+ // We are checking whether reply was hijacked or the response has ended.
102
+ return (this[kReplyHijacked] || this.raw.writableEnded) === true
103
+ }
104
+ },
105
+ statusCode: {
106
+ get () {
107
+ return this.raw.statusCode
108
+ },
109
+ set (value) {
110
+ this.code(value)
111
+ }
112
+ },
113
+ routeOptions: {
114
+ get () {
115
+ return this.request.routeOptions
116
+ }
117
+ }
118
+ })
119
+
120
+ Reply.prototype.writeEarlyHints = function (hints, callback) {
121
+ this.raw.writeEarlyHints(hints, callback)
122
+ return this
123
+ }
124
+
125
+ Reply.prototype.hijack = function () {
126
+ this[kReplyHijacked] = true
127
+ // Clear handler timeout and signal — hijacked replies manage their own lifecycle
128
+ if (this.request[kRequestSignal]) {
129
+ clearTimeout(this.request[kTimeoutTimer])
130
+ this.request[kTimeoutTimer] = null
131
+ if (this.request[kOnAbort]) {
132
+ this.request.raw.removeListener('close', this.request[kOnAbort])
133
+ this.request[kOnAbort] = null
134
+ }
135
+ }
136
+ return this
137
+ }
138
+
139
+ Reply.prototype.send = function (payload) {
140
+ if (this[kReplyIsRunningOnErrorHook]) {
141
+ throw new FST_ERR_SEND_INSIDE_ONERR()
142
+ }
143
+
144
+ if (this.sent === true) {
145
+ this.log.warn({ err: new FST_ERR_REP_ALREADY_SENT(this.request.url, this.request.method) })
146
+ return this
147
+ }
148
+
149
+ if (this[kReplyIsError] || payload instanceof Error) {
150
+ this[kReplyIsError] = false
151
+ onErrorHook(this, payload, onSendHook)
152
+ return this
153
+ }
154
+
155
+ if (payload === undefined) {
156
+ onSendHook(this, payload)
157
+ return this
158
+ }
159
+
160
+ const contentType = this.getHeader('content-type')
161
+ const hasContentType = contentType !== undefined
162
+
163
+ if (payload !== null) {
164
+ if (
165
+ // node:stream
166
+ typeof payload.pipe === 'function' ||
167
+ // node:stream/web
168
+ typeof payload.getReader === 'function' ||
169
+ // Response
170
+ toString.call(payload) === '[object Response]'
171
+ ) {
172
+ onSendHook(this, payload)
173
+ return this
174
+ }
175
+
176
+ if (payload.buffer instanceof ArrayBuffer) {
177
+ if (!hasContentType) {
178
+ this[kReplyHeaders]['content-type'] = CONTENT_TYPE.OCTET
179
+ }
180
+ const payloadToSend = Buffer.isBuffer(payload)
181
+ ? payload
182
+ : Buffer.from(payload.buffer, payload.byteOffset, payload.byteLength)
183
+ onSendHook(this, payloadToSend)
184
+ return this
185
+ }
186
+
187
+ if (!hasContentType && typeof payload === 'string') {
188
+ this[kReplyHeaders]['content-type'] = CONTENT_TYPE.PLAIN
189
+ onSendHook(this, payload)
190
+ return this
191
+ }
192
+ }
193
+
194
+ if (this[kReplySerializer] !== null) {
195
+ if (typeof payload !== 'string') {
196
+ preSerializationHook(this, payload)
197
+ return this
198
+ }
199
+ payload = this[kReplySerializer](payload)
200
+
201
+ // The indexOf below also matches custom json mimetypes such as 'application/hal+json' or 'application/ld+json'
202
+ } else if (!hasContentType || contentType.indexOf('json') !== -1) {
203
+ if (!hasContentType) {
204
+ this[kReplyHeaders]['content-type'] = CONTENT_TYPE.JSON
205
+ } else if (contentType.indexOf('charset') === -1) {
206
+ // If user doesn't set charset, we will set charset to utf-8
207
+ const customContentType = contentType.trim()
208
+ if (customContentType.endsWith(';')) {
209
+ // custom content-type is ended with ';'
210
+ this[kReplyHeaders]['content-type'] = `${customContentType} charset=utf-8`
211
+ } else {
212
+ this[kReplyHeaders]['content-type'] = `${customContentType}; charset=utf-8`
213
+ }
214
+ }
215
+
216
+ if (typeof payload !== 'string') {
217
+ preSerializationHook(this, payload)
218
+ return this
219
+ }
220
+ }
221
+
222
+ onSendHook(this, payload)
223
+
224
+ return this
225
+ }
226
+
227
+ Reply.prototype.getHeader = function (key) {
228
+ key = key.toLowerCase()
229
+ const value = this[kReplyHeaders][key]
230
+ return value !== undefined ? value : this.raw.getHeader(key)
231
+ }
232
+
233
+ Reply.prototype.getHeaders = function () {
234
+ return {
235
+ ...this.raw.getHeaders(),
236
+ ...this[kReplyHeaders]
237
+ }
238
+ }
239
+
240
+ Reply.prototype.hasHeader = function (key) {
241
+ key = key.toLowerCase()
242
+
243
+ return this[kReplyHeaders][key] !== undefined || this.raw.hasHeader(key)
244
+ }
245
+
246
+ Reply.prototype.removeHeader = function (key) {
247
+ // Node.js does not like headers with keys set to undefined,
248
+ // so we have to delete the key.
249
+ delete this[kReplyHeaders][key.toLowerCase()]
250
+ return this
251
+ }
252
+
253
+ Reply.prototype.header = function (key, value = '') {
254
+ key = key.toLowerCase()
255
+
256
+ if (this[kReplyHeaders][key] && key === 'set-cookie') {
257
+ // https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.2
258
+ if (typeof this[kReplyHeaders][key] === 'string') {
259
+ this[kReplyHeaders][key] = [this[kReplyHeaders][key]]
260
+ }
261
+
262
+ if (Array.isArray(value)) {
263
+ Array.prototype.push.apply(this[kReplyHeaders][key], value)
264
+ } else {
265
+ this[kReplyHeaders][key].push(value)
266
+ }
267
+ } else {
268
+ this[kReplyHeaders][key] = value
269
+ }
270
+
271
+ return this
272
+ }
273
+
274
+ Reply.prototype.headers = function (headers) {
275
+ const keys = Object.keys(headers)
276
+ for (let i = 0; i !== keys.length; ++i) {
277
+ const key = keys[i]
278
+ this.header(key, headers[key])
279
+ }
280
+
281
+ return this
282
+ }
283
+
284
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Trailer#directives
285
+ // https://datatracker.ietf.org/doc/html/rfc7230.html#chunked.trailer.part
286
+ const INVALID_TRAILERS = new Set([
287
+ 'transfer-encoding',
288
+ 'content-length',
289
+ 'host',
290
+ 'cache-control',
291
+ 'max-forwards',
292
+ 'te',
293
+ 'authorization',
294
+ 'set-cookie',
295
+ 'content-encoding',
296
+ 'content-type',
297
+ 'content-range',
298
+ 'trailer'
299
+ ])
300
+
301
+ Reply.prototype.trailer = function (key, fn) {
302
+ key = key.toLowerCase()
303
+ if (INVALID_TRAILERS.has(key)) {
304
+ throw new FST_ERR_BAD_TRAILER_NAME(key)
305
+ }
306
+ if (typeof fn !== 'function') {
307
+ throw new FST_ERR_BAD_TRAILER_VALUE(key, typeof fn)
308
+ }
309
+ if (this[kReplyTrailers] === null) this[kReplyTrailers] = {}
310
+ this[kReplyTrailers][key] = fn
311
+ return this
312
+ }
313
+
314
+ Reply.prototype.hasTrailer = function (key) {
315
+ return this[kReplyTrailers]?.[key.toLowerCase()] !== undefined
316
+ }
317
+
318
+ Reply.prototype.removeTrailer = function (key) {
319
+ if (this[kReplyTrailers] === null) return this
320
+ this[kReplyTrailers][key.toLowerCase()] = undefined
321
+ return this
322
+ }
323
+
324
+ Reply.prototype.code = function (code) {
325
+ const statusCode = +code
326
+ if (!(statusCode >= 100 && statusCode <= 599)) {
327
+ throw new FST_ERR_BAD_STATUS_CODE(code || String(code))
328
+ }
329
+
330
+ this.raw.statusCode = statusCode
331
+ this[kReplyHasStatusCode] = true
332
+ return this
333
+ }
334
+
335
+ Reply.prototype.status = Reply.prototype.code
336
+
337
+ Reply.prototype.getSerializationFunction = function (schemaOrStatus, contentType) {
338
+ let serialize
339
+
340
+ if (typeof schemaOrStatus === 'string' || typeof schemaOrStatus === 'number') {
341
+ if (typeof contentType === 'string') {
342
+ serialize = this[kRouteContext][kSchemaResponse]?.[schemaOrStatus]?.[contentType]
343
+ } else {
344
+ serialize = this[kRouteContext][kSchemaResponse]?.[schemaOrStatus]
345
+ }
346
+ } else if (typeof schemaOrStatus === 'object') {
347
+ serialize = this[kRouteContext][kReplyCacheSerializeFns]?.get(schemaOrStatus)
348
+ }
349
+
350
+ return serialize
351
+ }
352
+
353
+ Reply.prototype.compileSerializationSchema = function (schema, httpStatus = null, contentType = null) {
354
+ const { request } = this
355
+ const { method, url } = request
356
+
357
+ // Check if serialize function already compiled
358
+ if (this[kRouteContext][kReplyCacheSerializeFns]?.has(schema)) {
359
+ return this[kRouteContext][kReplyCacheSerializeFns].get(schema)
360
+ }
361
+
362
+ const serializerCompiler = this[kRouteContext].serializerCompiler ||
363
+ this.server[kSchemaController].serializerCompiler ||
364
+ (
365
+ // We compile the schemas if no custom serializerCompiler is provided
366
+ // nor set
367
+ this.server[kSchemaController].setupSerializer(this.server[kOptions]) ||
368
+ this.server[kSchemaController].serializerCompiler
369
+ )
370
+
371
+ const serializeFn = serializerCompiler({
372
+ schema,
373
+ method,
374
+ url,
375
+ httpStatus,
376
+ contentType
377
+ })
378
+
379
+ // We create a WeakMap to compile the schema only once
380
+ // Its done lazily to avoid add overhead by creating the WeakMap
381
+ // if it is not used
382
+ // TODO: Explore a central cache for all the schemas shared across
383
+ // encapsulated contexts
384
+ if (this[kRouteContext][kReplyCacheSerializeFns] == null) {
385
+ this[kRouteContext][kReplyCacheSerializeFns] = new WeakMap()
386
+ }
387
+
388
+ this[kRouteContext][kReplyCacheSerializeFns].set(schema, serializeFn)
389
+
390
+ return serializeFn
391
+ }
392
+
393
+ Reply.prototype.serializeInput = function (input, schema, httpStatus, contentType) {
394
+ const possibleContentType = httpStatus
395
+ let serialize
396
+ httpStatus = typeof schema === 'string' || typeof schema === 'number'
397
+ ? schema
398
+ : httpStatus
399
+
400
+ contentType = httpStatus && possibleContentType !== httpStatus
401
+ ? possibleContentType
402
+ : contentType
403
+
404
+ if (httpStatus != null) {
405
+ if (contentType != null) {
406
+ serialize = this[kRouteContext][kSchemaResponse]?.[httpStatus]?.[contentType]
407
+ } else {
408
+ serialize = this[kRouteContext][kSchemaResponse]?.[httpStatus]
409
+ }
410
+
411
+ if (serialize == null) {
412
+ if (contentType) throw new FST_ERR_MISSING_CONTENTTYPE_SERIALIZATION_FN(httpStatus, contentType)
413
+ throw new FST_ERR_MISSING_SERIALIZATION_FN(httpStatus)
414
+ }
415
+ } else {
416
+ // Check if serialize function already compiled
417
+ if (this[kRouteContext][kReplyCacheSerializeFns]?.has(schema)) {
418
+ serialize = this[kRouteContext][kReplyCacheSerializeFns].get(schema)
419
+ } else {
420
+ serialize = this.compileSerializationSchema(schema, httpStatus, contentType)
421
+ }
422
+ }
423
+
424
+ return serialize(input)
425
+ }
426
+
427
+ Reply.prototype.serialize = function (payload) {
428
+ if (this[kReplySerializer] !== null) {
429
+ return this[kReplySerializer](payload)
430
+ } else {
431
+ if (this[kRouteContext] && this[kRouteContext][kReplySerializerDefault]) {
432
+ return this[kRouteContext][kReplySerializerDefault](payload, this.raw.statusCode)
433
+ } else {
434
+ return serialize(this[kRouteContext], payload, this.raw.statusCode)
435
+ }
436
+ }
437
+ }
438
+
439
+ Reply.prototype.serializer = function (fn) {
440
+ this[kReplySerializer] = fn
441
+ return this
442
+ }
443
+
444
+ Reply.prototype.type = function (type) {
445
+ this[kReplyHeaders]['content-type'] = type
446
+ return this
447
+ }
448
+
449
+ Reply.prototype.redirect = function (url, code) {
450
+ if (!code) {
451
+ code = this[kReplyHasStatusCode] ? this.raw.statusCode : 302
452
+ }
453
+
454
+ return this.header('location', url).code(code).send()
455
+ }
456
+
457
+ Reply.prototype.callNotFound = function () {
458
+ notFound(this)
459
+ return this
460
+ }
461
+
462
+ // Make reply a thenable, so it could be used with async/await.
463
+ // See
464
+ // - https://github.com/fastify/fastify/issues/1864 for the discussions
465
+ // - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then for the signature
466
+ Reply.prototype.then = function (fulfilled, rejected) {
467
+ if (this.sent) {
468
+ fulfilled()
469
+ return
470
+ }
471
+
472
+ eos(this.raw, (err) => {
473
+ // We must not treat ERR_STREAM_PREMATURE_CLOSE as
474
+ // an error because it is created by eos, not by the stream.
475
+ if (err && err.code !== 'ERR_STREAM_PREMATURE_CLOSE') {
476
+ if (rejected) {
477
+ rejected(err)
478
+ } else {
479
+ this.log && this.log.warn('unhandled rejection on reply.then')
480
+ }
481
+ } else {
482
+ fulfilled()
483
+ }
484
+ })
485
+ }
486
+
487
+ Reply.prototype.getDecorator = function (name) {
488
+ if (!decorators.hasKey(this, name) && !decorators.exist(this, name)) {
489
+ throw new FST_ERR_DEC_UNDECLARED(name, 'reply')
490
+ }
491
+
492
+ const decorator = this[name]
493
+ if (typeof decorator === 'function') {
494
+ return decorator.bind(this)
495
+ }
496
+
497
+ return decorator
498
+ }
499
+
500
+ function preSerializationHook (reply, payload) {
501
+ if (reply[kRouteContext].preSerialization !== null) {
502
+ preSerializationHookRunner(
503
+ reply[kRouteContext].preSerialization,
504
+ reply.request,
505
+ reply,
506
+ payload,
507
+ preSerializationHookEnd
508
+ )
509
+ } else {
510
+ preSerializationHookEnd(null, undefined, reply, payload)
511
+ }
512
+ }
513
+
514
+ function preSerializationHookEnd (err, _request, reply, payload) {
515
+ if (err != null) {
516
+ onErrorHook(reply, err)
517
+ return
518
+ }
519
+
520
+ try {
521
+ if (reply[kReplySerializer] !== null) {
522
+ payload = reply[kReplySerializer](payload)
523
+ } else if (reply[kRouteContext] && reply[kRouteContext][kReplySerializerDefault]) {
524
+ payload = reply[kRouteContext][kReplySerializerDefault](payload, reply.raw.statusCode)
525
+ } else {
526
+ payload = serialize(reply[kRouteContext], payload, reply.raw.statusCode, reply[kReplyHeaders]['content-type'])
527
+ }
528
+ } catch (e) {
529
+ wrapSerializationError(e, reply)
530
+ onErrorHook(reply, e)
531
+ return
532
+ }
533
+
534
+ onSendHook(reply, payload)
535
+ }
536
+
537
+ function wrapSerializationError (error, reply) {
538
+ error.serialization = reply[kRouteContext].config
539
+ }
540
+
541
+ function onSendHook (reply, payload) {
542
+ if (reply[kRouteContext].onSend !== null) {
543
+ onSendHookRunner(
544
+ reply[kRouteContext].onSend,
545
+ reply.request,
546
+ reply,
547
+ payload,
548
+ wrapOnSendEnd
549
+ )
550
+ } else {
551
+ onSendEnd(reply, payload)
552
+ }
553
+ }
554
+
555
+ function wrapOnSendEnd (err, request, reply, payload) {
556
+ if (err != null) {
557
+ onErrorHook(reply, err)
558
+ } else {
559
+ onSendEnd(reply, payload)
560
+ }
561
+ }
562
+
563
+ function safeWriteHead (reply, statusCode) {
564
+ const res = reply.raw
565
+ try {
566
+ res.writeHead(statusCode, reply[kReplyHeaders])
567
+ } catch (err) {
568
+ if (err.code === 'ERR_HTTP_HEADERS_SENT') {
569
+ reply.log.warn(`Reply was already sent, did you forget to "return reply" in the "${reply.request.raw.url}" (${reply.request.raw.method}) route?`)
570
+ }
571
+ throw err
572
+ }
573
+ }
574
+
575
+ function onSendEnd (reply, payload) {
576
+ const res = reply.raw
577
+ const req = reply.request
578
+
579
+ // we check if we need to update the trailers header and set it
580
+ if (reply[kReplyTrailers] !== null) {
581
+ const trailerHeaders = Object.keys(reply[kReplyTrailers])
582
+ let header = ''
583
+ for (const trailerName of trailerHeaders) {
584
+ if (typeof reply[kReplyTrailers][trailerName] !== 'function') continue
585
+ header += ' '
586
+ header += trailerName
587
+ }
588
+ // it must be chunked for trailer to work
589
+ reply.header('Transfer-Encoding', 'chunked')
590
+ reply.header('Trailer', header.trim())
591
+ }
592
+
593
+ // since Response contain status code, headers and body,
594
+ // we need to update the status, add the headers and use it's body as payload
595
+ // before continuing
596
+ if (toString.call(payload) === '[object Response]') {
597
+ // https://developer.mozilla.org/en-US/docs/Web/API/Response/status
598
+ if (typeof payload.status === 'number') {
599
+ reply.code(payload.status)
600
+ }
601
+
602
+ // https://developer.mozilla.org/en-US/docs/Web/API/Response/headers
603
+ if (typeof payload.headers === 'object' && typeof payload.headers.forEach === 'function') {
604
+ for (const [headerName, headerValue] of payload.headers) {
605
+ reply.header(headerName, headerValue)
606
+ }
607
+ }
608
+
609
+ // https://developer.mozilla.org/en-US/docs/Web/API/Response/body
610
+ if (payload.body !== null) {
611
+ if (payload.bodyUsed) {
612
+ throw new FST_ERR_REP_RESPONSE_BODY_CONSUMED()
613
+ }
614
+ }
615
+ // Keep going, body is either null or ReadableStream
616
+ payload = payload.body
617
+ }
618
+ const statusCode = res.statusCode
619
+
620
+ if (payload === undefined || payload === null) {
621
+ // according to https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.2
622
+ // we cannot send a content-length for 304 and 204, and all status code
623
+ // < 200
624
+ // A sender MUST NOT send a Content-Length header field in any message
625
+ // that contains a Transfer-Encoding header field.
626
+ // For HEAD we don't overwrite the `content-length`
627
+ if (statusCode >= 200 && statusCode !== 204 && statusCode !== 304 && req.method !== 'HEAD' && reply[kReplyTrailers] === null) {
628
+ reply[kReplyHeaders]['content-length'] = '0'
629
+ }
630
+
631
+ safeWriteHead(reply, statusCode)
632
+ sendTrailer(payload, res, reply)
633
+ return
634
+ }
635
+
636
+ if ((statusCode >= 100 && statusCode < 200) || statusCode === 204) {
637
+ // Responses without a content body must not send content-type
638
+ // or content-length headers.
639
+ // See https://www.rfc-editor.org/rfc/rfc9110.html#section-8.6.
640
+ reply.removeHeader('content-type')
641
+ reply.removeHeader('content-length')
642
+ safeWriteHead(reply, statusCode)
643
+ sendTrailer(undefined, res, reply)
644
+ if (typeof payload.resume === 'function') {
645
+ payload.on('error', noop)
646
+ payload.resume()
647
+ }
648
+ return
649
+ }
650
+
651
+ // node:stream
652
+ if (typeof payload.pipe === 'function') {
653
+ sendStream(payload, res, reply)
654
+ return
655
+ }
656
+
657
+ // node:stream/web
658
+ if (typeof payload.getReader === 'function') {
659
+ sendWebStream(payload, res, reply)
660
+ return
661
+ }
662
+
663
+ if (typeof payload !== 'string' && !Buffer.isBuffer(payload)) {
664
+ throw new FST_ERR_REP_INVALID_PAYLOAD_TYPE(typeof payload)
665
+ }
666
+
667
+ if (reply[kReplyTrailers] === null) {
668
+ const contentLength = reply[kReplyHeaders]['content-length']
669
+ if (!contentLength ||
670
+ (req.raw.method !== 'HEAD' &&
671
+ Number(contentLength) !== Buffer.byteLength(payload)
672
+ )
673
+ ) {
674
+ reply[kReplyHeaders]['content-length'] = '' + Buffer.byteLength(payload)
675
+ }
676
+ }
677
+
678
+ safeWriteHead(reply, statusCode)
679
+ // write payload first
680
+ res.write(payload)
681
+ // then send trailers
682
+ sendTrailer(payload, res, reply)
683
+ }
684
+
685
+ function logStreamError (logger, err, res) {
686
+ if (err.code === 'ERR_STREAM_PREMATURE_CLOSE') {
687
+ if (!logger[kDisableRequestLogging]) {
688
+ logger.info({ res }, 'stream closed prematurely')
689
+ }
690
+ } else {
691
+ logger.warn({ err }, 'response terminated with an error with headers already sent')
692
+ }
693
+ }
694
+
695
+ function sendWebStream (payload, res, reply) {
696
+ if (payload.locked) {
697
+ throw new FST_ERR_REP_READABLE_STREAM_LOCKED()
698
+ }
699
+
700
+ let sourceOpen = true
701
+ let errorLogged = false
702
+ let waitingDrain = false
703
+ const reader = payload.getReader()
704
+
705
+ eos(res, function (err) {
706
+ if (sourceOpen) {
707
+ if (err != null && res.headersSent && !errorLogged) {
708
+ errorLogged = true
709
+ logStreamError(reply.log, err, res)
710
+ }
711
+ reader.cancel().catch(noop)
712
+ }
713
+ })
714
+
715
+ if (!res.headersSent) {
716
+ for (const key in reply[kReplyHeaders]) {
717
+ res.setHeader(key, reply[kReplyHeaders][key])
718
+ }
719
+ } else {
720
+ reply.log.warn('response will send, but you shouldn\'t use res.writeHead in stream mode')
721
+ }
722
+
723
+ function onRead (result) {
724
+ if (result.done) {
725
+ sourceOpen = false
726
+ sendTrailer(null, res, reply)
727
+ return
728
+ }
729
+ /* c8 ignore next 5 - race condition: eos handler typically fires first */
730
+ if (res.destroyed) {
731
+ sourceOpen = false
732
+ reader.cancel().catch(noop)
733
+ return
734
+ }
735
+ const shouldContinue = res.write(result.value)
736
+ if (shouldContinue === false) {
737
+ waitingDrain = true
738
+ res.once('drain', onDrain)
739
+ return
740
+ }
741
+ reader.read().then(onRead, onReadError)
742
+ }
743
+
744
+ function onDrain () {
745
+ if (!waitingDrain || !sourceOpen || res.destroyed) {
746
+ return
747
+ }
748
+ waitingDrain = false
749
+ reader.read().then(onRead, onReadError)
750
+ }
751
+
752
+ function onReadError (err) {
753
+ sourceOpen = false
754
+ if (res.headersSent || reply.request.raw.aborted === true) {
755
+ if (!errorLogged) {
756
+ errorLogged = true
757
+ logStreamError(reply.log, err, reply)
758
+ }
759
+ res.destroy()
760
+ } else {
761
+ onErrorHook(reply, err)
762
+ }
763
+ }
764
+
765
+ reader.read().then(onRead, onReadError)
766
+ }
767
+
768
+ function sendStream (payload, res, reply) {
769
+ let sourceOpen = true
770
+ let errorLogged = false
771
+
772
+ // set trailer when stream ended
773
+ sendStreamTrailer(payload, res, reply)
774
+
775
+ eos(payload, { readable: true, writable: false }, function (err) {
776
+ sourceOpen = false
777
+ if (err != null) {
778
+ if (res.headersSent || reply.request.raw.aborted === true) {
779
+ if (!errorLogged) {
780
+ errorLogged = true
781
+ logStreamError(reply.log, err, reply)
782
+ }
783
+ res.destroy()
784
+ } else {
785
+ onErrorHook(reply, err)
786
+ }
787
+ }
788
+ // there is nothing to do if there is not an error
789
+ })
790
+
791
+ eos(res, function (err) {
792
+ if (sourceOpen) {
793
+ if (err != null && res.headersSent && !errorLogged) {
794
+ errorLogged = true
795
+ logStreamError(reply.log, err, res)
796
+ }
797
+ if (typeof payload.destroy === 'function') {
798
+ payload.destroy()
799
+ } else if (typeof payload.close === 'function') {
800
+ payload.close(noop)
801
+ } else if (typeof payload.abort === 'function') {
802
+ payload.abort()
803
+ } else {
804
+ reply.log.warn('stream payload does not end properly')
805
+ }
806
+ }
807
+ })
808
+
809
+ // streams will error asynchronously, and we want to handle that error
810
+ // appropriately, e.g. a 404 for a missing file. So we cannot use
811
+ // writeHead, and we need to resort to setHeader, which will trigger
812
+ // a writeHead when there is data to send.
813
+ if (!res.headersSent) {
814
+ for (const key in reply[kReplyHeaders]) {
815
+ res.setHeader(key, reply[kReplyHeaders][key])
816
+ }
817
+ } else {
818
+ reply.log.warn('response will send, but you shouldn\'t use res.writeHead in stream mode')
819
+ }
820
+ payload.pipe(res)
821
+ }
822
+
823
+ function sendTrailer (payload, res, reply) {
824
+ if (reply[kReplyTrailers] === null) {
825
+ // when no trailer, we close the stream
826
+ res.end(null, null, null) // avoid ArgumentsAdaptorTrampoline from V8
827
+ return
828
+ }
829
+ const trailerHeaders = Object.keys(reply[kReplyTrailers])
830
+ const trailers = {}
831
+ let handled = 0
832
+ let skipped = true
833
+ function send () {
834
+ // add trailers when all handler handled
835
+ /* istanbul ignore else */
836
+ if (handled === 0) {
837
+ res.addTrailers(trailers)
838
+ // we need to properly close the stream
839
+ // after trailers sent
840
+ res.end(null, null, null) // avoid ArgumentsAdaptorTrampoline from V8
841
+ }
842
+ }
843
+
844
+ for (const trailerName of trailerHeaders) {
845
+ if (typeof reply[kReplyTrailers][trailerName] !== 'function') continue
846
+ skipped = false
847
+ handled--
848
+
849
+ function cb (err, value) {
850
+ // TODO: we may protect multiple callback calls
851
+ // or mixing async-await with callback
852
+ handled++
853
+
854
+ // we can safely ignore error for trailer
855
+ // since it does affect the client
856
+ // we log in here only for debug usage
857
+ if (err) reply.log.debug(err)
858
+ else trailers[trailerName] = value
859
+
860
+ // we push the check to the end of event
861
+ // loop, so the registration continue to
862
+ // process.
863
+ process.nextTick(send)
864
+ }
865
+
866
+ const result = reply[kReplyTrailers][trailerName](reply, payload, cb)
867
+ if (typeof result === 'object' && typeof result.then === 'function') {
868
+ result.then((v) => cb(null, v), cb)
869
+ }
870
+ }
871
+
872
+ // when all trailers are skipped
873
+ // we need to close the stream
874
+ if (skipped) res.end(null, null, null) // avoid ArgumentsAdaptorTrampoline from V8
875
+ }
876
+
877
+ function sendStreamTrailer (payload, res, reply) {
878
+ if (reply[kReplyTrailers] === null) return
879
+ payload.on('end', () => sendTrailer(null, res, reply))
880
+ }
881
+
882
+ function onErrorHook (reply, error, cb) {
883
+ if (reply[kRouteContext].onError !== null && !reply[kReplyNextErrorHandler]) {
884
+ reply[kReplyIsRunningOnErrorHook] = true
885
+ onSendHookRunner(
886
+ reply[kRouteContext].onError,
887
+ reply.request,
888
+ reply,
889
+ error,
890
+ () => handleError(reply, error, cb)
891
+ )
892
+ } else {
893
+ handleError(reply, error, cb)
894
+ }
895
+ }
896
+
897
+ function setupResponseListeners (reply) {
898
+ reply[kReplyStartTime] = now()
899
+
900
+ const onResFinished = err => {
901
+ reply[kReplyEndTime] = now()
902
+ reply.raw.removeListener('finish', onResFinished)
903
+ reply.raw.removeListener('error', onResFinished)
904
+
905
+ const ctx = reply[kRouteContext]
906
+
907
+ // Clean up handler timeout / signal resources
908
+ if (reply.request[kRequestSignal]) {
909
+ clearTimeout(reply.request[kTimeoutTimer])
910
+ reply.request[kTimeoutTimer] = null
911
+ if (reply.request[kOnAbort]) {
912
+ reply.request.raw.removeListener('close', reply.request[kOnAbort])
913
+ reply.request[kOnAbort] = null
914
+ }
915
+ }
916
+
917
+ if (ctx && ctx.onResponse !== null) {
918
+ onResponseHookRunner(
919
+ ctx.onResponse,
920
+ reply.request,
921
+ reply,
922
+ onResponseCallback
923
+ )
924
+ } else {
925
+ onResponseCallback(err, reply.request, reply)
926
+ }
927
+ }
928
+
929
+ reply.raw.on('finish', onResFinished)
930
+ reply.raw.on('error', onResFinished)
931
+ }
932
+
933
+ function onResponseCallback (err, request, reply) {
934
+ if (reply.log[kDisableRequestLogging]) {
935
+ return
936
+ }
937
+
938
+ const responseTime = reply.elapsedTime
939
+
940
+ if (err != null) {
941
+ reply.log.error({
942
+ res: reply,
943
+ err,
944
+ responseTime
945
+ }, 'request errored')
946
+ return
947
+ }
948
+
949
+ reply.log.info({
950
+ res: reply,
951
+ responseTime
952
+ }, 'request completed')
953
+ }
954
+
955
+ function buildReply (R) {
956
+ const props = R.props.slice()
957
+
958
+ function _Reply (res, request, log) {
959
+ this.raw = res
960
+ this[kReplyIsError] = false
961
+ this[kReplyErrorHandlerCalled] = false
962
+ this[kReplyHijacked] = false
963
+ this[kReplySerializer] = null
964
+ this.request = request
965
+ this[kReplyHeaders] = {}
966
+ this[kReplyTrailers] = null
967
+ this[kReplyStartTime] = undefined
968
+ this[kReplyEndTime] = undefined
969
+ this.log = log
970
+
971
+ let prop
972
+
973
+ for (let i = 0; i < props.length; i++) {
974
+ prop = props[i]
975
+ this[prop.key] = prop.value
976
+ }
977
+ }
978
+ Object.setPrototypeOf(_Reply.prototype, R.prototype)
979
+ Object.setPrototypeOf(_Reply, R)
980
+ _Reply.parent = R
981
+ _Reply.props = props
982
+ return _Reply
983
+ }
984
+
985
+ function notFound (reply) {
986
+ if (reply[kRouteContext][kFourOhFourContext] === null) {
987
+ reply.log.warn('Trying to send a NotFound error inside a 404 handler. Sending basic 404 response.')
988
+ reply.code(404).send('404 Not Found')
989
+ return
990
+ }
991
+
992
+ reply.request[kRouteContext] = reply[kRouteContext][kFourOhFourContext]
993
+
994
+ // preHandler hook
995
+ if (reply[kRouteContext].preHandler !== null) {
996
+ preHandlerHookRunner(
997
+ reply[kRouteContext].preHandler,
998
+ reply.request,
999
+ reply,
1000
+ internals.preHandlerCallback
1001
+ )
1002
+ } else {
1003
+ internals.preHandlerCallback(null, reply.request, reply)
1004
+ }
1005
+ }
1006
+
1007
+ /**
1008
+ * This function runs when a payload that is not a string|buffer|stream or null
1009
+ * should be serialized to be streamed to the response.
1010
+ * This is the default serializer that can be customized by the user using the replySerializer
1011
+ *
1012
+ * @param {object} context the request context
1013
+ * @param {object} data the JSON payload to serialize
1014
+ * @param {number} statusCode the http status code
1015
+ * @param {string} [contentType] the reply content type
1016
+ * @returns {string} the serialized payload
1017
+ */
1018
+ function serialize (context, data, statusCode, contentType) {
1019
+ const fnSerialize = getSchemaSerializer(context, statusCode, contentType)
1020
+ if (fnSerialize) {
1021
+ return fnSerialize(data)
1022
+ }
1023
+ return JSON.stringify(data)
1024
+ }
1025
+
1026
+ function noop () { }
1027
+
1028
+ module.exports = Reply
1029
+ module.exports.buildReply = buildReply
1030
+ module.exports.setupResponseListeners = setupResponseListeners