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