5 var http = require('http')
6 , path = require('path')
7 , connect = require('connect')
8 , utils = connect.utils
9 , sign = require('cookie-signature').sign
10 , normalizeType = require('./utils').normalizeType
11 , normalizeTypes = require('./utils').normalizeTypes
12 , etag = require('./utils').etag
13 , statusCodes = http.STATUS_CODES
14 , cookie = require('cookie')
15 , send = require('send')
17 , resolve = require('url').resolve
18 , basename = path.basename
19 , extname = path.extname;
25 var res = module.exports = {
26 __proto__: http.ServerResponse.prototype
32 * @param {Number} code
33 * @return {ServerResponse}
37 res.status = function(code){
38 this.statusCode = code;
43 * Set Link header field with the given `links`.
48 * next: 'http://api.example.com/users?page=2',
49 * last: 'http://api.example.com/users?page=5'
52 * @param {Object} links
53 * @return {ServerResponse}
57 res.links = function(links){
58 var link = this.get('Link') || '';
59 if (link) link += ', ';
60 return this.set('Link', link + Object.keys(links).map(function(rel){
61 return '<' + links[rel] + '>; rel="' + rel + '"';
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');
76 * @param {Mixed} body or status
78 * @return {ServerResponse}
82 res.send = function(body){
84 var head = 'HEAD' == req.method;
90 // allow status / body
91 if (2 == arguments.length) {
92 // res.send(body, status) backwards compat
93 if ('number' != typeof body && 'number' == typeof arguments[1]) {
94 this.statusCode = arguments[1];
96 this.statusCode = body;
101 switch (typeof body) {
104 this.get('Content-Type') || this.type('txt');
105 this.statusCode = body;
106 body = http.STATUS_CODES[body];
108 // string defaulting to html
110 if (!this.get('Content-Type')) {
111 this.charset = this.charset || 'utf-8';
119 } else if (Buffer.isBuffer(body)) {
120 this.get('Content-Type') || this.type('bin');
122 return this.json(body);
127 // populate Content-Length
128 if (undefined !== body && !this.get('Content-Length')) {
129 this.set('Content-Length', len = Buffer.isBuffer(body)
131 : Buffer.byteLength(body));
136 if (app.settings.etag && len && 'GET' == req.method) {
137 if (!this.get('ETag')) {
138 this.set('ETag', etag(body));
143 if (req.fresh) this.statusCode = 304;
145 // strip irrelevant headers
146 if (204 == this.statusCode || 304 == this.statusCode) {
147 this.removeHeader('Content-Type');
148 this.removeHeader('Content-Length');
149 this.removeHeader('Transfer-Encoding');
154 this.end(head ? null : body);
159 * Send JSON response.
164 * res.json({ user: 'tj' });
165 * res.json(500, 'oh noes!');
166 * res.json(404, 'I dont have that');
168 * @param {Mixed} obj or status
170 * @return {ServerResponse}
174 res.json = function(obj){
175 // allow status / body
176 if (2 == arguments.length) {
177 // res.json(body, status) backwards compat
178 if ('number' == typeof arguments[1]) {
179 this.statusCode = arguments[1];
181 this.statusCode = obj;
188 var replacer = app.get('json replacer');
189 var spaces = app.get('json spaces');
190 var body = JSON.stringify(obj, replacer, spaces);
193 this.charset = this.charset || 'utf-8';
194 this.get('Content-Type') || this.set('Content-Type', 'application/json');
196 return this.send(body);
200 * Send JSON response with JSONP callback support.
205 * res.jsonp({ user: 'tj' });
206 * res.jsonp(500, 'oh noes!');
207 * res.jsonp(404, 'I dont have that');
209 * @param {Mixed} obj or status
211 * @return {ServerResponse}
215 res.jsonp = function(obj){
216 // allow status / body
217 if (2 == arguments.length) {
218 // res.json(body, status) backwards compat
219 if ('number' == typeof arguments[1]) {
220 this.statusCode = arguments[1];
222 this.statusCode = obj;
229 var replacer = app.get('json replacer');
230 var spaces = app.get('json spaces');
231 var body = JSON.stringify(obj, replacer, spaces)
232 .replace(/\u2028/g, '\\u2028')
233 .replace(/\u2029/g, '\\u2029');
234 var callback = this.req.query[app.get('jsonp callback name')];
237 this.charset = this.charset || 'utf-8';
238 this.set('Content-Type', 'application/json');
242 if (Array.isArray(callback)) callback = callback[0];
243 this.set('Content-Type', 'text/javascript');
244 var cb = callback.replace(/[^\[\]\w$.]/g, '');
245 body = 'typeof ' + cb + ' === \'function\' && ' + cb + '(' + body + ');';
248 return this.send(body);
252 * Transfer the file at the given `path`.
254 * Automatically sets the _Content-Type_ response header field.
255 * The callback `fn(err)` is invoked when the transfer is complete
256 * or when an error occurs. Be sure to check `res.sentHeader`
257 * if you wish to attempt responding, as the header and some data
258 * may have already been transferred.
262 * - `maxAge` defaulting to 0
263 * - `root` root directory for relative filenames
267 * The following example illustrates how `res.sendfile()` may
268 * be used as an alternative for the `static()` middleware for
269 * dynamic situations. The code backing `res.sendfile()` is actually
270 * the same code, so HTTP cache support etc is identical.
272 * app.get('/user/:uid/photos/:file', function(req, res){
273 * var uid = req.params.uid
274 * , file = req.params.file;
276 * req.user.mayViewFilesFrom(uid, function(yes){
278 * res.sendfile('/uploads/' + uid + '/' + file);
280 * res.send(403, 'Sorry! you cant see that.');
285 * @param {String} path
286 * @param {Object|Function} options or fn
287 * @param {Function} fn
291 res.sendfile = function(path, options, fn){
294 , next = this.req.next
295 , options = options || {}
298 // support function as second arg
299 if ('function' == typeof options) {
305 req.socket.on('error', error);
308 function error(err) {
314 if (!self.headerSent) self.removeHeader('Content-Disposition');
316 // callback available
317 if (fn) return fn(err);
319 // list in limbo if there's no callback
320 if (self.headerSent) return;
327 function stream(stream) {
330 if (fn) stream.on('end', fn);
335 req.socket.removeListener('error', error);
339 var file = send(req, path);
340 if (options.root) file.root(options.root);
341 file.maxage(options.maxAge || 0);
342 file.on('error', error);
343 file.on('directory', next);
344 file.on('stream', stream);
346 this.on('finish', cleanup);
350 * Transfer the file at the given `path` as an attachment.
352 * Optionally providing an alternate attachment `filename`,
353 * and optional callback `fn(err)`. The callback is invoked
354 * when the data transfer is complete, or when an error has
355 * ocurred. Be sure to check `res.headerSent` if you plan to respond.
357 * This method uses `res.sendfile()`.
359 * @param {String} path
360 * @param {String|Function} filename or fn
361 * @param {Function} fn
365 res.download = function(path, filename, fn){
366 // support function as second arg
367 if ('function' == typeof filename) {
372 filename = filename || path;
373 this.set('Content-Disposition', 'attachment; filename="' + basename(filename) + '"');
374 return this.sendfile(path, fn);
378 * Set _Content-Type_ response header with `type` through `mime.lookup()`
379 * when it does not contain "/", or set the Content-Type to `type` otherwise.
386 * res.type('application/json');
389 * @param {String} type
390 * @return {ServerResponse} for chaining
395 res.type = function(type){
396 return this.set('Content-Type', ~type.indexOf('/')
398 : mime.lookup(type));
402 * Respond to the Acceptable formats using an `obj`
403 * of mime-type callbacks.
405 * This method uses `req.accepted`, an array of
406 * acceptable types ordered by their quality values.
407 * When "Accept" is not present the _first_ callback
408 * is invoked, otherwise the first match is used. When
409 * no match is performed the server responds with
410 * 406 "Not Acceptable".
412 * Content-Type is set for you, however if you choose
413 * you may alter this within the callback using `res.type()`
414 * or `res.set('Content-Type', ...)`.
417 * 'text/plain': function(){
421 * 'text/html': function(){
422 * res.send('<p>hey</p>');
425 * 'appliation/json': function(){
426 * res.send({ message: 'hey' });
430 * In addition to canonicalized MIME types you may
431 * also use extnames mapped to these types:
439 * res.send('<p>hey</p>');
443 * res.send({ message: 'hey' });
447 * By default Express passes an `Error`
448 * with a `.status` of 406 to `next(err)`
449 * if a match is not made. If you provide
450 * a `.default` callback it will be invoked
453 * @param {Object} obj
454 * @return {ServerResponse} for chaining
458 res.format = function(obj){
462 var fn = obj.default;
463 if (fn) delete obj.default;
464 var keys = Object.keys(obj);
466 var key = req.accepts(keys);
471 var type = normalizeType(key).value;
472 var charset = mime.charsets.lookup(type);
473 if (charset) type += '; charset=' + charset;
474 this.set('Content-Type', type);
475 obj[key](req, this, next);
479 var err = new Error('Not Acceptable');
481 err.types = normalizeTypes(keys).map(function(o){ return o.value });
489 * Set _Content-Disposition_ header to _attachment_ with optional `filename`.
491 * @param {String} filename
492 * @return {ServerResponse}
496 res.attachment = function(filename){
497 if (filename) this.type(extname(filename));
498 this.set('Content-Disposition', filename
499 ? 'attachment; filename="' + basename(filename) + '"'
505 * Set header `field` to `val`, or pass
506 * an object of header fields.
510 * res.set('Foo', ['bar', 'baz']);
511 * res.set('Accept', 'application/json');
512 * res.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' });
514 * Aliased as `res.header()`.
516 * @param {String|Object|Array} field
517 * @param {String} val
518 * @return {ServerResponse} for chaining
523 res.header = function(field, val){
524 if (2 == arguments.length) {
525 if (Array.isArray(val)) val = val.map(String);
526 else val = String(val);
527 this.setHeader(field, val);
529 for (var key in field) {
530 this.set(key, field[key]);
537 * Get value for header `field`.
539 * @param {String} field
544 res.get = function(field){
545 return this.getHeader(field);
549 * Clear cookie `name`.
551 * @param {String} name
552 * @param {Object} options
553 * @param {ServerResponse} for chaining
557 res.clearCookie = function(name, options){
558 var opts = { expires: new Date(1), path: '/' };
559 return this.cookie(name, '', options
560 ? utils.merge(opts, options)
565 * Set cookie `name` to `val`, with the given `options`.
569 * - `maxAge` max-age in milliseconds, converted to `expires`
570 * - `signed` sign the cookie
571 * - `path` defaults to "/"
575 * // "Remember Me" for 15 minutes
576 * res.cookie('rememberme', '1', { expires: new Date(Date.now() + 900000), httpOnly: true });
579 * res.cookie('rememberme', '1', { maxAge: 900000, httpOnly: true })
581 * @param {String} name
582 * @param {String|Object} val
583 * @param {Options} options
587 res.cookie = function(name, val, options){
588 options = utils.merge({}, options);
589 var secret = this.req.secret;
590 var signed = options.signed;
591 if (signed && !secret) throw new Error('connect.cookieParser("secret") required for signed cookies');
592 if ('number' == typeof val) val = val.toString();
593 if ('object' == typeof val) val = 'j:' + JSON.stringify(val);
594 if (signed) val = 's:' + sign(val, secret);
595 if ('maxAge' in options) {
596 options.expires = new Date(Date.now() + options.maxAge);
597 options.maxAge /= 1000;
599 if (null == options.path) options.path = '/';
600 this.set('Set-Cookie', cookie.serialize(name, String(val), options));
606 * Set the location header to `url`.
608 * The given `url` can also be "back", which redirects
609 * to the _Referrer_ or _Referer_ headers or "/".
613 * res.location('/foo/bar').;
614 * res.location('http://example.com');
615 * res.location('../login'); // /blog/post/1 -> /blog/login
619 * When an application is mounted and `res.location()`
620 * is given a path that does _not_ lead with "/" it becomes
621 * relative to the mount-point. For example if the application
622 * is mounted at "/blog", the following would become "/blog/login".
624 * res.location('login');
626 * While the leading slash would result in a location of "/login":
628 * res.location('/login');
630 * @param {String} url
634 res.location = function(url){
639 // "back" is an alias for the referrer
640 if(url === 'back') url = req.get('Referrer') || '/';
643 if (!~url.indexOf('://') && 0 != url.indexOf('//')) {
646 path = req.originalUrl.split('?')[0];
647 path = path + ('/' == path[path.length - 1] ? '' : '/');
648 url = resolve(path, url);
649 // relative to mount-point
650 } else if ('/' != url[0]) {
652 url = path + '/' + url;
657 this.set('Location', url);
662 * Redirect to the given `url` with optional response `status`
665 * The resulting `url` is determined by `res.location()`, so
666 * it will play nicely with mounted apps, relative paths,
671 * res.redirect('/foo/bar');
672 * res.redirect('http://example.com');
673 * res.redirect(301, 'http://example.com');
674 * res.redirect('http://example.com', 301);
675 * res.redirect('../login'); // /blog/post/1 -> /blog/login
677 * @param {String} url
678 * @param {Number} code
682 res.redirect = function(url){
683 var head = 'HEAD' == this.req.method
687 // allow status / url
688 if (2 == arguments.length) {
689 if ('number' == typeof url) {
693 status = arguments[1];
697 // Set location header
699 url = this.get('Location');
701 // Support text/{plain,html} by default
704 body = statusCodes[status] + '. Redirecting to ' + encodeURI(url);
708 var u = utils.escape(url);
709 body = '<p>' + statusCodes[status] + '. Redirecting to <a href="' + u + '">' + u + '</a></p>';
718 this.statusCode = status;
719 this.set('Content-Length', Buffer.byteLength(body));
720 this.end(head ? null : body);
724 * Add `field` to Vary. If already present in the Vary set, then
725 * this call is simply ignored.
727 * @param {Array|String} field
728 * @param {ServerResponse} for chaining
732 res.vary = function(field){
736 if (!field) return this;
739 if (Array.isArray(field)) {
740 field.forEach(function(field){
746 var vary = this.get('Vary');
750 vary = vary.split(/ *, */);
751 if (!~vary.indexOf(field)) vary.push(field);
752 this.set('Vary', vary.join(', '));
757 this.set('Vary', field);
762 * Render `view` with the given `options` and optional callback `fn`.
763 * When a callback function is given a response will _not_ be made
764 * automatically, otherwise a response of _200_ and _text/html_ is given.
768 * - `cache` boolean hinting to the engine it should cache
769 * - `filename` filename of the view being rendered
771 * @param {String} view
772 * @param {Object|Function} options or callback function
773 * @param {Function} fn
777 res.render = function(view, options, fn){
779 , options = options || {}
783 // support callback function as second arg
784 if ('function' == typeof options) {
785 fn = options, options = {};
789 options._locals = self.locals;
791 // default callback to respond
792 fn = fn || function(err, str){
793 if (err) return req.next(err);
798 app.render(view, options, fn);