UNPKG

14.2 kBJavaScriptView Raw
1'use strict'
2
3/*
4 * node-res
5 *
6 * (c) Harminder Virk <virk@adonisjs.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10*/
11
12const mime = require('mime-types')
13const etag = require('etag')
14const vary = require('vary')
15const onFinished = require('on-finished')
16const destroy = require('destroy')
17
18const methods = require('./methods')
19
20const returnContentAndType = function (body) {
21 /**
22 * Return the body and it's type when
23 * body is a string.
24 */
25 if (typeof (body) === 'string') {
26 return {
27 body,
28 type: /^\s*</.test(body) ? 'text/html' : 'text/plain'
29 }
30 }
31
32 /**
33 * If body is a buffer, return the exact copy
34 * and type as bin.
35 */
36 if (Buffer.isBuffer(body)) {
37 return { body, type: 'application/octet-stream' }
38 }
39
40 /**
41 * If body is a number or boolean. Convert it to
42 * a string and return the type as text.
43 */
44 if (typeof (body) === 'number' || typeof (body) === 'boolean') {
45 return { body: String(body), type: 'text/plain' }
46 }
47
48 /**
49 * Otherwise check whether body is an object or not. If yes
50 * stringify it and otherwise return the exact copy.
51 */
52 return typeof (body) === 'object'
53 ? { body: JSON.stringify(body), type: 'application/json' }
54 : { body }
55}
56
57/**
58 * A simple IO module to make consistent HTTP response, without
59 * worrying about underlying details.
60 *
61 * @module Response
62 */
63const Response = exports = module.exports = {}
64
65/**
66 * Copying all the descriptive methods to the response object.
67 */
68Response.descriptiveMethods = Object.keys(methods).map((method) => {
69 Response[method] = function (req, res, body) {
70 Response.status(res, methods[method])
71 Response.send(req, res, body)
72 }
73 return method
74})
75
76/**
77 * Returns the value of an existing header on
78 * the response object
79 *
80 * @method getHeader
81 *
82 * @param {ServerResponse} res
83 * @param {String} key
84 *
85 * @return {Array|String} Return type depends upon the header existing value
86 *
87 * @example
88 * ```js
89 * nodeRes.getHeader(res, 'Content-type')
90 * ```
91 */
92Response.getHeader = function (res, key) {
93 return res.getHeader(key)
94}
95
96/**
97 * Sets header on the response object. This method will wipe off
98 * existing values. To append to existing values, use `append`.
99 *
100 * @method header
101 *
102 * @param {http.ServerResponse} res
103 * @param {String} key
104 * @param {String|Array} value
105 *
106 * @return {void}
107 *
108 * @example
109 * ```js
110 * nodeRes.header(res, 'Content-type', 'application/json')
111 *
112 * // or set an array of headers
113 * nodeRes.header(res, 'Link', ['<http://localhost/>', '<http://localhost:3000/>'])
114 * ```
115 */
116Response.header = function (res, key, value) {
117 const values = Array.isArray(value) ? value.map(String) : value
118 res.setHeader(key, values)
119}
120
121/**
122 * Appends value to the header existing values.
123 *
124 * @method append
125 *
126 * @param {http.ServerResponse} res
127 * @param {String} key
128 * @param {String|Array} value
129 *
130 * @return {void}
131 *
132 * @example
133 * ```js
134 * nodeRes.append(res, 'Content-type', 'application/json')
135 *
136 * // or append an array of headers
137 * nodeRes.append(res, 'Link', ['<http://localhost/>', '<http://localhost:3000/>'])
138 * ```
139 */
140Response.append = function (res, key, value) {
141 const previousValue = Response.getHeader(res, key)
142
143 const headers = previousValue
144 ? (Array.isArray(previousValue) ? previousValue.concat(value) : [previousValue].concat(value))
145 : value
146
147 Response.header(res, key, headers)
148}
149
150/**
151 * Set status on the HTTP res object
152 *
153 * @method status
154 *
155 * @param {http.ServerResponse} res
156 * @param {Number} code
157 *
158 * @return {void}
159 *
160 * @example
161 * ```js
162 * nodeRes.status(res, 200)
163 * ```
164 */
165Response.status = function (res, code) {
166 res.statusCode = code
167}
168
169/**
170 * Sets the header on response object, only if it
171 * does not exists.
172 *
173 * @method safeHeader
174 *
175 * @param {http.ServerResponse} res
176 * @param {String} key
177 * @param {String|Array} value
178 *
179 * @return {void}
180 *
181 * @example
182 * ```js
183 * nodeRes.safeHeader(res, 'Content-type', 'application/json')
184 * ```
185 */
186Response.safeHeader = function (res, key, value) {
187 if (!res.getHeader(key)) {
188 Response.header(res, key, value)
189 }
190}
191
192/**
193 * Removes the header from response
194 *
195 * @method removeHeader
196 *
197 * @param {http.ServerResponse} res
198 * @param {String} key
199 *
200 * @return {void}
201 *
202 * @example
203 * ```js
204 * nodeRes.removeHeader(res, 'Content-type')
205 * ```
206 */
207Response.removeHeader = function (res, key) {
208 res.removeHeader(key)
209}
210
211/**
212 * Write string or buffer to the response object.
213 *
214 * @method write
215 *
216 * @param {http.ServerResponse} res
217 * @param {String|Buffer} body
218 *
219 * @return {void}
220 *
221 * @example
222 * ```js
223 * nodeRes.write(res, 'Hello world')
224 * ```
225 */
226Response.write = function (res, body) {
227 res.write(body)
228}
229
230/**
231 * Explictly end HTTP response
232 *
233 * @method end
234 *
235 * @param {http.ServerResponse} res
236 * @param {String|Buffer} [payload]
237 *
238 * @return {void}
239 *
240 * @example
241 * ```js
242 * nodeRes.end(res, 'Hello world')
243 * ```
244 */
245Response.end = function (res, payload) {
246 res.end(payload)
247}
248
249/**
250 * Send body as the HTTP response and end it. Also
251 * this method will set the appropriate `Content-type`
252 * and `Content-length`.
253 *
254 * If body is set to null, this method will end the response
255 * as 204.
256 *
257 * @method send
258 *
259 * @param {http.ServerRequest} req
260 * @param {http.ServerResponse} res
261 * @param {String|Buffer|Object|Stream} body
262 * @param {Boolean} [generateEtag = true]
263 *
264 * @return {void}
265 *
266 * @example
267 * ```js
268 * nodeRes.send(req, res, 'Hello world')
269 *
270 * // or html
271 * nodeRes.send(req, res, '<h2> Hello world </h2>')
272 *
273 * // or JSON
274 * nodeRes.send(req, res, { greeting: 'Hello world' })
275 *
276 * // or Buffer
277 * nodeRes.send(req, res, Buffer.from('Hello world', 'utf-8'))
278 *
279 * // Ignore etag
280 * nodeRes.send(req, res, 'Hello world', false)
281 * ```
282 */
283Response.send = function (req, res, body = null, generateEtag = true) {
284 /**
285 * Handle streams
286 */
287 if (body && typeof (body.pipe) === 'function') {
288 Response
289 .stream(res, body)
290 .catch((error) => {
291 Response.status(res, error.code === 'ENOENT' ? 404 : 500)
292 Response.send(req, res, error.message, generateEtag)
293 })
294 return
295 }
296
297 const chunk = Response.prepare(res, body)
298
299 if (chunk === null || req.method === 'HEAD') {
300 Response.end(res)
301 return
302 }
303
304 /**
305 * Generate etag when instructured for
306 */
307 if (generateEtag) {
308 Response.etag(res, chunk)
309 }
310
311 Response.end(res, chunk)
312}
313
314/**
315 * Sets the Etag header for a given body chunk
316 *
317 * @method etag
318 *
319 * @param {http.ServerResponse} res
320 * @param {String|Buffer} body
321 *
322 * @return {void}
323 *
324 * @example
325 * ```js
326 * nodeRes.etag(res, 'Hello world')
327 * ```
328 */
329Response.etag = function (res, body) {
330 Response.header(res, 'ETag', etag(body))
331}
332
333/**
334 * Prepares the response body by encoding it properly. Also
335 * sets appropriate headers based upon the body content type.
336 *
337 * This method is used internally by `send`, so you should
338 * never use it when calling `send`.
339 *
340 * It is helpful when you want to get the final payload and end the
341 * response at a later stage.
342 *
343 * @method prepare
344 *
345 * @param {http.ServerResponse} res
346 * @param {Mixed} body
347 *
348 * @return {String}
349 *
350 * @example
351 * ```js
352 * const chunk = nodeRes.prepare(res, '<h2> Hello </h2>')
353 *
354 * if (chunk) {
355 * nodeRes.etag(res, chunk)
356 *
357 * if (nodeReq.fresh(req, res)) {
358 * chunk = null
359 * nodeRes.status(304)
360 * }
361 *
362 * nodeRes.end(chunk)
363 * }
364 * ```
365 */
366Response.prepare = function (res, body) {
367 if (body === null) {
368 Response.status(res, 204)
369 Response.removeHeader(res, 'Content-Type')
370 Response.removeHeader(res, 'Content-Length')
371 Response.removeHeader(res, 'Transfer-Encoding')
372 return null
373 }
374
375 let { body: chunk, type } = returnContentAndType(body)
376
377 /**
378 * Remove unwanted headers when statuscode is 204 or 304
379 */
380 if (res.statusCode === 204 || res.statusCode === 304) {
381 Response.removeHeader(res, 'Content-Type')
382 Response.removeHeader(res, 'Content-Length')
383 Response.removeHeader(res, 'Transfer-Encoding')
384 return chunk
385 }
386
387 const headers = typeof res.getHeaders === 'function' ? res.getHeaders() : (res._headers || {})
388
389 /**
390 * Setting content type. Ideally we can use `Response.type`, which
391 * sets the right charset too. But we will be doing extra
392 * processing for no reasons.
393 */
394 if (type && !headers['content-type']) {
395 Response.header(res, 'Content-Type', `${type}; charset=utf-8`)
396 }
397
398 /**
399 * setting up content length as response header
400 */
401 if (chunk && !headers['content-length']) {
402 Response.header(res, 'Content-Length', Buffer.byteLength(chunk))
403 }
404
405 return chunk
406}
407
408/**
409 * Prepares response for JSONP
410 *
411 * @method prepareJsonp
412 *
413 * @param {http.ServerResponse} res
414 * @param {Object} body
415 * @param {String} callbackFn
416 *
417 * @return {String}
418 *
419 * @example
420 * ```js
421 * const chunk = nodeRes.prepareJsonp(res, '<h2> Hello </h2>', 'callback')
422 *
423 * if (chunk) {
424 * nodeRes.etag(res, chunk)
425 *
426 * if (nodeReq.fresh(req, res)) {
427 * chunk = null
428 * nodeRes.status(304)
429 * }
430 *
431 * nodeRes.end(chunk)
432 * }
433 * ```
434 */
435Response.prepareJsonp = function (res, body, callbackFn) {
436 Response.header(res, 'X-Content-Type-Options', 'nosniff')
437 Response.safeHeader(res, 'Content-Type', 'text/javascript; charset=utf-8')
438
439 const parsedBody = JSON
440 .stringify(body)
441 .replace(/\u2028/g, '\\u2028')
442 .replace(/\u2029/g, '\\u2029')
443
444 /**
445 * setting up callbackFn on response body , typeof will make
446 * sure not to throw error of client if callbackFn is not
447 * a function
448 */
449 return '/**/ typeof ' + callbackFn + " === 'function' && " + callbackFn + '(' + parsedBody + ');'
450}
451
452/**
453 * Returns the HTTP response with `Content-type`
454 * set to `application/json`.
455 *
456 * @method json
457 *
458 * @param {http.IncomingMessage} req
459 * @param {http.ServerResponse} res
460 * @param {Object} body
461 * @param {Boolean} [generateEtag = true]
462 *
463 * @return {void}
464 *
465 * @example
466 * ```js
467 * nodeRes.json(req, res, { name: 'virk' })
468 * nodeRes.json(req, res, [ 'virk', 'joe' ])
469 * ```
470 */
471Response.json = function (req, res, body, generateEtag) {
472 Response.safeHeader(res, 'Content-Type', 'application/json; charset=utf-8')
473 Response.send(req, res, body, generateEtag)
474}
475
476/**
477 * Make JSONP response with `Content-type` set to
478 * `text/javascript`.
479 *
480 * @method jsonp
481 *
482 * @param {http.IncomingMessage} req
483 * @param {http.ServerResponse} res
484 * @param {Object} body
485 * @param {String} [callbackFn = 'callback']
486 * @param {Boolean} [generateEtag = true]
487 *
488 * @return {void}
489 *
490 * @example
491 * ```js
492 * nodeRes.jsonp(req, res, { name: 'virk' }, 'callback')
493 * ```
494 */
495Response.jsonp = function (req, res, body, callbackFn = 'callback', generateEtag) {
496 Response.send(req, res, Response.prepareJsonp(res, body, callbackFn), generateEtag)
497}
498
499/**
500 * Set `Location` header on the HTTP response.
501 *
502 * @method location
503 *
504 * @param {http.ServerResponse} res
505 * @param {String} url
506 *
507 * @return {void}
508 */
509Response.location = function (res, url) {
510 Response.header(res, 'Location', url)
511}
512
513/**
514 * Redirect the HTTP request to the given url.
515 *
516 * @method redirect
517 *
518 * @param {http.IncomingMessage} req
519 * @param {http.ServerResponse} res
520 * @param {String} url
521 * @param {Number} [status = 302]
522 *
523 * @return {void}
524 *
525 * @example
526 * ```js
527 * nodeRes.redirect(req, res, '/')
528 * ```
529 */
530Response.redirect = function (req, res, url, status = 302) {
531 const body = ''
532 Response.status(res, status)
533 Response.location(res, url)
534 Response.send(req, res, body)
535}
536
537/**
538 * Add vary header to the HTTP response.
539 *
540 * @method vary
541 *
542 * @param {http.ServerResponse} res
543 * @param {String} field
544 *
545 * @return {void}
546 */
547Response.vary = function (res, field) {
548 vary(res, field)
549}
550
551/**
552 * Set content type header by looking up the actual
553 * type and setting charset to utf8.
554 *
555 * ### Note
556 * When defining custom charset, you must set pass the complete
557 * content type, otherwise `false` will be set as the
558 * content-type header.
559 *
560 * @method type
561 *
562 * @param {http.IncomingMessage} req
563 * @param {http.ServerResponse} res
564 * @param {String} [charset]
565 * @return {void}
566 *
567 * @example
568 * ```js
569 * nodeRes.type(res, 'html')
570 *
571 * nodeRes.type(res, 'json')
572 *
573 * nodeRes.type(res, 'text/html', 'ascii')
574 * ```
575 */
576Response.type = function (res, type, charset) {
577 type = charset ? `${type}; charset=${charset}` : type
578 Response.safeHeader(res, 'Content-Type', mime.contentType(type))
579}
580
581/**
582 * Pipe stream to the response. Also this method will make sure
583 * to destroy the stream, if request gets cancelled.
584 *
585 * The promise resolve when response finishes and rejects, when
586 * stream raises errors.
587 *
588 * @method stream
589 *
590 * @param {Object} res
591 * @param {Stream} body
592 *
593 * @returns {Promise}
594 *
595 * @example
596 * ```js
597 * Response.stream(res, fs.createReadStream('foo.txt'))
598 *
599 * // handle stream errors
600 * Response
601 * .stream(res, fs.createReadStream('foo.txt'))
602 * .catch((error) => {
603 * })
604 * ```
605 */
606Response.stream = function (res, body) {
607 return new Promise((resolve, reject) => {
608 if (typeof (body.pipe) !== 'function') {
609 reject(new Error('Body is not a valid stream'))
610 return
611 }
612
613 let finished = false
614
615 /**
616 * Error in stream
617 */
618 body.on('error', (error) => {
619 if (finished) {
620 return
621 }
622
623 finished = true
624 destroy(body)
625
626 reject(error)
627 })
628
629 /**
630 * Consumed stream
631 */
632 body.on('end', resolve)
633
634 /**
635 * Written response
636 */
637 onFinished(res, function () {
638 finished = true
639 destroy(body)
640 })
641
642 /**
643 * Pipe to res
644 */
645 body.pipe(res)
646 })
647}