1 |
|
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 |
|
12 | const mime = require('mime-types')
|
13 | const etag = require('etag')
|
14 | const vary = require('vary')
|
15 | const onFinished = require('on-finished')
|
16 | const destroy = require('destroy')
|
17 |
|
18 | const methods = require('./methods')
|
19 |
|
20 | const 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 | */
|
63 | const Response = exports = module.exports = {}
|
64 |
|
65 | /**
|
66 | * Copying all the descriptive methods to the response object.
|
67 | */
|
68 | Response.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 | */
|
92 | Response.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 | */
|
116 | Response.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 | */
|
140 | Response.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 | */
|
165 | Response.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 | */
|
186 | Response.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 | */
|
207 | Response.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 | */
|
226 | Response.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 | */
|
245 | Response.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 | */
|
283 | Response.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 | */
|
329 | Response.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 | */
|
366 | Response.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 | */
|
435 | Response.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 | */
|
471 | Response.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 | */
|
495 | Response.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 | */
|
509 | Response.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 | */
|
530 | Response.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 | */
|
547 | Response.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 | */
|
576 | Response.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 | */
|
606 | Response.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 | }
|