UNPKG

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