/node_modules/express/lib/response.js
JavaScript | 719 lines | 289 code | 80 blank | 350 comment | 91 complexity | fcac5a74cc92a6091e8952dacec7212c MD5 | raw file
Possible License(s): Apache-2.0, MIT
1/** 2 * Module dependencies. 3 */ 4 5var fs = require('fs') 6 , http = require('http') 7 , path = require('path') 8 , connect = require('connect') 9 , utils = connect.utils 10 , normalizeType = require('./utils').normalizeType 11 , normalizeTypes = require('./utils').normalizeTypes 12 , etag = require('./utils').etag 13 , statusCodes = http.STATUS_CODES 14 , send = connect.static.send 15 , cookie = require('cookie') 16 , send = require('send') 17 , crc = require('crc') 18 , mime = connect.mime 19 , basename = path.basename 20 , extname = path.extname 21 , join = path.join; 22 23/** 24 * Response prototype. 25 */ 26 27var res = module.exports = { 28 __proto__: http.ServerResponse.prototype 29}; 30 31/** 32 * Set status `code`. 33 * 34 * @param {Number} code 35 * @return {ServerResponse} 36 * @api public 37 */ 38 39res.status = function(code){ 40 this.statusCode = code; 41 return this; 42}; 43 44/** 45 * Set Link header field with the given `links`. 46 * 47 * Examples: 48 * 49 * res.links({ 50 * next: 'http://api.example.com/users?page=2', 51 * last: 'http://api.example.com/users?page=5' 52 * }); 53 * 54 * @param {Object} links 55 * @return {ServerResponse} 56 * @api public 57 */ 58 59res.links = function(links){ 60 return this.set('Link', Object.keys(links).map(function(rel){ 61 return '<' + links[rel] + '>; rel="' + rel + '"'; 62 }).join(', ')); 63}; 64 65/** 66 * Send a response. 67 * 68 * Examples: 69 * 70 * res.send(new Buffer('wahoo')); 71 * res.send({ some: 'json' }); 72 * res.send('<p>some html</p>'); 73 * res.send(404, 'Sorry, cant find that'); 74 * res.send(404); 75 * 76 * @param {Mixed} body or status 77 * @param {Mixed} body 78 * @return {ServerResponse} 79 * @api public 80 */ 81 82res.send = function(body){ 83 var req = this.req 84 , head = 'HEAD' == req.method 85 , len; 86 87 // allow status / body 88 if (2 == arguments.length) { 89 // res.send(body, status) backwards compat 90 if ('number' != typeof body && 'number' == typeof arguments[1]) { 91 this.statusCode = arguments[1]; 92 } else { 93 this.statusCode = body; 94 body = arguments[1]; 95 } 96 } 97 98 // convert string objects to primitives 99 if (body instanceof String) body = body.toString(); 100 101 switch (typeof body) { 102 // response status 103 case 'number': 104 this.get('Content-Type') || this.type('txt'); 105 this.statusCode = body; 106 body = http.STATUS_CODES[body]; 107 break; 108 // string defaulting to html 109 case 'string': 110 if (!this.get('Content-Type')) { 111 this.charset = this.charset || 'utf-8'; 112 this.type('html'); 113 } 114 break; 115 case 'boolean': 116 case 'object': 117 if (null == body) { 118 body = ''; 119 } else if (Buffer.isBuffer(body)) { 120 this.get('Content-Type') || this.type('bin'); 121 } else { 122 return this.json(body); 123 } 124 break; 125 } 126 127 // populate Content-Length 128 if (undefined !== body && !this.get('Content-Length')) { 129 this.set('Content-Length', len = Buffer.isBuffer(body) 130 ? body.length 131 : Buffer.byteLength(body)); 132 } 133 134 // ETag support 135 // TODO: W/ support 136 if (len > 1024) { 137 if (!this.get('ETag')) { 138 this.set('ETag', etag(body)); 139 } 140 } 141 142 // freshness 143 if (req.fresh) this.statusCode = 304; 144 145 // strip irrelevant headers 146 if (204 == this.statusCode || 304 == this.statusCode) { 147 this.removeHeader('Content-Type'); 148 this.removeHeader('Content-Length'); 149 body = ''; 150 } 151 152 // respond 153 this.end(head ? null : body); 154 return this; 155}; 156 157/** 158 * Send JSON response. 159 * 160 * Examples: 161 * 162 * res.json(null); 163 * res.json({ user: 'tj' }); 164 * res.json(500, 'oh noes!'); 165 * res.json(404, 'I dont have that'); 166 * 167 * @param {Mixed} obj or status 168 * @param {Mixed} obj 169 * @return {ServerResponse} 170 * @api public 171 */ 172 173res.json = function(obj){ 174 // allow status / body 175 if (2 == arguments.length) { 176 // res.json(body, status) backwards compat 177 if ('number' == typeof arguments[1]) { 178 this.statusCode = arguments[1]; 179 } else { 180 this.statusCode = obj; 181 obj = arguments[1]; 182 } 183 } 184 185 // settings 186 var app = this.app; 187 var replacer = app.get('json replacer'); 188 var spaces = app.get('json spaces'); 189 var body = JSON.stringify(obj, replacer, spaces); 190 191 // content-type 192 this.charset = this.charset || 'utf-8'; 193 this.set('Content-Type', 'application/json'); 194 195 return this.send(body); 196}; 197 198/** 199 * Send JSON response with JSONP callback support. 200 * 201 * Examples: 202 * 203 * res.jsonp(null); 204 * res.jsonp({ user: 'tj' }); 205 * res.jsonp(500, 'oh noes!'); 206 * res.jsonp(404, 'I dont have that'); 207 * 208 * @param {Mixed} obj or status 209 * @param {Mixed} obj 210 * @return {ServerResponse} 211 * @api public 212 */ 213 214res.jsonp = function(obj){ 215 // allow status / body 216 if (2 == arguments.length) { 217 // res.json(body, status) backwards compat 218 if ('number' == typeof arguments[1]) { 219 this.statusCode = arguments[1]; 220 } else { 221 this.statusCode = obj; 222 obj = arguments[1]; 223 } 224 } 225 226 // settings 227 var app = this.app; 228 var replacer = app.get('json replacer'); 229 var spaces = app.get('json spaces'); 230 var body = JSON.stringify(obj, replacer, spaces); 231 var callback = this.req.query[app.get('jsonp callback name')]; 232 233 // content-type 234 this.charset = this.charset || 'utf-8'; 235 this.set('Content-Type', 'application/json'); 236 237 // jsonp 238 if (callback) { 239 this.set('Content-Type', 'text/javascript'); 240 body = callback.replace(/[^\[\]\w$.]/g, '') + '(' + body + ');'; 241 } 242 243 return this.send(body); 244}; 245 246/** 247 * Transfer the file at the given `path`. 248 * 249 * Automatically sets the _Content-Type_ response header field. 250 * The callback `fn(err)` is invoked when the transfer is complete 251 * or when an error occurs. Be sure to check `res.sentHeader` 252 * if you wish to attempt responding, as the header and some data 253 * may have already been transferred. 254 * 255 * Options: 256 * 257 * - `maxAge` defaulting to 0 258 * - `root` root directory for relative filenames 259 * 260 * Examples: 261 * 262 * The following example illustrates how `res.sendfile()` may 263 * be used as an alternative for the `static()` middleware for 264 * dynamic situations. The code backing `res.sendfile()` is actually 265 * the same code, so HTTP cache support etc is identical. 266 * 267 * app.get('/user/:uid/photos/:file', function(req, res){ 268 * var uid = req.params.uid 269 * , file = req.params.file; 270 * 271 * req.user.mayViewFilesFrom(uid, function(yes){ 272 * if (yes) { 273 * res.sendfile('/uploads/' + uid + '/' + file); 274 * } else { 275 * res.send(403, 'Sorry! you cant see that.'); 276 * } 277 * }); 278 * }); 279 * 280 * @param {String} path 281 * @param {Object|Function} options or fn 282 * @param {Function} fn 283 * @api public 284 */ 285 286res.sendfile = function(path, options, fn){ 287 var self = this 288 , req = self.req 289 , next = this.req.next 290 , options = options || {} 291 , done; 292 293 // support function as second arg 294 if ('function' == typeof options) { 295 fn = options; 296 options = {}; 297 } 298 299 // socket errors 300 req.socket.on('error', error); 301 302 // errors 303 function error(err) { 304 if (done) return; 305 done = true; 306 307 // clean up 308 cleanup(); 309 if (!self.headerSent) self.removeHeader('Content-Disposition'); 310 311 // callback available 312 if (fn) return fn(err); 313 314 // list in limbo if there's no callback 315 if (self.headerSent) return; 316 317 // delegate 318 next(err); 319 } 320 321 // streaming 322 function stream() { 323 if (done) return; 324 cleanup(); 325 if (fn) self.on('finish', fn); 326 } 327 328 // cleanup 329 function cleanup() { 330 req.socket.removeListener('error', error); 331 } 332 333 // transfer 334 var file = send(req, path); 335 if (options.root) file.root(options.root); 336 file.maxage(options.maxAge || 0); 337 file.on('error', error); 338 file.on('directory', next); 339 file.on('stream', stream); 340 file.pipe(this); 341 this.on('finish', cleanup); 342}; 343 344/** 345 * Transfer the file at the given `path` as an attachment. 346 * 347 * Optionally providing an alternate attachment `filename`, 348 * and optional callback `fn(err)`. The callback is invoked 349 * when the data transfer is complete, or when an error has 350 * ocurred. Be sure to check `res.headerSent` if you plan to respond. 351 * 352 * This method uses `res.sendfile()`. 353 * 354 * @param {String} path 355 * @param {String|Function} filename or fn 356 * @param {Function} fn 357 * @api public 358 */ 359 360res.download = function(path, filename, fn){ 361 // support function as second arg 362 if ('function' == typeof filename) { 363 fn = filename; 364 filename = null; 365 } 366 367 filename = filename || path; 368 this.set('Content-Disposition', 'attachment; filename="' + basename(filename) + '"'); 369 return this.sendfile(path, fn); 370}; 371 372/** 373 * Set _Content-Type_ response header with `type` through `mime.lookup()` 374 * when it does not contain "/", or set the Content-Type to `type` otherwise. 375 * 376 * Examples: 377 * 378 * res.type('.html'); 379 * res.type('html'); 380 * res.type('json'); 381 * res.type('application/json'); 382 * res.type('png'); 383 * 384 * @param {String} type 385 * @return {ServerResponse} for chaining 386 * @api public 387 */ 388 389res.contentType = 390res.type = function(type){ 391 return this.set('Content-Type', ~type.indexOf('/') 392 ? type 393 : mime.lookup(type)); 394}; 395 396/** 397 * Respond to the Acceptable formats using an `obj` 398 * of mime-type callbacks. 399 * 400 * This method uses `req.accepted`, an array of 401 * acceptable types ordered by their quality values. 402 * When "Accept" is not present the _first_ callback 403 * is invoked, otherwise the first match is used. When 404 * no match is performed the server responds with 405 * 406 "Not Acceptable". 406 * 407 * Content-Type is set for you, however if you choose 408 * you may alter this within the callback using `res.type()` 409 * or `res.set('Content-Type', ...)`. 410 * 411 * res.format({ 412 * 'text/plain': function(){ 413 * res.send('hey'); 414 * }, 415 * 416 * 'text/html': function(){ 417 * res.send('<p>hey</p>'); 418 * }, 419 * 420 * 'appliation/json': function(){ 421 * res.send({ message: 'hey' }); 422 * } 423 * }); 424 * 425 * In addition to canonicalized MIME types you may 426 * also use extnames mapped to these types: 427 * 428 * res.format({ 429 * text: function(){ 430 * res.send('hey'); 431 * }, 432 * 433 * html: function(){ 434 * res.send('<p>hey</p>'); 435 * }, 436 * 437 * json: function(){ 438 * res.send({ message: 'hey' }); 439 * } 440 * }); 441 * 442 * By default Express passes an `Error` 443 * with a `.status` of 406 to `next(err)` 444 * if a match is not made. If you provide 445 * a `.default` callback it will be invoked 446 * instead. 447 * 448 * @param {Object} obj 449 * @return {ServerResponse} for chaining 450 * @api public 451 */ 452 453res.format = function(obj){ 454 var keys = Object.keys(obj) 455 , req = this.req 456 , next = req.next; 457 458 var fn = obj.default; 459 if (fn) delete obj.default; 460 461 var key = req.accepts(keys); 462 463 this.set('Vary', 'Accept'); 464 465 if (key) { 466 this.set('Content-Type', normalizeType(key)); 467 obj[key](req, this, next); 468 } else if (fn) { 469 fn(); 470 } else { 471 var err = new Error('Not Acceptable'); 472 err.status = 406; 473 err.types = normalizeTypes(keys); 474 next(err); 475 } 476 477 return this; 478}; 479 480/** 481 * Set _Content-Disposition_ header to _attachment_ with optional `filename`. 482 * 483 * @param {String} filename 484 * @return {ServerResponse} 485 * @api public 486 */ 487 488res.attachment = function(filename){ 489 if (filename) this.type(extname(filename)); 490 this.set('Content-Disposition', filename 491 ? 'attachment; filename="' + basename(filename) + '"' 492 : 'attachment'); 493 return this; 494}; 495 496/** 497 * Set header `field` to `val`, or pass 498 * an object of header fields. 499 * 500 * Examples: 501 * 502 * res.set('Accept', 'application/json'); 503 * res.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' }); 504 * 505 * Aliased as `res.header()`. 506 * 507 * @param {String|Object} field 508 * @param {String} val 509 * @return {ServerResponse} for chaining 510 * @api public 511 */ 512 513res.set = 514res.header = function(field, val){ 515 if (2 == arguments.length) { 516 this.setHeader(field, '' + val); 517 } else { 518 for (var key in field) { 519 this.setHeader(key, '' + field[key]); 520 } 521 } 522 return this; 523}; 524 525/** 526 * Get value for header `field`. 527 * 528 * @param {String} field 529 * @return {String} 530 * @api public 531 */ 532 533res.get = function(field){ 534 return this.getHeader(field); 535}; 536 537/** 538 * Clear cookie `name`. 539 * 540 * @param {String} name 541 * @param {Object} options 542 * @param {ServerResponse} for chaining 543 * @api public 544 */ 545 546res.clearCookie = function(name, options){ 547 var opts = { expires: new Date(1), path: '/' }; 548 return this.cookie(name, '', options 549 ? utils.merge(opts, options) 550 : opts); 551}; 552 553/** 554 * Set cookie `name` to `val`, with the given `options`. 555 * 556 * Options: 557 * 558 * - `maxAge` max-age in milliseconds, converted to `expires` 559 * - `signed` sign the cookie 560 * - `path` defaults to "/" 561 * 562 * Examples: 563 * 564 * // "Remember Me" for 15 minutes 565 * res.cookie('rememberme', '1', { expires: new Date(Date.now() + 900000), httpOnly: true }); 566 * 567 * // save as above 568 * res.cookie('rememberme', '1', { maxAge: 900000, httpOnly: true }) 569 * 570 * @param {String} name 571 * @param {String|Object} val 572 * @param {Options} options 573 * @api public 574 */ 575 576res.cookie = function(name, val, options){ 577 options = options || {}; 578 var secret = this.req.secret; 579 var signed = options.signed; 580 if (signed && !secret) throw new Error('connect.cookieParser("secret") required for signed cookies'); 581 if ('object' == typeof val) val = 'j:' + JSON.stringify(val); 582 if (signed) val = 's:' + utils.sign(val, secret); 583 if ('maxAge' in options) options.expires = new Date(Date.now() + options.maxAge); 584 if (null == options.path) options.path = '/'; 585 this.set('Set-Cookie', cookie.serialize(name, String(val), options)); 586 return this; 587}; 588 589/** 590 * Redirect to the given `url` with optional response `status` 591 * defaulting to 302. 592 * 593 * The given `url` can also be the name of a mapped url, for 594 * example by default express supports "back" which redirects 595 * to the _Referrer_ or _Referer_ headers or "/". 596 * 597 * Examples: 598 * 599 * res.redirect('/foo/bar'); 600 * res.redirect('http://example.com'); 601 * res.redirect(301, 'http://example.com'); 602 * res.redirect('../login'); // /blog/post/1 -> /blog/login 603 * 604 * Mounting: 605 * 606 * When an application is mounted, and `res.redirect()` 607 * is given a path that does _not_ lead with "/". For 608 * example suppose a "blog" app is mounted at "/blog", 609 * the following redirect would result in "/blog/login": 610 * 611 * res.redirect('login'); 612 * 613 * While the leading slash would result in a redirect to "/login": 614 * 615 * res.redirect('/login'); 616 * 617 * @param {String} url 618 * @param {Number} code 619 * @api public 620 */ 621 622res.redirect = function(url){ 623 var app = this.app 624 , req = this.req 625 , head = 'HEAD' == req.method 626 , status = 302 627 , body; 628 629 // allow status / url 630 if (2 == arguments.length) { 631 status = url; 632 url = arguments[1]; 633 } 634 635 // setup redirect map 636 var map = { back: req.get('Referrer') || '/' }; 637 638 // perform redirect 639 url = map[url] || url; 640 641 // relative 642 if (!~url.indexOf('://') && 0 != url.indexOf('//')) { 643 var path = app.path(); 644 645 // relative to path 646 if ('.' == url[0]) { 647 url = req.path + '/' + url; 648 // relative to mount-point 649 } else if ('/' != url[0]) { 650 url = path + '/' + url; 651 } 652 653 // Absolute 654 var host = req.get('Host'); 655 url = '//' + host + url; 656 } 657 658 // Support text/{plain,html} by default 659 this.format({ 660 text: function(){ 661 body = statusCodes[status] + '. Redirecting to ' + url; 662 }, 663 664 html: function(){ 665 var u = utils.escape(url); 666 body = '<p>' + statusCodes[status] + '. Redirecting to <a href="' + u + '">' + u + '</a></p>'; 667 }, 668 669 default: function(){ 670 body = ''; 671 } 672 }); 673 674 // Respond 675 this.statusCode = status; 676 this.set('Location', url); 677 this.set('Content-Length', Buffer.byteLength(body)); 678 this.end(head ? null : body); 679}; 680 681/** 682 * Render `view` with the given `options` and optional callback `fn`. 683 * When a callback function is given a response will _not_ be made 684 * automatically, otherwise a response of _200_ and _text/html_ is given. 685 * 686 * Options: 687 * 688 * - `cache` boolean hinting to the engine it should cache 689 * - `filename` filename of the view being rendered 690 * 691 * @param {String} view 692 * @param {Object|Function} options or callback function 693 * @param {Function} fn 694 * @api public 695 */ 696 697res.render = function(view, options, fn){ 698 var self = this 699 , options = options || {} 700 , req = this.req 701 , app = req.app; 702 703 // support callback function as second arg 704 if ('function' == typeof options) { 705 fn = options, options = {}; 706 } 707 708 // merge res.locals 709 options._locals = self.locals; 710 711 // default callback to respond 712 fn = fn || function(err, str){ 713 if (err) return req.next(err); 714 self.send(str); 715 }; 716 717 // render 718 app.render(view, options, fn); 719};