UNPKG

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