1 | 'use strict'
|
2 |
|
3 | const eos = require('node:stream').finished
|
4 | const Readable = require('node:stream').Readable
|
5 |
|
6 | const {
|
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')
|
28 | const {
|
29 | onSendHookRunner,
|
30 | onResponseHookRunner,
|
31 | preHandlerHookRunner,
|
32 | preSerializationHookRunner
|
33 | } = require('./hooks')
|
34 |
|
35 | const internals = require('./handleRequest')[Symbol.for('internals')]
|
36 | const loggerUtils = require('./logger')
|
37 | const now = loggerUtils.now
|
38 | const { handleError } = require('./error-handler')
|
39 | const { getSchemaSerializer } = require('./schemas')
|
40 |
|
41 | const CONTENT_TYPE = {
|
42 | JSON: 'application/json; charset=utf-8',
|
43 | PLAIN: 'text/plain; charset=utf-8',
|
44 | OCTET: 'application/octet-stream'
|
45 | }
|
46 | const {
|
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')
|
58 | const { FSTDEP010, FSTDEP013, FSTDEP019, FSTDEP020, FSTDEP021 } = require('./warnings')
|
59 |
|
60 | const toString = Object.prototype.toString
|
61 |
|
62 | function 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 | }
|
75 | Reply.props = []
|
76 |
|
77 | Object.defineProperties(Reply.prototype, {
|
78 | [kRouteContext]: {
|
79 | get () {
|
80 | return this.request[kRouteContext]
|
81 | }
|
82 | },
|
83 |
|
84 |
|
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 |
|
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 |
|
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 |
|
140 | Reply.prototype.hijack = function () {
|
141 | this[kReplyHijacked] = true
|
142 | return this
|
143 | }
|
144 |
|
145 | Reply.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 |
|
172 | typeof payload.pipe === 'function' ||
|
173 |
|
174 | typeof payload.getReader === 'function' ||
|
175 |
|
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 |
|
207 | } else if (hasContentType === false || contentType.indexOf('json') > -1) {
|
208 | if (hasContentType === false) {
|
209 | this[kReplyHeaders]['content-type'] = CONTENT_TYPE.JSON
|
210 | } else {
|
211 |
|
212 | if (contentType.indexOf('charset') === -1) {
|
213 | const customContentType = contentType.trim()
|
214 | if (customContentType.endsWith(';')) {
|
215 |
|
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 |
|
233 | Reply.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 |
|
243 | Reply.prototype.getHeaders = function () {
|
244 | return {
|
245 | ...this.raw.getHeaders(),
|
246 | ...this[kReplyHeaders]
|
247 | }
|
248 | }
|
249 |
|
250 | Reply.prototype.hasHeader = function (key) {
|
251 | key = key.toLowerCase()
|
252 |
|
253 | return this[kReplyHeaders][key] !== undefined || this.raw.hasHeader(key)
|
254 | }
|
255 |
|
256 | Reply.prototype.removeHeader = function (key) {
|
257 |
|
258 |
|
259 | delete this[kReplyHeaders][key.toLowerCase()]
|
260 | return this
|
261 | }
|
262 |
|
263 | Reply.prototype.header = function (key, value = '') {
|
264 | key = key.toLowerCase()
|
265 |
|
266 | if (this[kReplyHeaders][key] && key === 'set-cookie') {
|
267 |
|
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 |
|
284 | Reply.prototype.headers = function (headers) {
|
285 | const keys = Object.keys(headers)
|
286 |
|
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 |
|
296 |
|
297 | const 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 |
|
312 | Reply.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 |
|
325 | Reply.prototype.hasTrailer = function (key) {
|
326 | return this[kReplyTrailers]?.[key.toLowerCase()] !== undefined
|
327 | }
|
328 |
|
329 | Reply.prototype.removeTrailer = function (key) {
|
330 | if (this[kReplyTrailers] === null) return this
|
331 | this[kReplyTrailers][key.toLowerCase()] = undefined
|
332 | return this
|
333 | }
|
334 |
|
335 | Reply.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 |
|
346 | Reply.prototype.status = Reply.prototype.code
|
347 |
|
348 | Reply.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 |
|
364 | Reply.prototype.compileSerializationSchema = function (schema, httpStatus = null, contentType = null) {
|
365 | const { request } = this
|
366 | const { method, url } = request
|
367 |
|
368 |
|
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 |
|
377 |
|
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 |
|
391 |
|
392 |
|
393 |
|
394 |
|
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 |
|
404 | Reply.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 |
|
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 |
|
438 | Reply.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 |
|
450 | Reply.prototype.serializer = function (fn) {
|
451 | this[kReplySerializer] = fn
|
452 | return this
|
453 | }
|
454 |
|
455 | Reply.prototype.type = function (type) {
|
456 | this[kReplyHeaders]['content-type'] = type
|
457 | return this
|
458 | }
|
459 |
|
460 | Reply.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 |
|
475 | Reply.prototype.callNotFound = function () {
|
476 | notFound(this)
|
477 | return this
|
478 | }
|
479 |
|
480 |
|
481 | Reply.prototype.getResponseTime = function () {
|
482 | FSTDEP020()
|
483 |
|
484 | return this.elapsedTime
|
485 | }
|
486 |
|
487 |
|
488 |
|
489 |
|
490 |
|
491 | Reply.prototype.then = function (fulfilled, rejected) {
|
492 | if (this.sent) {
|
493 | fulfilled()
|
494 | return
|
495 | }
|
496 |
|
497 | eos(this.raw, (err) => {
|
498 |
|
499 |
|
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 |
|
512 | function 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 |
|
526 | function 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 |
|
549 | function wrapSerializationError (error, reply) {
|
550 | error.serialization = reply[kRouteContext].config
|
551 | }
|
552 |
|
553 | function 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 |
|
567 | function wrapOnSendEnd (err, request, reply, payload) {
|
568 | if (err != null) {
|
569 | onErrorHook(reply, err)
|
570 | } else {
|
571 | onSendEnd(reply, payload)
|
572 | }
|
573 | }
|
574 |
|
575 | function 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 |
|
587 | function onSendEnd (reply, payload) {
|
588 | const res = reply.raw
|
589 | const req = reply.request
|
590 |
|
591 |
|
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 |
|
601 | reply.header('Transfer-Encoding', 'chunked')
|
602 | reply.header('Trailer', header.trim())
|
603 | }
|
604 |
|
605 |
|
606 |
|
607 | const isResponse = toString.call(payload) === '[object Response]'
|
608 | if (isResponse) {
|
609 |
|
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 |
|
618 |
|
619 |
|
620 |
|
621 |
|
622 |
|
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 |
|
634 |
|
635 |
|
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 |
|
648 | if (typeof payload.pipe === 'function') {
|
649 | sendStream(payload, res, reply)
|
650 | return
|
651 | }
|
652 |
|
653 |
|
654 | if (typeof payload.getReader === 'function') {
|
655 | sendWebStream(payload, res, reply)
|
656 | return
|
657 | }
|
658 |
|
659 |
|
660 | if (isResponse) {
|
661 |
|
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 |
|
669 | if (payload.body != null) {
|
670 | if (payload.bodyUsed) {
|
671 | throw new FST_ERR_REP_RESPONSE_BODY_CONSUMED()
|
672 | }
|
673 |
|
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 |
|
696 | res.write(payload)
|
697 |
|
698 | sendTrailer(payload, res, reply)
|
699 | }
|
700 |
|
701 | function 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 |
|
711 | function sendWebStream (payload, res, reply) {
|
712 | const nodeStream = Readable.fromWeb(payload)
|
713 | sendStream(nodeStream, res, reply)
|
714 | }
|
715 |
|
716 | function sendStream (payload, res, reply) {
|
717 | let sourceOpen = true
|
718 | let errorLogged = false
|
719 |
|
720 |
|
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 |
|
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 |
|
758 |
|
759 |
|
760 |
|
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 |
|
771 | function sendTrailer (payload, res, reply) {
|
772 | if (reply[kReplyTrailers] === null) {
|
773 |
|
774 | res.end(null, null, null)
|
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 |
|
783 |
|
784 | if (handled === 0) {
|
785 | res.addTrailers(trailers)
|
786 |
|
787 |
|
788 | res.end(null, null, null)
|
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 |
|
799 |
|
800 | handled++
|
801 |
|
802 |
|
803 |
|
804 |
|
805 | if (err) reply.log.debug(err)
|
806 | else trailers[trailerName] = value
|
807 |
|
808 |
|
809 |
|
810 |
|
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 |
|
819 | FSTDEP013()
|
820 | cb(null, result)
|
821 | }
|
822 | }
|
823 |
|
824 |
|
825 |
|
826 | if (skipped) res.end(null, null, null)
|
827 | }
|
828 |
|
829 | function sendStreamTrailer (payload, res, reply) {
|
830 | if (reply[kReplyTrailers] === null) return
|
831 | payload.on('end', () => sendTrailer(null, res, reply))
|
832 | }
|
833 |
|
834 | function 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 |
|
849 | function 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 |
|
875 | function 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 |
|
897 | function 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 |
|
914 | var prop
|
915 |
|
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 |
|
928 | function 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 |
|
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 |
|
952 |
|
953 |
|
954 |
|
955 |
|
956 |
|
957 |
|
958 |
|
959 |
|
960 |
|
961 | function 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 |
|
969 | function noop () { }
|
970 |
|
971 | module.exports = Reply
|
972 | module.exports.buildReply = buildReply
|
973 | module.exports.setupResponseListeners = setupResponseListeners
|