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