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
			| 
											3 years ago
										 | '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 |