1 | 'use strict'
|
2 |
|
3 | const eos = require('readable-stream').finished
|
4 | const statusCodes = require('http').STATUS_CODES
|
5 | const flatstr = require('flatstr')
|
6 | const FJS = require('fast-json-stringify')
|
7 | const {
|
8 | kFourOhFourContext,
|
9 | kReplyErrorHandlerCalled,
|
10 | kReplySent,
|
11 | kReplySentOverwritten,
|
12 | kReplyStartTime,
|
13 | kReplySerializer,
|
14 | kReplySerializerDefault,
|
15 | kReplyIsError,
|
16 | kReplyHeaders,
|
17 | kReplyHasStatusCode,
|
18 | kReplyIsRunningOnErrorHook,
|
19 | kDisableRequestLogging
|
20 | } = require('./symbols.js')
|
21 | const { hookRunner, hookIterator, onSendHookRunner } = require('./hooks')
|
22 | const validation = require('./validation')
|
23 | const serialize = validation.serialize
|
24 |
|
25 | const internals = require('./handleRequest')[Symbol.for('internals')]
|
26 | const loggerUtils = require('./logger')
|
27 | const now = loggerUtils.now
|
28 | const wrapThenable = require('./wrapThenable')
|
29 |
|
30 | const serializeError = FJS({
|
31 | type: 'object',
|
32 | properties: {
|
33 | statusCode: { type: 'number' },
|
34 | code: { type: 'string' },
|
35 | error: { type: 'string' },
|
36 | message: { type: 'string' }
|
37 | }
|
38 | })
|
39 |
|
40 | const CONTENT_TYPE = {
|
41 | JSON: 'application/json; charset=utf-8',
|
42 | PLAIN: 'text/plain; charset=utf-8',
|
43 | OCTET: 'application/octet-stream'
|
44 | }
|
45 | const {
|
46 | codes: {
|
47 | FST_ERR_REP_INVALID_PAYLOAD_TYPE,
|
48 | FST_ERR_REP_ALREADY_SENT,
|
49 | FST_ERR_REP_SENT_VALUE,
|
50 | FST_ERR_SEND_INSIDE_ONERR,
|
51 | FST_ERR_BAD_STATUS_CODE
|
52 | }
|
53 | } = require('./errors')
|
54 |
|
55 | var getHeader
|
56 |
|
57 | function Reply (res, context, request, log) {
|
58 | this.res = res
|
59 | this.context = context
|
60 | this[kReplySent] = false
|
61 | this[kReplySerializer] = null
|
62 | this[kReplyErrorHandlerCalled] = false
|
63 | this[kReplyIsError] = false
|
64 | this[kReplyIsRunningOnErrorHook] = false
|
65 | this.request = request
|
66 | this[kReplyHeaders] = {}
|
67 | this[kReplyHasStatusCode] = false
|
68 | this[kReplyStartTime] = undefined
|
69 | this.log = log
|
70 | }
|
71 |
|
72 | Object.defineProperty(Reply.prototype, 'sent', {
|
73 | enumerable: true,
|
74 | get () {
|
75 | return this[kReplySent]
|
76 | },
|
77 | set (value) {
|
78 | if (value !== true) {
|
79 | throw new FST_ERR_REP_SENT_VALUE()
|
80 | }
|
81 |
|
82 | if (this[kReplySent]) {
|
83 | throw new FST_ERR_REP_ALREADY_SENT()
|
84 | }
|
85 |
|
86 | this[kReplySentOverwritten] = true
|
87 | this[kReplySent] = true
|
88 | }
|
89 | })
|
90 |
|
91 | Object.defineProperty(Reply.prototype, 'statusCode', {
|
92 | get () {
|
93 | return this.res.statusCode
|
94 | },
|
95 | set (value) {
|
96 | this.code(value)
|
97 | }
|
98 | })
|
99 |
|
100 | Reply.prototype.send = function (payload) {
|
101 | if (this[kReplyIsRunningOnErrorHook] === true) {
|
102 | throw new FST_ERR_SEND_INSIDE_ONERR()
|
103 | }
|
104 |
|
105 | if (this[kReplySent]) {
|
106 | this.log.warn({ err: new FST_ERR_REP_ALREADY_SENT() }, 'Reply already sent')
|
107 | return this
|
108 | }
|
109 |
|
110 | if (payload instanceof Error || this[kReplyIsError] === true) {
|
111 | onErrorHook(this, payload, onSendHook)
|
112 | return this
|
113 | }
|
114 |
|
115 | if (payload === undefined) {
|
116 | onSendHook(this, payload)
|
117 | return this
|
118 | }
|
119 |
|
120 | var contentType = getHeader(this, 'content-type')
|
121 | var hasContentType = contentType !== undefined
|
122 |
|
123 | if (payload !== null) {
|
124 | if (Buffer.isBuffer(payload) || typeof payload.pipe === 'function') {
|
125 | if (hasContentType === false) {
|
126 | this[kReplyHeaders]['content-type'] = CONTENT_TYPE.OCTET
|
127 | }
|
128 | onSendHook(this, payload)
|
129 | return this
|
130 | }
|
131 |
|
132 | if (hasContentType === false && typeof payload === 'string') {
|
133 | this[kReplyHeaders]['content-type'] = CONTENT_TYPE.PLAIN
|
134 | onSendHook(this, payload)
|
135 | return this
|
136 | }
|
137 | }
|
138 |
|
139 | if (this[kReplySerializer] !== null) {
|
140 | payload = this[kReplySerializer](payload)
|
141 |
|
142 |
|
143 | } else if (hasContentType === false || contentType.indexOf('json') > -1) {
|
144 | if (hasContentType === false) {
|
145 | this[kReplyHeaders]['content-type'] = CONTENT_TYPE.JSON
|
146 | } else {
|
147 |
|
148 | if (contentType.indexOf('charset') === -1) {
|
149 |
|
150 | if (contentType.indexOf('/json') > -1) {
|
151 | this[kReplyHeaders]['content-type'] = CONTENT_TYPE.JSON
|
152 | } else {
|
153 | const currContentType = this[kReplyHeaders]['content-type']
|
154 |
|
155 | const customJsonType = currContentType.substring(
|
156 | currContentType.indexOf('/'),
|
157 | currContentType.indexOf('json') + 4
|
158 | )
|
159 |
|
160 |
|
161 |
|
162 | this[kReplyHeaders]['content-type'] = CONTENT_TYPE.JSON.replace('/json', customJsonType)
|
163 | }
|
164 | }
|
165 | }
|
166 | if (typeof payload !== 'string') {
|
167 | preserializeHook(this, payload)
|
168 | return this
|
169 | }
|
170 | }
|
171 |
|
172 | onSendHook(this, payload)
|
173 |
|
174 | return this
|
175 | }
|
176 |
|
177 | Reply.prototype.getHeader = function (key) {
|
178 | return getHeader(this, key)
|
179 | }
|
180 |
|
181 | Reply.prototype.hasHeader = function (key) {
|
182 | return this[kReplyHeaders][key.toLowerCase()] !== undefined
|
183 | }
|
184 |
|
185 | Reply.prototype.removeHeader = function (key) {
|
186 |
|
187 |
|
188 | delete this[kReplyHeaders][key.toLowerCase()]
|
189 | return this
|
190 | }
|
191 |
|
192 | Reply.prototype.header = function (key, value) {
|
193 | var _key = key.toLowerCase()
|
194 |
|
195 |
|
196 | value = value === undefined ? '' : value
|
197 |
|
198 | if (this[kReplyHeaders][_key] && _key === 'set-cookie') {
|
199 |
|
200 | if (typeof this[kReplyHeaders][_key] === 'string') {
|
201 | this[kReplyHeaders][_key] = [this[kReplyHeaders][_key]]
|
202 | }
|
203 | if (Array.isArray(value)) {
|
204 | Array.prototype.push.apply(this[kReplyHeaders][_key], value)
|
205 | } else {
|
206 | this[kReplyHeaders][_key].push(value)
|
207 | }
|
208 | } else {
|
209 | this[kReplyHeaders][_key] = value
|
210 | }
|
211 | return this
|
212 | }
|
213 |
|
214 | Reply.prototype.headers = function (headers) {
|
215 | var keys = Object.keys(headers)
|
216 | for (var i = 0; i < keys.length; i++) {
|
217 | this.header(keys[i], headers[keys[i]])
|
218 | }
|
219 | return this
|
220 | }
|
221 |
|
222 | Reply.prototype.code = function (code) {
|
223 | const intValue = parseInt(code)
|
224 | if (isNaN(intValue) || intValue < 100 || intValue > 600) {
|
225 | throw new FST_ERR_BAD_STATUS_CODE(String(code))
|
226 | }
|
227 |
|
228 | this.res.statusCode = intValue
|
229 | this[kReplyHasStatusCode] = true
|
230 | return this
|
231 | }
|
232 |
|
233 | Reply.prototype.status = Reply.prototype.code
|
234 |
|
235 | Reply.prototype.serialize = function (payload) {
|
236 | if (this[kReplySerializer] !== null) {
|
237 | return this[kReplySerializer](payload)
|
238 | } else {
|
239 | if (this.context && this.context[kReplySerializerDefault]) {
|
240 | return this.context[kReplySerializerDefault](payload, this.res.statusCode)
|
241 | } else {
|
242 | return serialize(this.context, payload, this.res.statusCode)
|
243 | }
|
244 | }
|
245 | }
|
246 |
|
247 | Reply.prototype.serializer = function (fn) {
|
248 | this[kReplySerializer] = fn
|
249 | return this
|
250 | }
|
251 |
|
252 | Reply.prototype.type = function (type) {
|
253 | this[kReplyHeaders]['content-type'] = type
|
254 | return this
|
255 | }
|
256 |
|
257 | Reply.prototype.redirect = function (code, url) {
|
258 | if (typeof code === 'string') {
|
259 | url = code
|
260 | code = this[kReplyHasStatusCode] ? this.res.statusCode : 302
|
261 | }
|
262 |
|
263 | this.header('location', url).code(code).send()
|
264 | }
|
265 |
|
266 | Reply.prototype.callNotFound = function () {
|
267 | notFound(this)
|
268 | }
|
269 |
|
270 | Reply.prototype.getResponseTime = function () {
|
271 | var responseTime = 0
|
272 |
|
273 | if (this[kReplyStartTime] !== undefined) {
|
274 | responseTime = now() - this[kReplyStartTime]
|
275 | }
|
276 |
|
277 | return responseTime
|
278 | }
|
279 |
|
280 |
|
281 |
|
282 |
|
283 |
|
284 |
|
285 | Reply.prototype.then = function (fullfilled, rejected) {
|
286 | if (this.sent) {
|
287 | fullfilled()
|
288 | return
|
289 | }
|
290 |
|
291 | eos(this.res, function (err) {
|
292 |
|
293 |
|
294 | if (err && err.code !== 'ERR_STREAM_PREMATURE_CLOSE') {
|
295 | if (rejected) {
|
296 | rejected(err)
|
297 | }
|
298 | } else {
|
299 | fullfilled()
|
300 | }
|
301 | })
|
302 | }
|
303 |
|
304 | function preserializeHook (reply, payload) {
|
305 | if (reply.context.preSerialization !== null) {
|
306 | onSendHookRunner(
|
307 | reply.context.preSerialization,
|
308 | reply.request,
|
309 | reply,
|
310 | payload,
|
311 | preserializeHookEnd
|
312 | )
|
313 | } else {
|
314 | preserializeHookEnd(null, reply.request, reply, payload)
|
315 | }
|
316 | }
|
317 |
|
318 | function preserializeHookEnd (err, request, reply, payload) {
|
319 | if (err != null) {
|
320 | onErrorHook(reply, err)
|
321 | return
|
322 | }
|
323 |
|
324 | if (reply.context && reply.context[kReplySerializerDefault]) {
|
325 | payload = reply.context[kReplySerializerDefault](payload, reply.res.statusCode)
|
326 | } else {
|
327 | payload = serialize(reply.context, payload, reply.res.statusCode)
|
328 | }
|
329 |
|
330 | flatstr(payload)
|
331 |
|
332 | onSendHook(reply, payload)
|
333 | }
|
334 |
|
335 | function onSendHook (reply, payload) {
|
336 | reply[kReplySent] = true
|
337 | if (reply.context.onSend !== null) {
|
338 | onSendHookRunner(
|
339 | reply.context.onSend,
|
340 | reply.request,
|
341 | reply,
|
342 | payload,
|
343 | wrapOnSendEnd
|
344 | )
|
345 | } else {
|
346 | onSendEnd(reply, payload)
|
347 | }
|
348 | }
|
349 |
|
350 | function wrapOnSendEnd (err, request, reply, payload) {
|
351 | if (err != null) {
|
352 | onErrorHook(reply, err)
|
353 | } else {
|
354 | onSendEnd(reply, payload)
|
355 | }
|
356 | }
|
357 |
|
358 | function onSendEnd (reply, payload) {
|
359 | var res = reply.res
|
360 | var statusCode = res.statusCode
|
361 |
|
362 | if (payload === undefined || payload === null) {
|
363 | reply[kReplySent] = true
|
364 |
|
365 |
|
366 |
|
367 |
|
368 | if (statusCode >= 200 && statusCode !== 204 && statusCode !== 304) {
|
369 | reply[kReplyHeaders]['content-length'] = '0'
|
370 | }
|
371 |
|
372 | res.writeHead(statusCode, reply[kReplyHeaders])
|
373 |
|
374 | res.end(null, null, null)
|
375 | return
|
376 | }
|
377 |
|
378 | if (typeof payload.pipe === 'function') {
|
379 | sendStream(payload, res, reply)
|
380 | return
|
381 | }
|
382 |
|
383 | if (typeof payload !== 'string' && !Buffer.isBuffer(payload)) {
|
384 | throw new FST_ERR_REP_INVALID_PAYLOAD_TYPE(typeof payload)
|
385 | }
|
386 |
|
387 | if (!reply[kReplyHeaders]['content-length']) {
|
388 | reply[kReplyHeaders]['content-length'] = '' + Buffer.byteLength(payload)
|
389 | }
|
390 |
|
391 | reply[kReplySent] = true
|
392 |
|
393 | res.writeHead(statusCode, reply[kReplyHeaders])
|
394 |
|
395 |
|
396 | res.end(payload, null, null)
|
397 | }
|
398 |
|
399 | function sendStream (payload, res, reply) {
|
400 | var sourceOpen = true
|
401 |
|
402 | eos(payload, { readable: true, writable: false }, function (err) {
|
403 | sourceOpen = false
|
404 | if (err != null) {
|
405 | if (res.headersSent) {
|
406 | reply.log.warn({ err }, 'response terminated with an error with headers already sent')
|
407 | res.destroy()
|
408 | } else {
|
409 | onErrorHook(reply, err)
|
410 | }
|
411 | }
|
412 |
|
413 | })
|
414 |
|
415 | eos(res, function (err) {
|
416 | if (err != null) {
|
417 | if (res.headersSent) {
|
418 | reply.log.warn({ err }, 'response terminated with an error with headers already sent')
|
419 | }
|
420 | if (sourceOpen) {
|
421 | if (payload.destroy) {
|
422 | payload.destroy()
|
423 | } else if (typeof payload.close === 'function') {
|
424 | payload.close(noop)
|
425 | } else if (typeof payload.abort === 'function') {
|
426 | payload.abort()
|
427 | }
|
428 | }
|
429 | }
|
430 | })
|
431 |
|
432 |
|
433 |
|
434 |
|
435 |
|
436 | if (!res.headersSent) {
|
437 | for (var key in reply[kReplyHeaders]) {
|
438 | res.setHeader(key, reply[kReplyHeaders][key])
|
439 | }
|
440 | } else {
|
441 | reply.log.warn('response will send, but you shouldn\'t use res.writeHead in stream mode')
|
442 | }
|
443 | payload.pipe(res)
|
444 | }
|
445 |
|
446 | function onErrorHook (reply, error, cb) {
|
447 | reply[kReplySent] = true
|
448 | if (reply.context.onError !== null && reply[kReplyErrorHandlerCalled] === true) {
|
449 | reply[kReplyIsRunningOnErrorHook] = true
|
450 | onSendHookRunner(
|
451 | reply.context.onError,
|
452 | reply.request,
|
453 | reply,
|
454 | error,
|
455 | () => handleError(reply, error, cb)
|
456 | )
|
457 | } else {
|
458 | handleError(reply, error, cb)
|
459 | }
|
460 | }
|
461 |
|
462 | function handleError (reply, error, cb) {
|
463 | reply[kReplyIsRunningOnErrorHook] = false
|
464 | var res = reply.res
|
465 | var statusCode = res.statusCode
|
466 | statusCode = (statusCode >= 400) ? statusCode : 500
|
467 |
|
468 | if (error != null) {
|
469 | if (error.headers !== undefined) {
|
470 | reply.headers(error.headers)
|
471 | }
|
472 | if (error.status >= 400) {
|
473 | statusCode = error.status
|
474 | } else if (error.statusCode >= 400) {
|
475 | statusCode = error.statusCode
|
476 | }
|
477 | }
|
478 |
|
479 | res.statusCode = statusCode
|
480 |
|
481 | var errorHandler = reply.context.errorHandler
|
482 | if (errorHandler && reply[kReplyErrorHandlerCalled] === false) {
|
483 | reply[kReplySent] = false
|
484 | reply[kReplyIsError] = false
|
485 | reply[kReplyErrorHandlerCalled] = true
|
486 | var result = errorHandler(error, reply.request, reply)
|
487 | if (result && typeof result.then === 'function') {
|
488 | wrapThenable(result, reply)
|
489 | }
|
490 | return
|
491 | }
|
492 |
|
493 | var payload = serializeError({
|
494 | error: statusCodes[statusCode + ''],
|
495 | code: error.code,
|
496 | message: error.message || '',
|
497 | statusCode: statusCode
|
498 | })
|
499 | flatstr(payload)
|
500 | reply[kReplyHeaders]['content-type'] = CONTENT_TYPE.JSON
|
501 | reply[kReplyHeaders]['content-length'] = '' + Buffer.byteLength(payload)
|
502 |
|
503 | if (cb) {
|
504 | cb(reply, payload)
|
505 | return
|
506 | }
|
507 |
|
508 | reply[kReplySent] = true
|
509 | res.writeHead(res.statusCode, reply[kReplyHeaders])
|
510 | res.end(payload)
|
511 | }
|
512 |
|
513 | function setupResponseListeners (reply) {
|
514 | reply[kReplyStartTime] = now()
|
515 |
|
516 | var onResFinished = err => {
|
517 | reply.res.removeListener('finish', onResFinished)
|
518 | reply.res.removeListener('error', onResFinished)
|
519 |
|
520 | var ctx = reply.context
|
521 |
|
522 | if (ctx && ctx.onResponse !== null) {
|
523 | hookRunner(
|
524 | ctx.onResponse,
|
525 | onResponseIterator,
|
526 | reply.request,
|
527 | reply,
|
528 | onResponseCallback
|
529 | )
|
530 | } else {
|
531 | onResponseCallback(err, reply.request, reply)
|
532 | }
|
533 | }
|
534 |
|
535 | reply.res.on('finish', onResFinished)
|
536 | reply.res.on('error', onResFinished)
|
537 | }
|
538 |
|
539 | function onResponseIterator (fn, request, reply, next) {
|
540 | return fn(request, reply, next)
|
541 | }
|
542 |
|
543 | function onResponseCallback (err, request, reply) {
|
544 | if (reply.log[kDisableRequestLogging]) {
|
545 | return
|
546 | }
|
547 |
|
548 | var responseTime = reply.getResponseTime()
|
549 |
|
550 | if (err != null) {
|
551 | reply.log.error({
|
552 | res: reply.res,
|
553 | err,
|
554 | responseTime
|
555 | }, 'request errored')
|
556 | return
|
557 | }
|
558 |
|
559 | reply.log.info({
|
560 | res: reply.res,
|
561 | responseTime
|
562 | }, 'request completed')
|
563 | }
|
564 |
|
565 | function buildReply (R) {
|
566 | function _Reply (res, context, request, log) {
|
567 | this.res = res
|
568 | this.context = context
|
569 | this[kReplyIsError] = false
|
570 | this[kReplyErrorHandlerCalled] = false
|
571 | this[kReplySent] = false
|
572 | this[kReplySentOverwritten] = false
|
573 | this[kReplySerializer] = null
|
574 | this.request = request
|
575 | this[kReplyHeaders] = {}
|
576 | this[kReplyStartTime] = undefined
|
577 | this.log = log
|
578 | }
|
579 | _Reply.prototype = new R()
|
580 | return _Reply
|
581 | }
|
582 |
|
583 | function notFound (reply) {
|
584 | reply[kReplySent] = false
|
585 | reply[kReplyIsError] = false
|
586 |
|
587 | if (reply.context[kFourOhFourContext] === null) {
|
588 | reply.log.warn('Trying to send a NotFound error inside a 404 handler. Sending basic 404 response.')
|
589 | reply.code(404).send('404 Not Found')
|
590 | return
|
591 | }
|
592 |
|
593 | reply.context = reply.context[kFourOhFourContext]
|
594 |
|
595 |
|
596 | if (reply.context.preHandler !== null) {
|
597 | hookRunner(
|
598 | reply.context.preHandler,
|
599 | hookIterator,
|
600 | reply.request,
|
601 | reply,
|
602 | internals.preHandlerCallback
|
603 | )
|
604 | } else {
|
605 | internals.preHandlerCallback(null, reply.request, reply)
|
606 | }
|
607 | }
|
608 |
|
609 | function noop () {}
|
610 |
|
611 | function getHeaderProper (reply, key) {
|
612 | key = key.toLowerCase()
|
613 | var res = reply.res
|
614 | var value = reply[kReplyHeaders][key]
|
615 | if (value === undefined && res.hasHeader(key)) {
|
616 | value = res.getHeader(key)
|
617 | }
|
618 | return value
|
619 | }
|
620 |
|
621 | function getHeaderFallback (reply, key) {
|
622 | key = key.toLowerCase()
|
623 | var res = reply.res
|
624 | var value = reply[kReplyHeaders][key]
|
625 | if (value === undefined) {
|
626 | value = res.getHeader(key)
|
627 | }
|
628 | return value
|
629 | }
|
630 |
|
631 |
|
632 |
|
633 | {
|
634 | const v = process.version.match(/v(\d+)/)[1]
|
635 | if (Number(v) > 7) {
|
636 | getHeader = getHeaderProper
|
637 | } else {
|
638 | getHeader = getHeaderFallback
|
639 | }
|
640 | }
|
641 |
|
642 | module.exports = Reply
|
643 | module.exports.buildReply = buildReply
|
644 | module.exports.setupResponseListeners = setupResponseListeners
|