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