You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							865 lines
						
					
					
						
							23 KiB
						
					
					
				
			
		
		
	
	
							865 lines
						
					
					
						
							23 KiB
						
					
					
				| 'use strict'
 | |
| 
 | |
| const eos = require('stream').finished
 | |
| const statusCodes = require('http').STATUS_CODES
 | |
| const flatstr = require('flatstr')
 | |
| const FJS = require('fast-json-stringify')
 | |
| const {
 | |
|   kSchemaResponse,
 | |
|   kFourOhFourContext,
 | |
|   kReplyErrorHandlerCalled,
 | |
|   kReplySent,
 | |
|   kReplySentOverwritten,
 | |
|   kReplyStartTime,
 | |
|   kReplyEndTime,
 | |
|   kReplySerializer,
 | |
|   kReplySerializerDefault,
 | |
|   kReplyIsError,
 | |
|   kReplyHeaders,
 | |
|   kReplyTrailers,
 | |
|   kReplyHasStatusCode,
 | |
|   kReplyIsRunningOnErrorHook,
 | |
|   kDisableRequestLogging
 | |
| } = require('./symbols.js')
 | |
| const { hookRunner, hookIterator, onSendHookRunner } = require('./hooks')
 | |
| 
 | |
| const internals = require('./handleRequest')[Symbol.for('internals')]
 | |
| const loggerUtils = require('./logger')
 | |
| const now = loggerUtils.now
 | |
| const wrapThenable = require('./wrapThenable')
 | |
| 
 | |
| const serializeError = FJS({
 | |
|   type: 'object',
 | |
|   properties: {
 | |
|     statusCode: { type: 'number' },
 | |
|     code: { type: 'string' },
 | |
|     error: { type: 'string' },
 | |
|     message: { type: 'string' }
 | |
|   }
 | |
| })
 | |
| 
 | |
| const CONTENT_TYPE = {
 | |
|   JSON: 'application/json; charset=utf-8',
 | |
|   PLAIN: 'text/plain; charset=utf-8',
 | |
|   OCTET: 'application/octet-stream'
 | |
| }
 | |
| const {
 | |
|   FST_ERR_REP_INVALID_PAYLOAD_TYPE,
 | |
|   FST_ERR_REP_ALREADY_SENT,
 | |
|   FST_ERR_REP_SENT_VALUE,
 | |
|   FST_ERR_SEND_INSIDE_ONERR,
 | |
|   FST_ERR_BAD_STATUS_CODE,
 | |
|   FST_ERR_BAD_TRAILER_NAME,
 | |
|   FST_ERR_BAD_TRAILER_VALUE
 | |
| } = require('./errors')
 | |
| const warning = require('./warnings')
 | |
| 
 | |
| function Reply (res, request, log) {
 | |
|   this.raw = res
 | |
|   this[kReplySent] = false
 | |
|   this[kReplySerializer] = null
 | |
|   this[kReplyErrorHandlerCalled] = false
 | |
|   this[kReplyIsError] = false
 | |
|   this[kReplyIsRunningOnErrorHook] = false
 | |
|   this.request = request
 | |
|   this[kReplyHeaders] = {}
 | |
|   this[kReplyTrailers] = null
 | |
|   this[kReplyHasStatusCode] = false
 | |
|   this[kReplyStartTime] = undefined
 | |
|   this.log = log
 | |
| }
 | |
| Reply.props = []
 | |
| 
 | |
| Object.defineProperties(Reply.prototype, {
 | |
|   context: {
 | |
|     get () {
 | |
|       return this.request.context
 | |
|     }
 | |
|   },
 | |
|   res: {
 | |
|     get () {
 | |
|       warning.emit('FSTDEP002')
 | |
|       return this.raw
 | |
|     }
 | |
|   },
 | |
|   sent: {
 | |
|     enumerable: true,
 | |
|     get () {
 | |
|       return this[kReplySent]
 | |
|     },
 | |
|     set (value) {
 | |
|       if (value !== true) {
 | |
|         throw new FST_ERR_REP_SENT_VALUE()
 | |
|       }
 | |
| 
 | |
|       if (this[kReplySent]) {
 | |
|         throw new FST_ERR_REP_ALREADY_SENT()
 | |
|       }
 | |
| 
 | |
|       this[kReplySentOverwritten] = true
 | |
|       this[kReplySent] = true
 | |
|     }
 | |
|   },
 | |
|   statusCode: {
 | |
|     get () {
 | |
|       return this.raw.statusCode
 | |
|     },
 | |
|     set (value) {
 | |
|       this.code(value)
 | |
|     }
 | |
|   },
 | |
|   server: {
 | |
|     value: null,
 | |
|     writable: true
 | |
|   }
 | |
| })
 | |
