UNPKG

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