thoughts/node_modules/express/lib/response.js
node_modules/express/lib/response.jsRaw
1/*!
2 * express
3 * Copyright(c) 2009-2013 TJ Holowaychuk
4 * Copyright(c) 2014-2015 Douglas Christopher Wilson
5 * MIT Licensed
6 */
7
8'use strict';
9
10/**
11 * Module dependencies.
12 * @private
13 */
14
15var contentDisposition = require('content-disposition');
16var deprecate = require('depd')('express');
17var encodeUrl = require('encodeurl');
18var escapeHtml = require('escape-html');
19var http = require('http');
20var isAbsolute = require('./utils').isAbsolute;
21var onFinished = require('on-finished');
22var path = require('path');
23var merge = require('utils-merge');
24var sign = require('cookie-signature').sign;
25var normalizeType = require('./utils').normalizeType;
26var normalizeTypes = require('./utils').normalizeTypes;
27var setCharset = require('./utils').setCharset;
28var statusCodes = http.STATUS_CODES;
29var cookie = require('cookie');
30var send = require('send');
31var extname = path.extname;
32var mime = send.mime;
33var resolve = path.resolve;
34var vary = require('vary');
35
36/**
37 * Response prototype.
38 */
39
40var res = module.exports = {
41 __proto__: http.ServerResponse.prototype
42};
43
44/**
45 * Module variables.
46 * @private
47 */
48
49var charsetRegExp = /;\s*charset\s*=/;
50
51/**
52 * Set status `code`.
53 *
54 * @param {Number} code
55 * @return {ServerResponse}
56 * @public
57 */
58
59res.status = function status(code) {
60 this.statusCode = code;
61 return this;
62};
63
64/**
65 * Set Link header field with the given `links`.
66 *
67 * Examples:
68 *
69 * res.links({
70 * next: 'http://api.example.com/users?page=2',
71 * last: 'http://api.example.com/users?page=5'
72 * });
73 *
74 * @param {Object} links
75 * @return {ServerResponse}
76 * @public
77 */
78
79res.links = function(links){
80 var link = this.get('Link') || '';
81 if (link) link += ', ';
82 return this.set('Link', link + Object.keys(links).map(function(rel){
83 return '<' + links[rel] + '>; rel="' + rel + '"';
84 }).join(', '));
85};
86
87/**
88 * Send a response.
89 *
90 * Examples:
91 *
92 * res.send(new Buffer('wahoo'));
93 * res.send({ some: 'json' });
94 * res.send('<p>some html</p>');
95 *
96 * @param {string|number|boolean|object|Buffer} body
97 * @public
98 */
99
100res.send = function send(body) {
101 var chunk = body;
102 var encoding;
103 var len;
104 var req = this.req;
105 var type;
106
107 // settings
108 var app = this.app;
109
110 // allow status / body
111 if (arguments.length === 2) {
112 // res.send(body, status) backwards compat
113 if (typeof arguments[0] !== 'number' && typeof arguments[1] === 'number') {
114 deprecate('res.send(body, status): Use res.status(status).send(body) instead');
115 this.statusCode = arguments[1];
116 } else {
117 deprecate('res.send(status, body): Use res.status(status).send(body) instead');
118 this.statusCode = arguments[0];
119 chunk = arguments[1];
120 }
121 }
122
123 // disambiguate res.send(status) and res.send(status, num)
124 if (typeof chunk === 'number' && arguments.length === 1) {
125 // res.send(status) will set status message as text string
126 if (!this.get('Content-Type')) {
127 this.type('txt');
128 }
129
130 deprecate('res.send(status): Use res.sendStatus(status) instead');
131 this.statusCode = chunk;
132 chunk = statusCodes[chunk];
133 }
134
135 switch (typeof chunk) {
136 // string defaulting to html
137 case 'string':
138 if (!this.get('Content-Type')) {
139 this.type('html');
140 }
141 break;
142 case 'boolean':
143 case 'number':
144 case 'object':
145 if (chunk === null) {
146 chunk = '';
147 } else if (Buffer.isBuffer(chunk)) {
148 if (!this.get('Content-Type')) {
149 this.type('bin');
150 }
151 } else {
152 return this.json(chunk);
153 }
154 break;
155 }
156
157 // write strings in utf-8
158 if (typeof chunk === 'string') {
159 encoding = 'utf8';
160 type = this.get('Content-Type');
161
162 // reflect this in content-type
163 if (typeof type === 'string') {
164 this.set('Content-Type', setCharset(type, 'utf-8'));
165 }
166 }
167
168 // populate Content-Length
169 if (chunk !== undefined) {
170 if (!Buffer.isBuffer(chunk)) {
171 // convert chunk to Buffer; saves later double conversions
172 chunk = new Buffer(chunk, encoding);
173 encoding = undefined;
174 }
175
176 len = chunk.length;
177 this.set('Content-Length', len);
178 }
179
180 // populate ETag
181 var etag;
182 var generateETag = len !== undefined && app.get('etag fn');
183 if (typeof generateETag === 'function' && !this.get('ETag')) {
184 if ((etag = generateETag(chunk, encoding))) {
185 this.set('ETag', etag);
186 }
187 }
188
189 // freshness
190 if (req.fresh) this.statusCode = 304;
191
192 // strip irrelevant headers
193 if (204 === this.statusCode || 304 === this.statusCode) {
194 this.removeHeader('Content-Type');
195 this.removeHeader('Content-Length');
196 this.removeHeader('Transfer-Encoding');
197 chunk = '';
198 }
199
200 if (req.method === 'HEAD') {
201 // skip body for HEAD
202 this.end();
203 } else {
204 // respond
205 this.end(chunk, encoding);
206 }
207
208 return this;
209};
210
211/**
212 * Send JSON response.
213 *
214 * Examples:
215 *
216 * res.json(null);
217 * res.json({ user: 'tj' });
218 *
219 * @param {string|number|boolean|object} obj
220 * @public
221 */
222
223res.json = function json(obj) {
224 var val = obj;
225
226 // allow status / body
227 if (arguments.length === 2) {
228 // res.json(body, status) backwards compat
229 if (typeof arguments[1] === 'number') {
230 deprecate('res.json(obj, status): Use res.status(status).json(obj) instead');
231 this.statusCode = arguments[1];
232 } else {
233 deprecate('res.json(status, obj): Use res.status(status).json(obj) instead');
234 this.statusCode = arguments[0];
235 val = arguments[1];
236 }
237 }
238
239 // settings
240 var app = this.app;
241 var replacer = app.get('json replacer');
242 var spaces = app.get('json spaces');
243 var body = stringify(val, replacer, spaces);
244
245 // content-type
246 if (!this.get('Content-Type')) {
247 this.set('Content-Type', 'application/json');
248 }
249
250 return this.send(body);
251};
252
253/**
254 * Send JSON response with JSONP callback support.
255 *
256 * Examples:
257 *
258 * res.jsonp(null);
259 * res.jsonp({ user: 'tj' });
260 *
261 * @param {string|number|boolean|object} obj
262 * @public
263 */
264
265res.jsonp = function jsonp(obj) {
266 var val = obj;
267
268 // allow status / body
269 if (arguments.length === 2) {
270 // res.json(body, status) backwards compat
271 if (typeof arguments[1] === 'number') {
272 deprecate('res.jsonp(obj, status): Use res.status(status).json(obj) instead');
273 this.statusCode = arguments[1];
274 } else {
275 deprecate('res.jsonp(status, obj): Use res.status(status).jsonp(obj) instead');
276 this.statusCode = arguments[0];
277 val = arguments[1];
278 }
279 }
280
281 // settings
282 var app = this.app;
283 var replacer = app.get('json replacer');
284 var spaces = app.get('json spaces');
285 var body = stringify(val, replacer, spaces);
286 var callback = this.req.query[app.get('jsonp callback name')];
287
288 // content-type
289 if (!this.get('Content-Type')) {
290 this.set('X-Content-Type-Options', 'nosniff');
291 this.set('Content-Type', 'application/json');
292 }
293
294 // fixup callback
295 if (Array.isArray(callback)) {
296 callback = callback[0];
297 }
298
299 // jsonp
300 if (typeof callback === 'string' && callback.length !== 0) {
301 this.charset = 'utf-8';
302 this.set('X-Content-Type-Options', 'nosniff');
303 this.set('Content-Type', 'text/javascript');
304
305 // restrict callback charset
306 callback = callback.replace(/[^\[\]\w$.]/g, '');
307
308 // replace chars not allowed in JavaScript that are in JSON
309 body = body
310 .replace(/\u2028/g, '\\u2028')
311 .replace(/\u2029/g, '\\u2029');
312
313 // the /**/ is a specific security mitigation for "Rosetta Flash JSONP abuse"
314 // the typeof check is just to reduce client error noise
315 body = '/**/ typeof ' + callback + ' === \'function\' && ' + callback + '(' + body + ');';
316 }
317
318 return this.send(body);
319};
320
321/**
322 * Send given HTTP status code.
323 *
324 * Sets the response status to `statusCode` and the body of the
325 * response to the standard description from node's http.STATUS_CODES
326 * or the statusCode number if no description.
327 *
328 * Examples:
329 *
330 * res.sendStatus(200);
331 *
332 * @param {number} statusCode
333 * @public
334 */
335
336res.sendStatus = function sendStatus(statusCode) {
337 var body = statusCodes[statusCode] || String(statusCode);
338
339 this.statusCode = statusCode;
340 this.type('txt');
341
342 return this.send(body);
343};
344
345/**
346 * Transfer the file at the given `path`.
347 *
348 * Automatically sets the _Content-Type_ response header field.
349 * The callback `callback(err)` is invoked when the transfer is complete
350 * or when an error occurs. Be sure to check `res.sentHeader`
351 * if you wish to attempt responding, as the header and some data
352 * may have already been transferred.
353 *
354 * Options:
355 *
356 * - `maxAge` defaulting to 0 (can be string converted by `ms`)
357 * - `root` root directory for relative filenames
358 * - `headers` object of headers to serve with file
359 * - `dotfiles` serve dotfiles, defaulting to false; can be `"allow"` to send them
360 *
361 * Other options are passed along to `send`.
362 *
363 * Examples:
364 *
365 * The following example illustrates how `res.sendFile()` may
366 * be used as an alternative for the `static()` middleware for
367 * dynamic situations. The code backing `res.sendFile()` is actually
368 * the same code, so HTTP cache support etc is identical.
369 *
370 * app.get('/user/:uid/photos/:file', function(req, res){
371 * var uid = req.params.uid
372 * , file = req.params.file;
373 *
374 * req.user.mayViewFilesFrom(uid, function(yes){
375 * if (yes) {
376 * res.sendFile('/uploads/' + uid + '/' + file);
377 * } else {
378 * res.send(403, 'Sorry! you cant see that.');
379 * }
380 * });
381 * });
382 *
383 * @public
384 */
385
386res.sendFile = function sendFile(path, options, callback) {
387 var done = callback;
388 var req = this.req;
389 var res = this;
390 var next = req.next;
391 var opts = options || {};
392
393 if (!path) {
394 throw new TypeError('path argument is required to res.sendFile');
395 }
396
397 // support function as second arg
398 if (typeof options === 'function') {
399 done = options;
400 opts = {};
401 }
402
403 if (!opts.root && !isAbsolute(path)) {
404 throw new TypeError('path must be absolute or specify root to res.sendFile');
405 }
406
407 // create file stream
408 var pathname = encodeURI(path);
409 var file = send(req, pathname, opts);
410
411 // transfer
412 sendfile(res, file, opts, function (err) {
413 if (done) return done(err);
414 if (err && err.code === 'EISDIR') return next();
415
416 // next() all but write errors
417 if (err && err.code !== 'ECONNABORTED' && err.syscall !== 'write') {
418 next(err);
419 }
420 });
421};
422
423/**
424 * Transfer the file at the given `path`.
425 *
426 * Automatically sets the _Content-Type_ response header field.
427 * The callback `callback(err)` is invoked when the transfer is complete
428 * or when an error occurs. Be sure to check `res.sentHeader`
429 * if you wish to attempt responding, as the header and some data
430 * may have already been transferred.
431 *
432 * Options:
433 *
434 * - `maxAge` defaulting to 0 (can be string converted by `ms`)
435 * - `root` root directory for relative filenames
436 * - `headers` object of headers to serve with file
437 * - `dotfiles` serve dotfiles, defaulting to false; can be `"allow"` to send them
438 *
439 * Other options are passed along to `send`.
440 *
441 * Examples:
442 *
443 * The following example illustrates how `res.sendfile()` may
444 * be used as an alternative for the `static()` middleware for
445 * dynamic situations. The code backing `res.sendfile()` is actually
446 * the same code, so HTTP cache support etc is identical.
447 *
448 * app.get('/user/:uid/photos/:file', function(req, res){
449 * var uid = req.params.uid
450 * , file = req.params.file;
451 *
452 * req.user.mayViewFilesFrom(uid, function(yes){
453 * if (yes) {
454 * res.sendfile('/uploads/' + uid + '/' + file);
455 * } else {
456 * res.send(403, 'Sorry! you cant see that.');
457 * }
458 * });
459 * });
460 *
461 * @public
462 */
463
464res.sendfile = function (path, options, callback) {
465 var done = callback;
466 var req = this.req;
467 var res = this;
468 var next = req.next;
469 var opts = options || {};
470
471 // support function as second arg
472 if (typeof options === 'function') {
473 done = options;
474 opts = {};
475 }
476
477 // create file stream
478 var file = send(req, path, opts);
479
480 // transfer
481 sendfile(res, file, opts, function (err) {
482 if (done) return done(err);
483 if (err && err.code === 'EISDIR') return next();
484
485 // next() all but write errors
486 if (err && err.code !== 'ECONNABORT' && err.syscall !== 'write') {
487 next(err);
488 }
489 });
490};
491
492res.sendfile = deprecate.function(res.sendfile,
493 'res.sendfile: Use res.sendFile instead');
494
495/**
496 * Transfer the file at the given `path` as an attachment.
497 *
498 * Optionally providing an alternate attachment `filename`,
499 * and optional callback `callback(err)`. The callback is invoked
500 * when the data transfer is complete, or when an error has
501 * ocurred. Be sure to check `res.headersSent` if you plan to respond.
502 *
503 * This method uses `res.sendfile()`.
504 *
505 * @public
506 */
507
508res.download = function download(path, filename, callback) {
509 var done = callback;
510 var name = filename;
511
512 // support function as second arg
513 if (typeof filename === 'function') {
514 done = filename;
515 name = null;
516 }
517
518 // set Content-Disposition when file is sent
519 var headers = {
520 'Content-Disposition': contentDisposition(name || path)
521 };
522
523 // Resolve the full path for sendFile
524 var fullPath = resolve(path);
525
526 return this.sendFile(fullPath, { headers: headers }, done);
527};
528
529/**
530 * Set _Content-Type_ response header with `type` through `mime.lookup()`
531 * when it does not contain "/", or set the Content-Type to `type` otherwise.
532 *
533 * Examples:
534 *
535 * res.type('.html');
536 * res.type('html');
537 * res.type('json');
538 * res.type('application/json');
539 * res.type('png');
540 *
541 * @param {String} type
542 * @return {ServerResponse} for chaining
543 * @public
544 */
545
546res.contentType =
547res.type = function contentType(type) {
548 var ct = type.indexOf('/') === -1
549 ? mime.lookup(type)
550 : type;
551
552 return this.set('Content-Type', ct);
553};
554
555/**
556 * Respond to the Acceptable formats using an `obj`
557 * of mime-type callbacks.
558 *
559 * This method uses `req.accepted`, an array of
560 * acceptable types ordered by their quality values.
561 * When "Accept" is not present the _first_ callback
562 * is invoked, otherwise the first match is used. When
563 * no match is performed the server responds with
564 * 406 "Not Acceptable".
565 *
566 * Content-Type is set for you, however if you choose
567 * you may alter this within the callback using `res.type()`
568 * or `res.set('Content-Type', ...)`.
569 *
570 * res.format({
571 * 'text/plain': function(){
572 * res.send('hey');
573 * },
574 *
575 * 'text/html': function(){
576 * res.send('<p>hey</p>');
577 * },
578 *
579 * 'appliation/json': function(){
580 * res.send({ message: 'hey' });
581 * }
582 * });
583 *
584 * In addition to canonicalized MIME types you may
585 * also use extnames mapped to these types:
586 *
587 * res.format({
588 * text: function(){
589 * res.send('hey');
590 * },
591 *
592 * html: function(){
593 * res.send('<p>hey</p>');
594 * },
595 *
596 * json: function(){
597 * res.send({ message: 'hey' });
598 * }
599 * });
600 *
601 * By default Express passes an `Error`
602 * with a `.status` of 406 to `next(err)`
603 * if a match is not made. If you provide
604 * a `.default` callback it will be invoked
605 * instead.
606 *
607 * @param {Object} obj
608 * @return {ServerResponse} for chaining
609 * @public
610 */
611
612res.format = function(obj){
613 var req = this.req;
614 var next = req.next;
615
616 var fn = obj.default;
617 if (fn) delete obj.default;
618 var keys = Object.keys(obj);
619
620 var key = keys.length > 0
621 ? req.accepts(keys)
622 : false;
623
624 this.vary("Accept");
625
626 if (key) {
627 this.set('Content-Type', normalizeType(key).value);
628 obj[key](req, this, next);
629 } else if (fn) {
630 fn();
631 } else {
632 var err = new Error('Not Acceptable');
633 err.status = err.statusCode = 406;
634 err.types = normalizeTypes(keys).map(function(o){ return o.value });
635 next(err);
636 }
637
638 return this;
639};
640
641/**
642 * Set _Content-Disposition_ header to _attachment_ with optional `filename`.
643 *
644 * @param {String} filename
645 * @return {ServerResponse}
646 * @public
647 */
648
649res.attachment = function attachment(filename) {
650 if (filename) {
651 this.type(extname(filename));
652 }
653
654 this.set('Content-Disposition', contentDisposition(filename));
655
656 return this;
657};
658
659/**
660 * Append additional header `field` with value `val`.
661 *
662 * Example:
663 *
664 * res.append('Link', ['<http://localhost/>', '<http://localhost:3000/>']);
665 * res.append('Set-Cookie', 'foo=bar; Path=/; HttpOnly');
666 * res.append('Warning', '199 Miscellaneous warning');
667 *
668 * @param {String} field
669 * @param {String|Array} val
670 * @return {ServerResponse} for chaining
671 * @public
672 */
673
674res.append = function append(field, val) {
675 var prev = this.get(field);
676 var value = val;
677
678 if (prev) {
679 // concat the new and prev vals
680 value = Array.isArray(prev) ? prev.concat(val)
681 : Array.isArray(val) ? [prev].concat(val)
682 : [prev, val];
683 }
684
685 return this.set(field, value);
686};
687
688/**
689 * Set header `field` to `val`, or pass
690 * an object of header fields.
691 *
692 * Examples:
693 *
694 * res.set('Foo', ['bar', 'baz']);
695 * res.set('Accept', 'application/json');
696 * res.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' });
697 *
698 * Aliased as `res.header()`.
699 *
700 * @param {String|Object} field
701 * @param {String|Array} val
702 * @return {ServerResponse} for chaining
703 * @public
704 */
705
706res.set =
707res.header = function header(field, val) {
708 if (arguments.length === 2) {
709 var value = Array.isArray(val)
710 ? val.map(String)
711 : String(val);
712
713 // add charset to content-type
714 if (field.toLowerCase() === 'content-type' && !charsetRegExp.test(value)) {
715 var charset = mime.charsets.lookup(value.split(';')[0]);
716 if (charset) value += '; charset=' + charset.toLowerCase();
717 }
718
719 this.setHeader(field, value);
720 } else {
721 for (var key in field) {
722 this.set(key, field[key]);
723 }
724 }
725 return this;
726};
727
728/**
729 * Get value for header `field`.
730 *
731 * @param {String} field
732 * @return {String}
733 * @public
734 */
735
736res.get = function(field){
737 return this.getHeader(field);
738};
739
740/**
741 * Clear cookie `name`.
742 *
743 * @param {String} name
744 * @param {Object} [options]
745 * @return {ServerResponse} for chaining
746 * @public
747 */
748
749res.clearCookie = function clearCookie(name, options) {
750 var opts = merge({ expires: new Date(1), path: '/' }, options);
751
752 return this.cookie(name, '', opts);
753};
754
755/**
756 * Set cookie `name` to `value`, with the given `options`.
757 *
758 * Options:
759 *
760 * - `maxAge` max-age in milliseconds, converted to `expires`
761 * - `signed` sign the cookie
762 * - `path` defaults to "/"
763 *
764 * Examples:
765 *
766 * // "Remember Me" for 15 minutes
767 * res.cookie('rememberme', '1', { expires: new Date(Date.now() + 900000), httpOnly: true });
768 *
769 * // save as above
770 * res.cookie('rememberme', '1', { maxAge: 900000, httpOnly: true })
771 *
772 * @param {String} name
773 * @param {String|Object} value
774 * @param {Options} options
775 * @return {ServerResponse} for chaining
776 * @public
777 */
778
779res.cookie = function (name, value, options) {
780 var opts = merge({}, options);
781 var secret = this.req.secret;
782 var signed = opts.signed;
783
784 if (signed && !secret) {
785 throw new Error('cookieParser("secret") required for signed cookies');
786 }
787
788 var val = typeof value === 'object'
789 ? 'j:' + JSON.stringify(value)
790 : String(value);
791
792 if (signed) {
793 val = 's:' + sign(val, secret);
794 }
795
796 if ('maxAge' in opts) {
797 opts.expires = new Date(Date.now() + opts.maxAge);
798 opts.maxAge /= 1000;
799 }
800
801 if (opts.path == null) {
802 opts.path = '/';
803 }
804
805 this.append('Set-Cookie', cookie.serialize(name, String(val), opts));
806
807 return this;
808};
809
810/**
811 * Set the location header to `url`.
812 *
813 * The given `url` can also be "back", which redirects
814 * to the _Referrer_ or _Referer_ headers or "/".
815 *
816 * Examples:
817 *
818 * res.location('/foo/bar').;
819 * res.location('http://example.com');
820 * res.location('../login');
821 *
822 * @param {String} url
823 * @return {ServerResponse} for chaining
824 * @public
825 */
826
827res.location = function location(url) {
828 var loc = url;
829
830 // "back" is an alias for the referrer
831 if (url === 'back') {
832 loc = this.req.get('Referrer') || '/';
833 }
834
835 // set location
836 return this.set('Location', encodeUrl(loc));
837};
838
839/**
840 * Redirect to the given `url` with optional response `status`
841 * defaulting to 302.
842 *
843 * The resulting `url` is determined by `res.location()`, so
844 * it will play nicely with mounted apps, relative paths,
845 * `"back"` etc.
846 *
847 * Examples:
848 *
849 * res.redirect('/foo/bar');
850 * res.redirect('http://example.com');
851 * res.redirect(301, 'http://example.com');
852 * res.redirect('../login'); // /blog/post/1 -> /blog/login
853 *
854 * @public
855 */
856
857res.redirect = function redirect(url) {
858 var address = url;
859 var body;
860 var status = 302;
861
862 // allow status / url
863 if (arguments.length === 2) {
864 if (typeof arguments[0] === 'number') {
865 status = arguments[0];
866 address = arguments[1];
867 } else {
868 deprecate('res.redirect(url, status): Use res.redirect(status, url) instead');
869 status = arguments[1];
870 }
871 }
872
873 // Set location header
874 address = this.location(address).get('Location');
875
876 // Support text/{plain,html} by default
877 this.format({
878 text: function(){
879 body = statusCodes[status] + '. Redirecting to ' + address;
880 },
881
882 html: function(){
883 var u = escapeHtml(address);
884 body = '<p>' + statusCodes[status] + '. Redirecting to <a href="' + u + '">' + u + '</a></p>';
885 },
886
887 default: function(){
888 body = '';
889 }
890 });
891
892 // Respond
893 this.statusCode = status;
894 this.set('Content-Length', Buffer.byteLength(body));
895
896 if (this.req.method === 'HEAD') {
897 this.end();
898 } else {
899 this.end(body);
900 }
901};
902
903/**
904 * Add `field` to Vary. If already present in the Vary set, then
905 * this call is simply ignored.
906 *
907 * @param {Array|String} field
908 * @return {ServerResponse} for chaining
909 * @public
910 */
911
912res.vary = function(field){
913 // checks for back-compat
914 if (!field || (Array.isArray(field) && !field.length)) {
915 deprecate('res.vary(): Provide a field name');
916 return this;
917 }
918
919 vary(this, field);
920
921 return this;
922};
923
924/**
925 * Render `view` with the given `options` and optional callback `fn`.
926 * When a callback function is given a response will _not_ be made
927 * automatically, otherwise a response of _200_ and _text/html_ is given.
928 *
929 * Options:
930 *
931 * - `cache` boolean hinting to the engine it should cache
932 * - `filename` filename of the view being rendered
933 *
934 * @public
935 */
936
937res.render = function render(view, options, callback) {
938 var app = this.req.app;
939 var done = callback;
940 var opts = options || {};
941 var req = this.req;
942 var self = this;
943
944 // support callback function as second arg
945 if (typeof options === 'function') {
946 done = options;
947 opts = {};
948 }
949
950 // merge res.locals
951 opts._locals = self.locals;
952
953 // default callback to respond
954 done = done || function (err, str) {
955 if (err) return req.next(err);
956 self.send(str);
957 };
958
959 // render
960 app.render(view, opts, done);
961};
962
963// pipe the send file stream
964function sendfile(res, file, options, callback) {
965 var done = false;
966 var streaming;
967
968 // request aborted
969 function onaborted() {
970 if (done) return;
971 done = true;
972
973 var err = new Error('Request aborted');
974 err.code = 'ECONNABORTED';
975 callback(err);
976 }
977
978 // directory
979 function ondirectory() {
980 if (done) return;
981 done = true;
982
983 var err = new Error('EISDIR, read');
984 err.code = 'EISDIR';
985 callback(err);
986 }
987
988 // errors
989 function onerror(err) {
990 if (done) return;
991 done = true;
992 callback(err);
993 }
994
995 // ended
996 function onend() {
997 if (done) return;
998 done = true;
999 callback();
1000 }
1001
1002 // file
1003 function onfile() {
1004 streaming = false;
1005 }
1006
1007 // finished
1008 function onfinish(err) {
1009 if (err && err.code === 'ECONNRESET') return onaborted();
1010 if (err) return onerror(err);
1011 if (done) return;
1012
1013 setImmediate(function () {
1014 if (streaming !== false && !done) {
1015 onaborted();
1016 return;
1017 }
1018
1019 if (done) return;
1020 done = true;
1021 callback();
1022 });
1023 }
1024
1025 // streaming
1026 function onstream() {
1027 streaming = true;
1028 }
1029
1030 file.on('directory', ondirectory);
1031 file.on('end', onend);
1032 file.on('error', onerror);
1033 file.on('file', onfile);
1034 file.on('stream', onstream);
1035 onFinished(res, onfinish);
1036
1037 if (options.headers) {
1038 // set headers on successful transfer
1039 file.on('headers', function headers(res) {
1040 var obj = options.headers;
1041 var keys = Object.keys(obj);
1042
1043 for (var i = 0; i < keys.length; i++) {
1044 var k = keys[i];
1045 res.setHeader(k, obj[k]);
1046 }
1047 });
1048 }
1049
1050 // pipe
1051 file.pipe(res);
1052}
1053
1054/**
1055 * Stringify JSON, like JSON.stringify, but v8 optimized.
1056 * @private
1057 */
1058
1059function stringify(value, replacer, spaces) {
1060 // v8 checks arguments.length for optimizing simple call
1061 // https://bugs.chromium.org/p/v8/issues/detail?id=4730
1062 return replacer || spaces
1063 ? JSON.stringify(value, replacer, spaces)
1064 : JSON.stringify(value);
1065}