| 
 | |
| Reply.prototype.hijack = function () {
 | |
|   this[kReplySent] = true
 | |
|   return this
 | |
| }
 | |
| 
 | |
| Reply.prototype.send = function (payload) {
 | |
|   if (this[kReplyIsRunningOnErrorHook] === true) {
 | |
|     throw new FST_ERR_SEND_INSIDE_ONERR()
 | |
|   }
 | |
| 
 | |
|   if (this[kReplySent]) {
 | |
|     this.log.warn({ err: new FST_ERR_REP_ALREADY_SENT() }, 'Reply already sent')
 | |
|     return this
 | |
|   }
 | |
| 
 | |
|   if (payload instanceof Error || this[kReplyIsError] === true) {
 | |
|     onErrorHook(this, payload, onSendHook)
 | |
|     return this
 | |
|   }
 | |
| 
 | |
|   if (payload === undefined) {
 | |
|     onSendHook(this, payload)
 | |
|     return this
 | |
|   }
 | |
| 
 | |
|   const contentType = this.getHeader('content-type')
 | |
|   const hasContentType = contentType !== undefined
 | |
| 
 | |
|   if (payload !== null) {
 | |
|     if (Buffer.isBuffer(payload) || typeof payload.pipe === 'function') {
 | |
|       if (hasContentType === false) {
 | |
|         this[kReplyHeaders]['content-type'] = CONTENT_TYPE.OCTET
 | |
|       }
 | |
|       onSendHook(this, payload)
 | |
|       return this
 | |
|     }
 | |
| 
 | |
|     if (hasContentType === false && typeof payload === 'string') {
 | |
|       this[kReplyHeaders]['content-type'] = CONTENT_TYPE.PLAIN
 | |
|       onSendHook(this, payload)
 | |
|       return this
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   if (this[kReplySerializer] !== null) {
 | |
|     if (typeof payload !== 'string') {
 | |
|       preserializeHook(this, payload)
 | |
|       return this
 | |
|     } else {
 | |
|       payload = this[kReplySerializer](payload)
 | |
|     }
 | |
| 
 | |
|   // The indexOf below also matches custom json mimetypes such as 'application/hal+json' or 'application/ld+json'
 | |
|   } else if (hasContentType === false || contentType.indexOf('json') > -1) {
 | |
|     if (hasContentType === false) {
 | |
|       this[kReplyHeaders]['content-type'] = CONTENT_TYPE.JSON
 | |
|     } else {
 | |
|       // If hasContentType === true, we have a JSON mimetype
 | |
|       if (contentType.indexOf('charset') === -1) {
 | |
|         // If we have simply application/json instead of a custom json mimetype
 | |
|         if (contentType.indexOf('/json') > -1) {
 | |
|           this[kReplyHeaders]['content-type'] = CONTENT_TYPE.JSON
 | |
|         } else {
 | |
|           const currContentType = this[kReplyHeaders]['content-type']
 | |
|           // We extract the custom mimetype part (e.g. 'hal+' from 'application/hal+json')
 | |
|           const customJsonType = currContentType.substring(
 | |
|             currContentType.indexOf('/'),
 | |
|             currContentType.indexOf('json') + 4
 | |
|           )
 | |
| 
 | |
|           // We ensure we set the header to the proper JSON content-type if necessary
 | |
|           // (e.g. 'application/hal+json' instead of 'application/json')
 | |
|           this[kReplyHeaders]['content-type'] = CONTENT_TYPE.JSON.replace('/json', customJsonType)
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|     if (typeof payload !== 'string') {
 | |
|       preserializeHook(this, payload)
 | |
|       return this
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   onSendHook(this, payload)
 | |
| 
 | |
|   return this
 | |
| }
 | |
| 
 | |
| Reply.prototype.getHeader = function (key) {
 | |
|   key = key.toLowerCase()
 | |
|   const res = this.raw
 | |
|   let value = this[kReplyHeaders][key]
 | |
|   if (value === undefined && res.hasHeader(key)) {
 | |
|     value = res.getHeader(key)
 | |
|   }
 | |
|   return value
 | |
| }
 | |
| 
 | |
| Reply.prototype.getHeaders = function () {
 | |
|   return {
 | |
|     ...this.raw.getHeaders(),
 | |
|     ...this[kReplyHeaders]
 | |
|   }
 | |
| }
 | |
| 
 | |
| Reply.prototype.hasHeader = function (key) {
 | |
|   key = key.toLowerCase()
 | |
|   if (this[kReplyHeaders][key] !== undefined) {
 | |
|     return true
 | |
|   }
 | |
|   return this.raw.hasHeader(key)
 | |
| }
 | |
| 
 | |
| Reply.prototype.removeHeader = function (key) {
 | |
|   // Node.js does not like headers with keys set to undefined,
 | |
|   // so we have to delete the key.
 | |
|   delete this[kReplyHeaders][key.toLowerCase()]
 | |
|   return this
 | |
| }
 | |
| 
 | |
| Reply.prototype.header = function (key, value) {
 | |
|   const _key = key.toLowerCase()
 | |
| 
 | |
|   // default the value to ''
 | |
|   value = value === undefined ? '' : value
 | |
| 
 | |
|   if (this[kReplyHeaders][_key] && _key === 'set-cookie') {
 | |
|     // https://tools.ietf.org/html/rfc7230#section-3.2.2
 | |
|     if (typeof this[kReplyHeaders][_key] === 'string') {
 | |
|       this[kReplyHeaders][_key] = [this[kReplyHeaders][_key]]
 | |
|     }
 | |
|     if (Array.isArray(value)) {
 | |
|       Array.prototype.push.apply(this[kReplyHeaders][_key], value)
 | |
|     } else {
 | |
|       this[kReplyHeaders][_key].push(value)
 | |
|     }
 | |
|   } else {
 | |
|     this[kReplyHeaders][_key] = value
 | |
|   }
 | |
|   return this
 | |
| }
 | |
| 
 | |
| Reply.prototype.headers = function (headers) {
 | |
|   const keys = Object.keys(headers)
 | |
|   /* eslint-disable no-var */
 | |
|   for (var i = 0; i !== keys.length; ++i) {
 | |
|     const key = keys[i]
 | |
|     this.header(key, headers[key])
 | |
|   }
 | |
|   return this
 | |
| }
 | |
| 
 | |
| // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Trailer#directives
 | |
| // https://httpwg.org/specs/rfc7230.html#chunked.trailer.part
 | |
| const INVALID_TRAILERS = new Set([
 | |
|   'transfer-encoding',
 | |
|   'content-length',
 | |
|   'host',
 | |
|   'cache-control',
 | |
|   'max-forwards',
 | |
|   'te',
 | |
|   'authorization',
 | |
|   'set-cookie',
 | |
|   'content-encoding',
 | |
|   'content-type',
 | |
|   'content-range',
 | |
|   'trailer'
 | |
| ])
 | |
| 
 | |
| Reply.prototype.trailer = function (key, fn) {
 | |
|   key = key.toLowerCase()
 | |
|   if (INVALID_TRAILERS.has(key)) {
 | |
|     throw new FST_ERR_BAD_TRAILER_NAME(key)
 | |
|   }
 | |
|   if (typeof fn !== 'function') {
 | |
|     throw new FST_ERR_BAD_TRAILER_VALUE(key, typeof fn)
 | |
|   }
 | |
|   if (this[kReplyTrailers] === null) this[kReplyTrailers] = {}
 | |
|   this[kReplyTrailers][key] = fn
 | |
|   return this
 | |
| }
 | |
| 
 | |
| Reply.prototype.hasTrailer = function (key) {
 | |
|   if (this[kReplyTrailers] === null) return false
 | |
|   return this[kReplyTrailers][key.toLowerCase()] !== undefined
 | |
| }
 | |
| 
 | |
| Reply.prototype.removeTrailer = function (key) {
 | |
|   if (this[kReplyTrailers] === null) return this
 | |
|   this[kReplyTrailers][key.toLowerCase()] = undefined
 | |
|   return this
 | |
| }
 | |
| 
 | |
| Reply.prototype.code = function (code) {
 | |
|   const intValue = parseInt(code)
 | |
|   if (isNaN(intValue) || intValue < 100 || intValue > 600) {
 | |
|     throw new FST_ERR_BAD_STATUS_CODE(code || String(code))
 | |
|   }
 | |
| 
 | |
|   this.raw.statusCode = intValue
 | |
|   this[kReplyHasStatusCode] = true
 | |
|   return this
 | |
| }
 | |
| 
 | |
| Reply.prototype.status = Reply.prototype.code
 | |
| 
 | |
| Reply.prototype.serialize = function (payload) {
 | |
|   if (this[kReplySerializer] !== null) {
 | |
|     return this[kReplySerializer](payload)
 | |
|   } else {
 | |
|     if (this.context && this.context[kReplySerializerDefault]) {
 | |
|       return this.context[kReplySerializerDefault](payload, this.raw.statusCode)
 | |
|     } else {
 | |
|       return serialize(this.context, payload, this.raw.statusCode)
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| Reply.prototype.serializer = function (fn) {
 | |
|   this[kReplySerializer] = fn
 | |
|   return this
 | |
| }
 | |
| 
 | |
| Reply.prototype.type = function (type) {
 | |
|   this[kReplyHeaders]['content-type'] = type
 | |
|   return this
 | |
| }
 | |
| 
 | |
| Reply.prototype.redirect = function (code, url) {
 | |
|   if (typeof code === 'string') {
 | |
|     url = code
 | |
|     code = this[kReplyHasStatusCode] ? this.raw.statusCode : 302
 | |
|   }
 | |
| 
 | |
|   this.header('location', url).code(code).send()
 | |
| }
 | |
| 
 | |
| Reply.prototype.callNotFound = function () {
 | |
|   notFound(this)
 | |
| }
 | |
| 
 | |
| Reply.prototype.getResponseTime = function () {
 | |
|   let responseTime = 0
 | |
| 
 | |
|   if (this[kReplyStartTime] !== undefined) {
 | |
|     responseTime = (this[kReplyEndTime] || now()) - this[kReplyStartTime]
 | |
|   }
 | |
| 
 | |
|   return responseTime
 | |
| }
 | |
| 
 | |
| // Make reply a thenable, so it could be used with async/await.
 | |
| // See
 | |
| // - https://github.com/fastify/fastify/issues/1864 for the discussions
 | |
| // - https://promisesaplus.com/ for the definition of thenable
 | |
| // - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then for the signature
 | |
| Reply.prototype.then = function (fulfilled, rejected) {
 | |
|   if (this.sent) {
 | |
|     fulfilled()
 | |
|     return
 | |
|   }
 | |
| 
 | |
|   eos(this.raw, (err) => {
 | |
|     // We must not treat ERR_STREAM_PREMATURE_CLOSE as
 | |
|     // an error because it is created by eos, not by the stream.
 | |
|     if (err && err.code !== 'ERR_STREAM_PREMATURE_CLOSE') {
 | |
|       if (rejected) {
 | |
|         rejected(err)
 | |
|       } else {
 | |
|         this.log && this.log.warn('unhandled rejection on reply.then')
 | |
|       }
 | |
|     } else {
 | |
|       fulfilled()
 | |
|     }
 | |
|   })
 | |
| }
 | |
| 
 | |
| function preserializeHook (reply, payload) {
 | |
|   if (reply.context.preSerialization !== null) {
 | |
|     onSendHookRunner(
 | |
|       reply.context.preSerialization,
 | |
|       reply.request,
 | |
|       reply,
 | |
|       payload,
 | |
|       preserializeHookEnd
 | |
|     )
 | |
|   } else {
 | |
|     preserializeHookEnd(null, reply.request, reply, payload)
 | |
|   }
 | |
| }
 | |
| 
 | |
| function preserializeHookEnd (err, request, reply, payload) {
 | |
|   if (err != null) {
 | |
|     onErrorHook(reply, err)
 | |
|     return
 | |
|   }
 | |
| 
 | |
|   try {
 | |
|     if (reply[kReplySerializer] !== null) {
 | |
|       payload = reply[kReplySerializer](payload)
 | |
|     } else if (reply.context && reply.context[kReplySerializerDefault]) {
 | |
|       payload = reply.context[kReplySerializerDefault](payload, reply.raw.statusCode)
 | |
|     } else {
 | |
|       payload = serialize(reply.context, payload, reply.raw.statusCode)
 | |
|     }
 | |
|   } catch (e) {
 | |
|     wrapSeralizationError(e, reply)
 | |
|     onErrorHook(reply, e)
 | |
|     return
 | |
|   }
 | |
| 
 | |
|   flatstr(payload)
 | |
| 
 | |
|   onSendHook(reply, payload)
 | |
| }
 | |
| 
 | |
| function wrapSeralizationError (error, reply) {
 | |
|   error.serialization = reply.context.config
 | |
| }
 | |
| 
 | |
| function onSendHook (reply, payload) {
 | |
|   if (reply.context.onSend !== null) {
 | |
|     reply[kReplySent] = true
 | |
|     onSendHookRunner(
 | |
|       reply.context.onSend,
 | |
|       reply.request,
 | |
|       reply,
 | |
|       payload,
 | |
|       wrapOnSendEnd
 | |
|     )
 | |
|   } else {
 | |
|     onSendEnd(reply, payload)
 | |
|   }
 | |
| }
 | |
| 
 | |
| function wrapOnSendEnd (err, request, reply, payload) {
 | |
|   if (err != null) {
 | |
|     onErrorHook(reply, err)
 | |
|   } else {
 | |
|     onSendEnd(reply, payload)
 | |
|   }
 | |
| }
 | |
| 
 | |
| function onSendEnd (reply, payload) {
 | |
|   const res = reply.raw
 | |
|   const req = reply.request
 | |
|   const statusCode = res.statusCode
 | |
| 
 | |
|   // we check if we need to update the trailers header and set it
 | |
|   if (reply[kReplyTrailers] !== null) {
 | |
|     const trailerHeaders = Object.keys(reply[kReplyTrailers])
 | |
|     let header = ''
 | |
|     for (const trailerName of trailerHeaders) {
 | |
|       if (typeof reply[kReplyTrailers][trailerName] !== 'function') continue
 | |
|       header += ' '
 | |
|       header += trailerName
 | |
|     }
 | |
|     // it must be chunked for trailer to work
 | |
|     reply.header('Transfer-Encoding', 'chunked')
 | |
|     reply.header('Trailer', header.trim())
 | |
|   }
 | |
| 
 | |
|   if (payload === undefined || payload === null) {
 | |
|     reply[kReplySent] = true
 | |
| 
 | |
|     // according to https://tools.ietf.org/html/rfc7230#section-3.3.2
 | |
|     // we cannot send a content-length for 304 and 204, and all status code
 | |
|     // < 200
 | |
|     // A sender MUST NOT send a Content-Length header field in any message
 | |
|     // that contains a Transfer-Encoding header field.
 | |
|     // For HEAD we don't overwrite the `content-length`
 | |
|     if (statusCode >= 200 && statusCode !== 204 && statusCode !== 304 && req.method !== 'HEAD' && reply[kReplyTrailers] === null) {
 | |
|       reply[kReplyHeaders]['content-length'] = '0'
 | |
|     }
 | |
| 
 | |
|     res.writeHead(statusCode, reply[kReplyHeaders])
 | |
|     sendTrailer(payload, res, reply)
 | |
|     // avoid ArgumentsAdaptorTrampoline from V8
 | |
|     res.end(null, null, null)
 | |
|     return
 | |
|   }
 | |
| 
 | |
|   if (typeof payload.pipe === 'function') {
 | |
|     reply[kReplySent] = true
 | |
| 
 | |
|     sendStream(payload, res, reply)
 | |
|     return
 | |
|   }
 | |
| 
 | |
|   if (typeof payload !== 'string' && !Buffer.isBuffer(payload)) {
 | |
|     throw new FST_ERR_REP_INVALID_PAYLOAD_TYPE(typeof payload)
 | |
|   }
 | |
| 
 | |
|   if (reply[kReplyTrailers] === null) {
 | |
|     if (!reply[kReplyHeaders]['content-length']) {
 | |
|       reply[kReplyHeaders]['content-length'] = '' + Buffer.byteLength(payload)
 | |
|     } else if (req.raw.method !== 'HEAD' && reply[kReplyHeaders]['content-length'] !== Buffer.byteLength(payload)) {
 | |
|       reply[kReplyHeaders]['content-length'] = '' + Buffer.byteLength(payload)
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   reply[kReplySent] = true
 | |
| 
 | |
|   res.writeHead(statusCode, reply[kReplyHeaders])
 | |
|   // write payload first
 | |
|   res.write(payload)
 | |
|   // then send trailers
 | |
|   sendTrailer(payload, res, reply)
 | |
|   // avoid ArgumentsAdaptorTrampoline from V8
 | |
|   res.end(null, null, null)
 | |
| }
 | |
| 
 | |
| function logStreamError (logger, err, res) {
 | |
|   if (err.code === 'ERR_STREAM_PREMATURE_CLOSE') {
 | |
|     if (!logger[kDisableRequestLogging]) {
 | |
|       logger.info({ res }, 'stream closed prematurely')
 | |
|     }
 | |
|   } else {
 | |
|     logger.warn({ err }, 'response terminated with an error with headers already sent')
 | |
|   }
 | |
| }
 | |
| 
 | |
| function sendStream (payload, res, reply) {
 | |
|   let sourceOpen = true
 | |
|   let errorLogged = false
 | |
| 
 | |
|   // set trailer when stream ended
 | |
|   sendStreamTrailer(payload, res, reply)
 | |
| 
 | |
|   eos(payload, { readable: true, writable: false }, function (err) {
 | |
|     sourceOpen = false
 | |
|     if (err != null) {
 | |
|       if (res.headersSent || reply.request.raw.aborted === true) {
 | |
|         if (!errorLogged) {
 | |
|           errorLogged = true
 | |
|           logStreamError(reply.log, err, res)
 | |
|         }
 | |
|         res.destroy()
 | |
|       } else {
 | |
|         onErrorHook(reply, err)
 | |
|       }
 | |
|     }
 | |
|     // there is nothing to do if there is not an error
 | |
|   })
 | |
| 
 | |
|   eos(res, function (err) {
 | |
|     if (sourceOpen) {
 | |
|       if (err != null && res.headersSent && !errorLogged) {
 | |
|         errorLogged = true
 | |
|         logStreamError(reply.log, err, res)
 | |
|       }
 | |
|       if (typeof payload.destroy === 'function') {
 | |
|         payload.destroy()
 | |
|       } else if (typeof payload.close === 'function') {
 | |
|         payload.close(noop)
 | |
|       } else if (typeof payload.abort === 'function') {
 | |
|         payload.abort()
 | |
|       } else {
 | |
|         reply.log.warn('stream payload does not end properly')
 | |
|       }
 | |
|     }
 | |
|   })
 | |
| 
 | |
|   // streams will error asynchronously, and we want to handle that error
 | |
|   // appropriately, e.g. a 404 for a missing file. So we cannot use
 | |
|   // writeHead, and we need to resort to setHeader, which will trigger
 | |
|   // a writeHead when there is data to send.
 | |
|   if (!res.headersSent) {
 | |
|     for (const key in reply[kReplyHeaders]) {
 | |
|       res.setHeader(key, reply[kReplyHeaders][key])
 | |
|     }
 | |
|   } else {
 | |
|     reply.log.warn('response will send, but you shouldn\'t use res.writeHead in stream mode')
 | |
|   }
 | |
|   payload.pipe(res)
 | |
| }
 | |
| 
 | |
| function sendTrailer (payload, res, reply) {
 | |
|   if (reply[kReplyTrailers] === null) return
 | |
|   const trailerHeaders = Object.keys(reply[kReplyTrailers])
 | |
|   const trailers = {}
 | |
|   for (const trailerName of trailerHeaders) {
 | |
|     if (typeof reply[kReplyTrailers][trailerName] !== 'function') continue
 | |
|     trailers[trailerName] = reply[kReplyTrailers][trailerName](reply, payload)
 | |
|   }
 | |
|   res.addTrailers(trailers)
 | |
| }
 | |
| 
 | |
| function sendStreamTrailer (payload, res, reply) {
 | |
|   if (reply[kReplyTrailers] === null) return
 | |
|   payload.on('end', () => sendTrailer(null, res, reply))
 | |
| }
 | |
| 
 | |
| function onErrorHook (reply, error, cb) {
 | |
|   reply[kReplySent] = true
 | |
|   if (reply.context.onError !== null && reply[kReplyErrorHandlerCalled] === true) {
 | |
|     reply[kReplyIsRunningOnErrorHook] = true
 | |
|     onSendHookRunner(
 | |
|       reply.context.onError,
 | |
|       reply.request,
 | |
|       reply,
 | |
|       error,
 | |
|       () => handleError(reply, error, cb)
 | |
|     )
 | |
|   } else {
 | |
|     handleError(reply, error, cb)
 | |
|   }
 | |
| }
 | |
| 
 | |
| function handleError (reply, error, cb) {
 | |
|   reply[kReplyIsRunningOnErrorHook] = false
 | |
|   const res = reply.raw
 | |
|   let statusCode = res.statusCode
 | |
|   statusCode = (statusCode >= 400) ? statusCode : 500
 | |
|   // treat undefined and null as same
 | |
|   if (error != null) {
 | |
|     if (error.headers !== undefined) {
 | |
|       reply.headers(error.headers)
 | |
|     }
 | |
|     if (error.status >= 400) {
 | |
|       statusCode = error.status
 | |
|     } else if (error.statusCode >= 400) {
 | |
|       statusCode = error.statusCode
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   res.statusCode = statusCode
 | |
| 
 | |
|   const errorHandler = reply.context.errorHandler
 | |
|   if (errorHandler && reply[kReplyErrorHandlerCalled] === false) {
 | |
|     reply[kReplySent] = false
 | |
|     reply[kReplyIsError] = false
 | |
|     reply[kReplyErrorHandlerCalled] = true
 | |
|     // remove header is needed in here, because when we pipe to a stream
 | |
|     // `undefined` value header will directly passed to node response
 | |
|     reply.removeHeader('content-length')
 | |
|     const result = errorHandler(error, reply.request, reply)
 | |
|     if (result !== undefined) {
 | |
|       if (result !== null && typeof result.then === 'function') {
 | |
|         wrapThenable(result, reply)
 | |
|       } else {
 | |
|         reply.send(result)
 | |
|       }
 | |
|     }
 | |
|     return
 | |
|   }
 | |
| 
 | |
|   let payload
 | |
|   try {
 | |
|     const serializerFn = getSchemaSerializer(reply.context, statusCode)
 | |
|     payload = (serializerFn === false)
 | |
|       ? serializeError({
 | |
|         error: statusCodes[statusCode + ''],
 | |
|         code: error.code,
 | |
|         message: error.message || '',
 | |
|         statusCode
 | |
|       })
 | |
|       : serializerFn(Object.create(error, {
 | |
|         error: { value: statusCodes[statusCode + ''] },
 | |
|         message: { value: error.message || '' },
 | |
|         statusCode: { value: statusCode }
 | |
|       }))
 | |
| 
 | |
|     if (serializerFn !== false && typeof payload !== 'string') {
 | |
|       throw new FST_ERR_REP_INVALID_PAYLOAD_TYPE(typeof payload)
 | |
|     }
 | |
|   } catch (err) {
 | |
|     // error is always FST_ERR_SCH_SERIALIZATION_BUILD because this is called from route/compileSchemasForSerialization
 | |
|     reply.log.error({ err, statusCode: res.statusCode }, 'The serializer for the given status code failed')
 | |
|     res.statusCode = 500
 | |
|     payload = serializeError({
 | |
|       error: statusCodes['500'],
 | |
|       code: err.code,
 | |
|       message: err.message,
 | |
|       statusCode: 500
 | |
|     })
 | |
|   }
 | |
| 
 | |
|   flatstr(payload)
 | |
|   reply[kReplyHeaders]['content-type'] = CONTENT_TYPE.JSON
 | |
|   reply[kReplyHeaders]['content-length'] = '' + Buffer.byteLength(payload)
 | |
| 
 | |
|   if (cb) {
 | |
|     cb(reply, payload)
 | |
|     return
 | |
|   }
 | |
| 
 | |
|   reply[kReplySent] = true
 | |
|   res.writeHead(res.statusCode, reply[kReplyHeaders])
 | |
|   res.end(payload)
 | |
| }
 | |
| 
 | |
| function setupResponseListeners (reply) {
 | |
|   reply[kReplyStartTime] = now()
 | |
| 
 | |
|   const onResFinished = err => {
 | |
|     reply[kReplyEndTime] = now()
 | |
|     reply.raw.removeListener('finish', onResFinished)
 | |
|     reply.raw.removeListener('error', onResFinished)
 | |
| 
 | |
|     const ctx = reply.context
 | |
| 
 | |
|     if (ctx && ctx.onResponse !== null) {
 | |
|       hookRunner(
 | |
|         ctx.onResponse,
 | |
|         onResponseIterator,
 | |
|         reply.request,
 | |
|         reply,
 | |
|         onResponseCallback
 | |
|       )
 | |
|     } else {
 | |
|       onResponseCallback(err, reply.request, reply)
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   reply.raw.on('finish', onResFinished)
 | |
|   reply.raw.on('error', onResFinished)
 | |
| }
 | |
| 
 | |
| function onResponseIterator (fn, request, reply, next) {
 | |
|   return fn(request, reply, next)
 | |
| }
 | |
| 
 | |
| function onResponseCallback (err, request, reply) {
 | |
|   if (reply.log[kDisableRequestLogging]) {
 | |
|     return
 | |
|   }
 | |
| 
 | |
|   const responseTime = reply.getResponseTime()
 | |
| 
 | |
|   if (err != null) {
 | |
|     reply.log.error({
 | |
|       res: reply,
 | |
|       err,
 | |
|       responseTime
 | |
|     }, 'request errored')
 | |
|     return
 | |
|   }
 | |
| 
 | |
|   reply.log.info({
 | |
|     res: reply,
 | |
|     responseTime
 | |
|   }, 'request completed')
 | |
| }
 | |
| 
 | |
| function buildReply (R) {
 | |
|   const props = [...R.props]
 | |
| 
 | |
|   function _Reply (res, request, log) {
 | |
|     this.raw = res
 | |
|     this[kReplyIsError] = false
 | |
|     this[kReplyErrorHandlerCalled] = false
 | |
|     this[kReplySent] = false
 | |
|     this[kReplySentOverwritten] = false
 | |
|     this[kReplySerializer] = null
 | |
|     this.request = request
 | |
|     this[kReplyHeaders] = {}
 | |
|     this[kReplyTrailers] = null
 | |
|     this[kReplyStartTime] = undefined
 | |
|     this[kReplyEndTime] = undefined
 | |
|     this.log = log
 | |
| 
 | |
|     // eslint-disable-next-line no-var
 | |
|     var prop
 | |
|     // eslint-disable-next-line no-var
 | |
|     for (var i = 0; i < props.length; i++) {
 | |
|       prop = props[i]
 | |
|       this[prop.key] = prop.value
 | |
|     }
 | |
|   }
 | |
|   _Reply.prototype = new R()
 | |
|   _Reply.props = props
 | |
|   return _Reply
 | |
| }
 | |
| 
 | |
| function notFound (reply) {
 | |
|   reply[kReplySent] = false
 | |
|   reply[kReplyIsError] = false
 | |
| 
 | |
|   if (reply.context[kFourOhFourContext] === null) {
 | |
|     reply.log.warn('Trying to send a NotFound error inside a 404 handler. Sending basic 404 response.')
 | |
|     reply.code(404).send('404 Not Found')
 | |
|     return
 | |
|   }
 | |
| 
 | |
|   reply.request.context = reply.context[kFourOhFourContext]
 | |
| 
 | |
|   // preHandler hook
 | |
|   if (reply.context.preHandler !== null) {
 | |
|     hookRunner(
 | |
|       reply.context.preHandler,
 | |
|       hookIterator,
 | |
|       reply.request,
 | |
|       reply,
 | |
|       internals.preHandlerCallback
 | |
|     )
 | |
|   } else {
 | |
|     internals.preHandlerCallback(null, reply.request, reply)
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * This function runs when a payload that is not a string|buffer|stream or null
 | |
|  * should be serialized to be streamed to the response.
 | |
|  * This is the default serializer that can be customized by the user using the replySerializer
 | |
|  *
 | |
|  * @param {object} context the request context
 | |
|  * @param {object} data the JSON payload to serialize
 | |
|  * @param {number} statusCode the http status code
 | |
|  * @returns {string} the serialized payload
 | |
|  */
 | |
| function serialize (context, data, statusCode) {
 | |
|   const fnSerialize = getSchemaSerializer(context, statusCode)
 | |
|   if (fnSerialize) {
 | |
|     return fnSerialize(data)
 | |
|   }
 | |
|   return JSON.stringify(data)
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Search for the right JSON schema compiled function in the request context
 | |
|  * setup by the route configuration `schema.response`.
 | |
|  * It will look for the exact match (eg 200) or generic (eg 2xx)
 | |
|  *
 | |
|  * @param {object} context the request context
 | |
|  * @param {number} statusCode the http status code
 | |
|  * @returns {function|boolean} the right JSON Schema function to serialize
 | |
|  * the reply or false if it is not set
 | |
|  */
 | |
| function getSchemaSerializer (context, statusCode) {
 | |
|   const responseSchemaDef = context[kSchemaResponse]
 | |
|   if (!responseSchemaDef) {
 | |
|     return false
 | |
|   }
 | |
|   if (responseSchemaDef[statusCode]) {
 | |
|     return responseSchemaDef[statusCode]
 | |
|   }
 | |
|   const fallbackStatusCode = (statusCode + '')[0] + 'xx'
 | |
|   if (responseSchemaDef[fallbackStatusCode]) {
 | |
|     return responseSchemaDef[fallbackStatusCode]
 | |
|   }
 | |
|   return false
 | |
| }
 | |
| 
 | |
| function noop () { }
 | |
| 
 | |
| module.exports = Reply
 | |
| module.exports.buildReply = buildReply
 | |
| module.exports.setupResponseListeners = setupResponseListeners
 |