Support proper 205 responses using res.send
[express.git] / lib / response.js
blob9cf3d52be52195acbf16c85c047d993e5948fb07
1 /*!
2  * express
3  * Copyright(c) 2009-2013 TJ Holowaychuk
4  * Copyright(c) 2014-2015 Douglas Christopher Wilson
5  * MIT Licensed
6  */
8 'use strict';
10 /**
11  * Module dependencies.
12  * @private
13  */
15 var Buffer = require('safe-buffer').Buffer
16 var contentDisposition = require('content-disposition');
17 var deprecate = require('depd')('express');
18 var encodeUrl = require('encodeurl');
19 var escapeHtml = require('escape-html');
20 var http = require('http');
21 var isAbsolute = require('./utils').isAbsolute;
22 var onFinished = require('on-finished');
23 var path = require('path');
24 var statuses = require('statuses')
25 var merge = require('utils-merge');
26 var sign = require('cookie-signature').sign;
27 var normalizeType = require('./utils').normalizeType;
28 var normalizeTypes = require('./utils').normalizeTypes;
29 var setCharset = require('./utils').setCharset;
30 var cookie = require('cookie');
31 var send = require('send');
32 var extname = path.extname;
33 var mime = send.mime;
34 var resolve = path.resolve;
35 var vary = require('vary');
37 /**
38  * Response prototype.
39  * @public
40  */
42 var res = Object.create(http.ServerResponse.prototype)
44 /**
45  * Module exports.
46  * @public
47  */
49 module.exports = res
51 /**
52  * Module variables.
53  * @private
54  */
56 var charsetRegExp = /;\s*charset\s*=/;
58 /**
59  * Set status `code`.
60  *
61  * @param {Number} code
62  * @return {ServerResponse}
63  * @public
64  */
66 res.status = function status(code) {
67   this.statusCode = code;
68   return this;
71 /**
72  * Set Link header field with the given `links`.
73  *
74  * Examples:
75  *
76  *    res.links({
77  *      next: 'http://api.example.com/users?page=2',
78  *      last: 'http://api.example.com/users?page=5'
79  *    });
80  *
81  * @param {Object} links
82  * @return {ServerResponse}
83  * @public
84  */
86 res.links = function(links){
87   var link = this.get('Link') || '';
88   if (link) link += ', ';
89   return this.set('Link', link + Object.keys(links).map(function(rel){
90     return '<' + links[rel] + '>; rel="' + rel + '"';
91   }).join(', '));
94 /**
95  * Send a response.
96  *
97  * Examples:
98  *
99  *     res.send(Buffer.from('wahoo'));
100  *     res.send({ some: 'json' });
101  *     res.send('<p>some html</p>');
103  * @param {string|number|boolean|object|Buffer} body
104  * @public
105  */
107 res.send = function send(body) {
108   var chunk = body;
109   var encoding;
110   var req = this.req;
111   var type;
113   // settings
114   var app = this.app;
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   }
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     }
136     deprecate('res.send(status): Use res.sendStatus(status) instead');
137     this.statusCode = chunk;
138     chunk = statuses[chunk]
139   }
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   }
163   // write strings in utf-8
164   if (typeof chunk === 'string') {
165     encoding = 'utf8';
166     type = this.get('Content-Type');
168     // reflect this in content-type
169     if (typeof type === 'string') {
170       this.set('Content-Type', setCharset(type, 'utf-8'));
171     }
172   }
174   // determine if ETag should be generated
175   var etagFn = app.get('etag fn')
176   var generateETag = !this.get('ETag') && typeof etagFn === 'function'
178   // populate Content-Length
179   var len
180   if (chunk !== undefined) {
181     if (Buffer.isBuffer(chunk)) {
182       // get length of Buffer
183       len = chunk.length
184     } else if (!generateETag && chunk.length < 1000) {
185       // just calculate length when no ETag + small chunk
186       len = Buffer.byteLength(chunk, encoding)
187     } else {
188       // convert chunk to Buffer and calculate
189       chunk = Buffer.from(chunk, encoding)
190       encoding = undefined;
191       len = chunk.length
192     }
194     this.set('Content-Length', len);
195   }
197   // populate ETag
198   var etag;
199   if (generateETag && len !== undefined) {
200     if ((etag = etagFn(chunk, encoding))) {
201       this.set('ETag', etag);
202     }
203   }
205   // freshness
206   if (req.fresh) this.statusCode = 304;
208   // strip irrelevant headers
209   if (204 === this.statusCode || 304 === this.statusCode) {
210     this.removeHeader('Content-Type');
211     this.removeHeader('Content-Length');
212     this.removeHeader('Transfer-Encoding');
213     chunk = '';
214   }
216   // alter headers for 205
217   if (this.statusCode === 205) {
218     this.set('Content-Length', '0')
219     this.removeHeader('Transfer-Encoding')
220     chunk = ''
221   }
223   if (req.method === 'HEAD') {
224     // skip body for HEAD
225     this.end();
226   } else {
227     // respond
228     this.end(chunk, encoding);
229   }
231   return this;
235  * Send JSON response.
237  * Examples:
239  *     res.json(null);
240  *     res.json({ user: 'tj' });
242  * @param {string|number|boolean|object} obj
243  * @public
244  */
246 res.json = function json(obj) {
247   var val = obj;
249   // allow status / body
250   if (arguments.length === 2) {
251     // res.json(body, status) backwards compat
252     if (typeof arguments[1] === 'number') {
253       deprecate('res.json(obj, status): Use res.status(status).json(obj) instead');
254       this.statusCode = arguments[1];
255     } else {
256       deprecate('res.json(status, obj): Use res.status(status).json(obj) instead');
257       this.statusCode = arguments[0];
258       val = arguments[1];
259     }
260   }
262   // settings
263   var app = this.app;
264   var escape = app.get('json escape')
265   var replacer = app.get('json replacer');
266   var spaces = app.get('json spaces');
267   var body = stringify(val, replacer, spaces, escape)
269   // content-type
270   if (!this.get('Content-Type')) {
271     this.set('Content-Type', 'application/json');
272   }
274   return this.send(body);
278  * Send JSON response with JSONP callback support.
280  * Examples:
282  *     res.jsonp(null);
283  *     res.jsonp({ user: 'tj' });
285  * @param {string|number|boolean|object} obj
286  * @public
287  */
289 res.jsonp = function jsonp(obj) {
290   var val = obj;
292   // allow status / body
293   if (arguments.length === 2) {
294     // res.jsonp(body, status) backwards compat
295     if (typeof arguments[1] === 'number') {
296       deprecate('res.jsonp(obj, status): Use res.status(status).jsonp(obj) instead');
297       this.statusCode = arguments[1];
298     } else {
299       deprecate('res.jsonp(status, obj): Use res.status(status).jsonp(obj) instead');
300       this.statusCode = arguments[0];
301       val = arguments[1];
302     }
303   }
305   // settings
306   var app = this.app;
307   var escape = app.get('json escape')
308   var replacer = app.get('json replacer');
309   var spaces = app.get('json spaces');
310   var body = stringify(val, replacer, spaces, escape)
311   var callback = this.req.query[app.get('jsonp callback name')];
313   // content-type
314   if (!this.get('Content-Type')) {
315     this.set('X-Content-Type-Options', 'nosniff');
316     this.set('Content-Type', 'application/json');
317   }
319   // fixup callback
320   if (Array.isArray(callback)) {
321     callback = callback[0];
322   }
324   // jsonp
325   if (typeof callback === 'string' && callback.length !== 0) {
326     this.set('X-Content-Type-Options', 'nosniff');
327     this.set('Content-Type', 'text/javascript');
329     // restrict callback charset
330     callback = callback.replace(/[^\[\]\w$.]/g, '');
332     if (body === undefined) {
333       // empty argument
334       body = ''
335     } else if (typeof body === 'string') {
336       // replace chars not allowed in JavaScript that are in JSON
337       body = body
338         .replace(/\u2028/g, '\\u2028')
339         .replace(/\u2029/g, '\\u2029')
340     }
342     // the /**/ is a specific security mitigation for "Rosetta Flash JSONP abuse"
343     // the typeof check is just to reduce client error noise
344     body = '/**/ typeof ' + callback + ' === \'function\' && ' + callback + '(' + body + ');';
345   }
347   return this.send(body);
351  * Send given HTTP status code.
353  * Sets the response status to `statusCode` and the body of the
354  * response to the standard description from node's http.STATUS_CODES
355  * or the statusCode number if no description.
357  * Examples:
359  *     res.sendStatus(200);
361  * @param {number} statusCode
362  * @public
363  */
365 res.sendStatus = function sendStatus(statusCode) {
366   var body = statuses[statusCode] || String(statusCode)
368   this.statusCode = statusCode;
369   this.type('txt');
371   return this.send(body);
375  * Transfer the file at the given `path`.
377  * Automatically sets the _Content-Type_ response header field.
378  * The callback `callback(err)` is invoked when the transfer is complete
379  * or when an error occurs. Be sure to check `res.headersSent`
380  * if you wish to attempt responding, as the header and some data
381  * may have already been transferred.
383  * Options:
385  *   - `maxAge`   defaulting to 0 (can be string converted by `ms`)
386  *   - `root`     root directory for relative filenames
387  *   - `headers`  object of headers to serve with file
388  *   - `dotfiles` serve dotfiles, defaulting to false; can be `"allow"` to send them
390  * Other options are passed along to `send`.
392  * Examples:
394  *  The following example illustrates how `res.sendFile()` may
395  *  be used as an alternative for the `static()` middleware for
396  *  dynamic situations. The code backing `res.sendFile()` is actually
397  *  the same code, so HTTP cache support etc is identical.
399  *     app.get('/user/:uid/photos/:file', function(req, res){
400  *       var uid = req.params.uid
401  *         , file = req.params.file;
403  *       req.user.mayViewFilesFrom(uid, function(yes){
404  *         if (yes) {
405  *           res.sendFile('/uploads/' + uid + '/' + file);
406  *         } else {
407  *           res.send(403, 'Sorry! you cant see that.');
408  *         }
409  *       });
410  *     });
412  * @public
413  */
415 res.sendFile = function sendFile(path, options, callback) {
416   var done = callback;
417   var req = this.req;
418   var res = this;
419   var next = req.next;
420   var opts = options || {};
422   if (!path) {
423     throw new TypeError('path argument is required to res.sendFile');
424   }
426   if (typeof path !== 'string') {
427     throw new TypeError('path must be a string to res.sendFile')
428   }
430   // support function as second arg
431   if (typeof options === 'function') {
432     done = options;
433     opts = {};
434   }
436   if (!opts.root && !isAbsolute(path)) {
437     throw new TypeError('path must be absolute or specify root to res.sendFile');
438   }
440   // create file stream
441   var pathname = encodeURI(path);
442   var file = send(req, pathname, opts);
444   // transfer
445   sendfile(res, file, opts, function (err) {
446     if (done) return done(err);
447     if (err && err.code === 'EISDIR') return next();
449     // next() all but write errors
450     if (err && err.code !== 'ECONNABORTED' && err.syscall !== 'write') {
451       next(err);
452     }
453   });
457  * Transfer the file at the given `path`.
459  * Automatically sets the _Content-Type_ response header field.
460  * The callback `callback(err)` is invoked when the transfer is complete
461  * or when an error occurs. Be sure to check `res.headersSent`
462  * if you wish to attempt responding, as the header and some data
463  * may have already been transferred.
465  * Options:
467  *   - `maxAge`   defaulting to 0 (can be string converted by `ms`)
468  *   - `root`     root directory for relative filenames
469  *   - `headers`  object of headers to serve with file
470  *   - `dotfiles` serve dotfiles, defaulting to false; can be `"allow"` to send them
472  * Other options are passed along to `send`.
474  * Examples:
476  *  The following example illustrates how `res.sendfile()` may
477  *  be used as an alternative for the `static()` middleware for
478  *  dynamic situations. The code backing `res.sendfile()` is actually
479  *  the same code, so HTTP cache support etc is identical.
481  *     app.get('/user/:uid/photos/:file', function(req, res){
482  *       var uid = req.params.uid
483  *         , file = req.params.file;
485  *       req.user.mayViewFilesFrom(uid, function(yes){
486  *         if (yes) {
487  *           res.sendfile('/uploads/' + uid + '/' + file);
488  *         } else {
489  *           res.send(403, 'Sorry! you cant see that.');
490  *         }
491  *       });
492  *     });
494  * @public
495  */
497 res.sendfile = function (path, options, callback) {
498   var done = callback;
499   var req = this.req;
500   var res = this;
501   var next = req.next;
502   var opts = options || {};
504   // support function as second arg
505   if (typeof options === 'function') {
506     done = options;
507     opts = {};
508   }
510   // create file stream
511   var file = send(req, path, opts);
513   // transfer
514   sendfile(res, file, opts, function (err) {
515     if (done) return done(err);
516     if (err && err.code === 'EISDIR') return next();
518     // next() all but write errors
519     if (err && err.code !== 'ECONNABORTED' && err.syscall !== 'write') {
520       next(err);
521     }
522   });
525 res.sendfile = deprecate.function(res.sendfile,
526   'res.sendfile: Use res.sendFile instead');
529  * Transfer the file at the given `path` as an attachment.
531  * Optionally providing an alternate attachment `filename`,
532  * and optional callback `callback(err)`. The callback is invoked
533  * when the data transfer is complete, or when an error has
534  * occurred. Be sure to check `res.headersSent` if you plan to respond.
536  * Optionally providing an `options` object to use with `res.sendFile()`.
537  * This function will set the `Content-Disposition` header, overriding
538  * any `Content-Disposition` header passed as header options in order
539  * to set the attachment and filename.
541  * This method uses `res.sendFile()`.
543  * @public
544  */
546 res.download = function download (path, filename, options, callback) {
547   var done = callback;
548   var name = filename;
549   var opts = options || null
551   // support function as second or third arg
552   if (typeof filename === 'function') {
553     done = filename;
554     name = null;
555     opts = null
556   } else if (typeof options === 'function') {
557     done = options
558     opts = null
559   }
561   // set Content-Disposition when file is sent
562   var headers = {
563     'Content-Disposition': contentDisposition(name || path)
564   };
566   // merge user-provided headers
567   if (opts && opts.headers) {
568     var keys = Object.keys(opts.headers)
569     for (var i = 0; i < keys.length; i++) {
570       var key = keys[i]
571       if (key.toLowerCase() !== 'content-disposition') {
572         headers[key] = opts.headers[key]
573       }
574     }
575   }
577   // merge user-provided options
578   opts = Object.create(opts)
579   opts.headers = headers
581   // Resolve the full path for sendFile
582   var fullPath = resolve(path);
584   // send file
585   return this.sendFile(fullPath, opts, done)
589  * Set _Content-Type_ response header with `type` through `mime.lookup()`
590  * when it does not contain "/", or set the Content-Type to `type` otherwise.
592  * Examples:
594  *     res.type('.html');
595  *     res.type('html');
596  *     res.type('json');
597  *     res.type('application/json');
598  *     res.type('png');
600  * @param {String} type
601  * @return {ServerResponse} for chaining
602  * @public
603  */
605 res.contentType =
606 res.type = function contentType(type) {
607   var ct = type.indexOf('/') === -1
608     ? mime.lookup(type)
609     : type;
611   return this.set('Content-Type', ct);
615  * Respond to the Acceptable formats using an `obj`
616  * of mime-type callbacks.
618  * This method uses `req.accepted`, an array of
619  * acceptable types ordered by their quality values.
620  * When "Accept" is not present the _first_ callback
621  * is invoked, otherwise the first match is used. When
622  * no match is performed the server responds with
623  * 406 "Not Acceptable".
625  * Content-Type is set for you, however if you choose
626  * you may alter this within the callback using `res.type()`
627  * or `res.set('Content-Type', ...)`.
629  *    res.format({
630  *      'text/plain': function(){
631  *        res.send('hey');
632  *      },
634  *      'text/html': function(){
635  *        res.send('<p>hey</p>');
636  *      },
638  *      'application/json': function () {
639  *        res.send({ message: 'hey' });
640  *      }
641  *    });
643  * In addition to canonicalized MIME types you may
644  * also use extnames mapped to these types:
646  *    res.format({
647  *      text: function(){
648  *        res.send('hey');
649  *      },
651  *      html: function(){
652  *        res.send('<p>hey</p>');
653  *      },
655  *      json: function(){
656  *        res.send({ message: 'hey' });
657  *      }
658  *    });
660  * By default Express passes an `Error`
661  * with a `.status` of 406 to `next(err)`
662  * if a match is not made. If you provide
663  * a `.default` callback it will be invoked
664  * instead.
666  * @param {Object} obj
667  * @return {ServerResponse} for chaining
668  * @public
669  */
671 res.format = function(obj){
672   var req = this.req;
673   var next = req.next;
675   var fn = obj.default;
676   if (fn) delete obj.default;
677   var keys = Object.keys(obj);
679   var key = keys.length > 0
680     ? req.accepts(keys)
681     : false;
683   this.vary("Accept");
685   if (key) {
686     this.set('Content-Type', normalizeType(key).value);
687     obj[key](req, this, next);
688   } else if (fn) {
689     fn();
690   } else {
691     var err = new Error('Not Acceptable');
692     err.status = err.statusCode = 406;
693     err.types = normalizeTypes(keys).map(function(o){ return o.value });
694     next(err);
695   }
697   return this;
701  * Set _Content-Disposition_ header to _attachment_ with optional `filename`.
703  * @param {String} filename
704  * @return {ServerResponse}
705  * @public
706  */
708 res.attachment = function attachment(filename) {
709   if (filename) {
710     this.type(extname(filename));
711   }
713   this.set('Content-Disposition', contentDisposition(filename));
715   return this;
719  * Append additional header `field` with value `val`.
721  * Example:
723  *    res.append('Link', ['<http://localhost/>', '<http://localhost:3000/>']);
724  *    res.append('Set-Cookie', 'foo=bar; Path=/; HttpOnly');
725  *    res.append('Warning', '199 Miscellaneous warning');
727  * @param {String} field
728  * @param {String|Array} val
729  * @return {ServerResponse} for chaining
730  * @public
731  */
733 res.append = function append(field, val) {
734   var prev = this.get(field);
735   var value = val;
737   if (prev) {
738     // concat the new and prev vals
739     value = Array.isArray(prev) ? prev.concat(val)
740       : Array.isArray(val) ? [prev].concat(val)
741         : [prev, val]
742   }
744   return this.set(field, value);
748  * Set header `field` to `val`, or pass
749  * an object of header fields.
751  * Examples:
753  *    res.set('Foo', ['bar', 'baz']);
754  *    res.set('Accept', 'application/json');
755  *    res.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' });
757  * Aliased as `res.header()`.
759  * @param {String|Object} field
760  * @param {String|Array} val
761  * @return {ServerResponse} for chaining
762  * @public
763  */
765 res.set =
766 res.header = function header(field, val) {
767   if (arguments.length === 2) {
768     var value = Array.isArray(val)
769       ? val.map(String)
770       : String(val);
772     // add charset to content-type
773     if (field.toLowerCase() === 'content-type') {
774       if (Array.isArray(value)) {
775         throw new TypeError('Content-Type cannot be set to an Array');
776       }
777       if (!charsetRegExp.test(value)) {
778         var charset = mime.charsets.lookup(value.split(';')[0]);
779         if (charset) value += '; charset=' + charset.toLowerCase();
780       }
781     }
783     this.setHeader(field, value);
784   } else {
785     for (var key in field) {
786       this.set(key, field[key]);
787     }
788   }
789   return this;
793  * Get value for header `field`.
795  * @param {String} field
796  * @return {String}
797  * @public
798  */
800 res.get = function(field){
801   return this.getHeader(field);
805  * Clear cookie `name`.
807  * @param {String} name
808  * @param {Object} [options]
809  * @return {ServerResponse} for chaining
810  * @public
811  */
813 res.clearCookie = function clearCookie(name, options) {
814   var opts = merge({ expires: new Date(1), path: '/' }, options);
816   return this.cookie(name, '', opts);
820  * Set cookie `name` to `value`, with the given `options`.
822  * Options:
824  *    - `maxAge`   max-age in milliseconds, converted to `expires`
825  *    - `signed`   sign the cookie
826  *    - `path`     defaults to "/"
828  * Examples:
830  *    // "Remember Me" for 15 minutes
831  *    res.cookie('rememberme', '1', { expires: new Date(Date.now() + 900000), httpOnly: true });
833  *    // same as above
834  *    res.cookie('rememberme', '1', { maxAge: 900000, httpOnly: true })
836  * @param {String} name
837  * @param {String|Object} value
838  * @param {Object} [options]
839  * @return {ServerResponse} for chaining
840  * @public
841  */
843 res.cookie = function (name, value, options) {
844   var opts = merge({}, options);
845   var secret = this.req.secret;
846   var signed = opts.signed;
848   if (signed && !secret) {
849     throw new Error('cookieParser("secret") required for signed cookies');
850   }
852   var val = typeof value === 'object'
853     ? 'j:' + JSON.stringify(value)
854     : String(value);
856   if (signed) {
857     val = 's:' + sign(val, secret);
858   }
860   if ('maxAge' in opts) {
861     opts.expires = new Date(Date.now() + opts.maxAge);
862     opts.maxAge /= 1000;
863   }
865   if (opts.path == null) {
866     opts.path = '/';
867   }
869   this.append('Set-Cookie', cookie.serialize(name, String(val), opts));
871   return this;
875  * Set the location header to `url`.
877  * The given `url` can also be "back", which redirects
878  * to the _Referrer_ or _Referer_ headers or "/".
880  * Examples:
882  *    res.location('/foo/bar').;
883  *    res.location('http://example.com');
884  *    res.location('../login');
886  * @param {String} url
887  * @return {ServerResponse} for chaining
888  * @public
889  */
891 res.location = function location(url) {
892   var loc = url;
894   // "back" is an alias for the referrer
895   if (url === 'back') {
896     loc = this.req.get('Referrer') || '/';
897   }
899   // set location
900   return this.set('Location', encodeUrl(loc));
904  * Redirect to the given `url` with optional response `status`
905  * defaulting to 302.
907  * The resulting `url` is determined by `res.location()`, so
908  * it will play nicely with mounted apps, relative paths,
909  * `"back"` etc.
911  * Examples:
913  *    res.redirect('/foo/bar');
914  *    res.redirect('http://example.com');
915  *    res.redirect(301, 'http://example.com');
916  *    res.redirect('../login'); // /blog/post/1 -> /blog/login
918  * @public
919  */
921 res.redirect = function redirect(url) {
922   var address = url;
923   var body;
924   var status = 302;
926   // allow status / url
927   if (arguments.length === 2) {
928     if (typeof arguments[0] === 'number') {
929       status = arguments[0];
930       address = arguments[1];
931     } else {
932       deprecate('res.redirect(url, status): Use res.redirect(status, url) instead');
933       status = arguments[1];
934     }
935   }
937   // Set location header
938   address = this.location(address).get('Location');
940   // Support text/{plain,html} by default
941   this.format({
942     text: function(){
943       body = statuses[status] + '. Redirecting to ' + address
944     },
946     html: function(){
947       var u = escapeHtml(address);
948       body = '<p>' + statuses[status] + '. Redirecting to <a href="' + u + '">' + u + '</a></p>'
949     },
951     default: function(){
952       body = '';
953     }
954   });
956   // Respond
957   this.statusCode = status;
958   this.set('Content-Length', Buffer.byteLength(body));
960   if (this.req.method === 'HEAD') {
961     this.end();
962   } else {
963     this.end(body);
964   }
968  * Add `field` to Vary. If already present in the Vary set, then
969  * this call is simply ignored.
971  * @param {Array|String} field
972  * @return {ServerResponse} for chaining
973  * @public
974  */
976 res.vary = function(field){
977   // checks for back-compat
978   if (!field || (Array.isArray(field) && !field.length)) {
979     deprecate('res.vary(): Provide a field name');
980     return this;
981   }
983   vary(this, field);
985   return this;
989  * Render `view` with the given `options` and optional callback `fn`.
990  * When a callback function is given a response will _not_ be made
991  * automatically, otherwise a response of _200_ and _text/html_ is given.
993  * Options:
995  *  - `cache`     boolean hinting to the engine it should cache
996  *  - `filename`  filename of the view being rendered
998  * @public
999  */
1001 res.render = function render(view, options, callback) {
1002   var app = this.req.app;
1003   var done = callback;
1004   var opts = options || {};
1005   var req = this.req;
1006   var self = this;
1008   // support callback function as second arg
1009   if (typeof options === 'function') {
1010     done = options;
1011     opts = {};
1012   }
1014   // merge res.locals
1015   opts._locals = self.locals;
1017   // default callback to respond
1018   done = done || function (err, str) {
1019     if (err) return req.next(err);
1020     self.send(str);
1021   };
1023   // render
1024   app.render(view, opts, done);
1027 // pipe the send file stream
1028 function sendfile(res, file, options, callback) {
1029   var done = false;
1030   var streaming;
1032   // request aborted
1033   function onaborted() {
1034     if (done) return;
1035     done = true;
1037     var err = new Error('Request aborted');
1038     err.code = 'ECONNABORTED';
1039     callback(err);
1040   }
1042   // directory
1043   function ondirectory() {
1044     if (done) return;
1045     done = true;
1047     var err = new Error('EISDIR, read');
1048     err.code = 'EISDIR';
1049     callback(err);
1050   }
1052   // errors
1053   function onerror(err) {
1054     if (done) return;
1055     done = true;
1056     callback(err);
1057   }
1059   // ended
1060   function onend() {
1061     if (done) return;
1062     done = true;
1063     callback();
1064   }
1066   // file
1067   function onfile() {
1068     streaming = false;
1069   }
1071   // finished
1072   function onfinish(err) {
1073     if (err && err.code === 'ECONNRESET') return onaborted();
1074     if (err) return onerror(err);
1075     if (done) return;
1077     setImmediate(function () {
1078       if (streaming !== false && !done) {
1079         onaborted();
1080         return;
1081       }
1083       if (done) return;
1084       done = true;
1085       callback();
1086     });
1087   }
1089   // streaming
1090   function onstream() {
1091     streaming = true;
1092   }
1094   file.on('directory', ondirectory);
1095   file.on('end', onend);
1096   file.on('error', onerror);
1097   file.on('file', onfile);
1098   file.on('stream', onstream);
1099   onFinished(res, onfinish);
1101   if (options.headers) {
1102     // set headers on successful transfer
1103     file.on('headers', function headers(res) {
1104       var obj = options.headers;
1105       var keys = Object.keys(obj);
1107       for (var i = 0; i < keys.length; i++) {
1108         var k = keys[i];
1109         res.setHeader(k, obj[k]);
1110       }
1111     });
1112   }
1114   // pipe
1115   file.pipe(res);
1119  * Stringify JSON, like JSON.stringify, but v8 optimized, with the
1120  * ability to escape characters that can trigger HTML sniffing.
1122  * @param {*} value
1123  * @param {function} replacer
1124  * @param {number} spaces
1125  * @param {boolean} escape
1126  * @returns {string}
1127  * @private
1128  */
1130 function stringify (value, replacer, spaces, escape) {
1131   // v8 checks arguments.length for optimizing simple call
1132   // https://bugs.chromium.org/p/v8/issues/detail?id=4730
1133   var json = replacer || spaces
1134     ? JSON.stringify(value, replacer, spaces)
1135     : JSON.stringify(value);
1137   if (escape && typeof json === 'string') {
1138     json = json.replace(/[<>&]/g, function (c) {
1139       switch (c.charCodeAt(0)) {
1140         case 0x3c:
1141           return '\\u003c'
1142         case 0x3e:
1143           return '\\u003e'
1144         case 0x26:
1145           return '\\u0026'
1146         /* istanbul ignore next: unreachable default */
1147         default:
1148           return c
1149       }
1150     })
1151   }
1153   return json