/*! * Express - response * Copyright(c) 2010 TJ Holowaychuk * MIT Licensed */ /** * Module dependencies. */ var fs = require( 'fs' ) , http = require( 'http' ) , path = require( 'path' ) , connect = require( 'connect' ) , utils = connect.utils , parseRange = require( './utils' ).parseRange , res = http.ServerResponse.prototype , send = connect.static.send , mime = require( 'mime' ) , basename = path.basename , join = path.join; /** * Send a response with the given `body` and optional `headers` and `status` code. * * Examples: * * res.send(); * res.send(new Buffer('wahoo')); * res.send({ some: 'json' }); * res.send('

some html

'); * res.send('Sorry, cant find that', 404); * res.send('text', { 'Content-Type': 'text/plain' }, 201); * res.send(404); * * @param {String|Object|Number|Buffer} body or status * @param {Object|Number} headers or status * @param {Number} status * @return {ServerResponse} * @api public */ res.send = function ( body, headers, status ) { // allow status as second arg if ( 'number' == typeof headers ) { status = headers, headers = null; } // default status status = status || this.statusCode; // allow 0 args as 204 if ( !arguments.length || undefined === body ) { status = 204; } // determine content type switch ( typeof body ) { case 'number': if ( !this.header( 'Content-Type' ) ) { this.contentType( '.txt' ); } body = http.STATUS_CODES[status = body]; break; case 'string': if ( !this.header( 'Content-Type' ) ) { this.charset = this.charset || 'utf-8'; this.contentType( '.html' ); } break; case 'boolean': case 'object': if ( Buffer.isBuffer( body ) ) { if ( !this.header( 'Content-Type' ) ) { this.contentType( '.bin' ); } } else { return this.json( body, headers, status ); } break; } // populate Content-Length if ( undefined !== body && !this.header( 'Content-Length' ) ) { this.header( 'Content-Length', Buffer.isBuffer( body ) ? body.length : Buffer.byteLength( body ) ); } // merge headers passed if ( headers ) { var fields = Object.keys( headers ); for ( var i = 0, len = fields.length; i < len; ++i ) { var field = fields[i]; this.header( field, headers[field] ); } } // strip irrelevant headers if ( 204 == status || 304 == status ) { this.removeHeader( 'Content-Type' ); this.removeHeader( 'Content-Length' ); body = ''; } // respond this.statusCode = status; this.end( 'HEAD' == this.req.method ? null : body ); return this; }; /** * Send JSON response with `obj`, optional `headers`, and optional `status`. * * Examples: * * res.json(null); * res.json({ user: 'tj' }); * res.json('oh noes!', 500); * res.json('I dont have that', 404); * * @param {Mixed} obj * @param {Object|Number} headers or status * @param {Number} status * @return {ServerResponse} * @api public */ res.json = function ( obj, headers, status ) { var body = JSON.stringify( obj ) , callback = this.req.query.callback , jsonp = this.app.enabled( 'jsonp callback' ); this.charset = this.charset || 'utf-8'; this.header( 'Content-Type', 'application/json' ); if ( callback && jsonp ) { this.header( 'Content-Type', 'text/javascript' ); body = callback.replace( /[^\w$.]/g, '' ) + '(' + body + ');'; } return this.send( body, headers, status ); }; /** * Set status `code`. * * @param {Number} code * @return {ServerResponse} * @api public */ res.status = function ( code ) { this.statusCode = code; return this; }; /** * Transfer the file at the given `path`. Automatically sets * the _Content-Type_ response header field. `next()` is called * when `path` is a directory, or when an error occurs. * * Options: * * - `maxAge` defaulting to 0 * - `root` root directory for relative filenames * * @param {String} path * @param {Object|Function} options or fn * @param {Function} fn * @api public */ res.sendfile = function ( path, options, fn ) { var next = this.req.next; options = options || {}; // support function as second arg if ( 'function' == typeof options ) { fn = options; options = {}; } options.path = encodeURIComponent( path ); options.callback = fn; send( this.req, this, next, options ); }; /** * Set _Content-Type_ response header passed through `mime.lookup()`. * * Examples: * * var filename = 'path/to/image.png'; * res.contentType(filename); * // res.headers['Content-Type'] is now "image/png" * * res.contentType('.html'); * res.contentType('html'); * res.contentType('json'); * res.contentType('png'); * * @param {String} type * @return {String} the resolved mime type * @api public */ res.contentType = function ( type ) { return this.header( 'Content-Type', mime.lookup( type ) ); }; /** * Set _Content-Disposition_ header to _attachment_ with optional `filename`. * * @param {String} filename * @return {ServerResponse} * @api public */ res.attachment = function ( filename ) { if ( filename ) { this.contentType( filename ); } this.header( 'Content-Disposition', filename ? 'attachment; filename="' + basename( filename ) + '"' : 'attachment' ); return this; }; /** * Transfer the file at the given `path`, with optional * `filename` as an attachment and optional callback `fn(err)`, * and optional `fn2(err)` which is invoked when an error has * occurred after header has been sent. * * @param {String} path * @param {String|Function} filename or fn * @param {Function} fn * @param {Function} fn2 * @api public */ res.download = function ( path, filename, fn, fn2 ) { var self = this; // support callback as second arg if ( 'function' == typeof filename ) { fn2 = fn; fn = filename; filename = null; } // transfer the file this.attachment( filename || path ).sendfile( path, function ( err ) { var sentHeader = self._header; if ( err ) { if ( !sentHeader ) { self.removeHeader( 'Content-Disposition' ); } if ( sentHeader ) { fn2 && fn2( err ); } else if ( fn ) { fn( err ); } else { self.req.next( err ); } } else if ( fn ) { fn(); } } ); }; /** * Set or get response header `name` with optional `val`. * * @param {String} name * @param {String} val * @return {ServerResponse} for chaining * @api public */ res.header = function ( name, val ) { if ( 1 == arguments.length ) { return this.getHeader( name ); } this.setHeader( name, val ); return this; }; /** * Clear cookie `name`. * * @param {String} name * @param {Object} options * @api public */ res.clearCookie = function ( name, options ) { var opts = { expires : new Date( 1 ) }; this.cookie( name, '', options ? utils.merge( options, opts ) : opts ); }; /** * Set cookie `name` to `val`, with the given `options`. * * Options: * * - `maxAge` max-age in milliseconds, converted to `expires` * - `path` defaults to the "basepath" setting which is typically "/" * * Examples: * * // "Remember Me" for 15 minutes * res.cookie('rememberme', '1', { expires: new Date(Date.now() + 900000), httpOnly: true }); * * // save as above * res.cookie('rememberme', '1', { maxAge: 900000, httpOnly: true }) * * @param {String} name * @param {String} val * @param {Options} options * @api public */ res.cookie = function ( name, val, options ) { options = options || {}; if ( 'maxAge' in options ) { options.expires = new Date( Date.now() + options.maxAge ); } if ( undefined === options.path ) { options.path = this.app.set( 'basepath' ); } var cookie = utils.serializeCookie( name, val, options ); this.header( 'Set-Cookie', cookie ); }; /** * Redirect to the given `url` with optional response `status` * defauling to 302. * * The given `url` can also be the name of a mapped url, for * example by default express supports "back" which redirects * to the _Referrer_ or _Referer_ headers or the application's * "basepath" setting. Express also supports "basepath" out of the box, * which can be set via `app.set('basepath', '/blog');`, and defaults * to '/'. * * Redirect Mapping: * * To extend the redirect mapping capabilities that Express provides, * we may use the `app.redirect()` method: * * app.redirect('google', 'http://google.com'); * * Now in a route we may call: * * res.redirect('google'); * * We may also map dynamic redirects: * * app.redirect('comments', function(req, res){ * return '/post/' + req.params.id + '/comments'; * }); * * So now we may do the following, and the redirect will dynamically adjust to * the context of the request. If we called this route with _GET /post/12_ our * redirect _Location_ would be _/post/12/comments_. * * app.get('/post/:id', function(req, res){ * res.redirect('comments'); * }); * * Unless an absolute `url` is given, the app's mount-point * will be respected. For example if we redirect to `/posts`, * and our app is mounted at `/blog` we will redirect to `/blog/posts`. * * @param {String} url * @param {Number} code * @api public */ res.redirect = function ( url, status ) { var app = this.app , req = this.req , base = app.set( 'basepath' ) || app.route , status = status || 302 , head = 'HEAD' == req.method , body; // Setup redirect map var map = { back : req.header( 'Referrer', base ), home : base }; // Support custom redirect map map.__proto__ = app.redirects; // Attempt mapped redirect var mapped = 'function' == typeof map[url] ? map[url]( req, this ) : map[url]; // Perform redirect url = mapped || url; // Relative if ( !~url.indexOf( '://' ) ) { // Respect mount-point if ( '/' != base && 0 != url.indexOf( base ) ) { url = base + url; } // Absolute var host = req.headers.host , tls = req.connection.encrypted; url = 'http' + (tls ? 's' : '') + '://' + host + url; } // Support text/{plain,html} by default if ( req.accepts( 'html' ) ) { body = '

' + http.STATUS_CODES[status] + '. Redirecting to ' + url + '

'; this.header( 'Content-Type', 'text/html' ); } else { body = http.STATUS_CODES[status] + '. Redirecting to ' + url; this.header( 'Content-Type', 'text/plain' ); } // Respond this.statusCode = status; this.header( 'Location', url ); this.end( head ? null : body ); }; /** * Assign the view local variable `name` to `val` or return the * local previously assigned to `name`. * * @param {String} name * @param {Mixed} val * @return {Mixed} val * @api public */ res.local = function ( name, val ) { this._locals = this._locals || {}; return undefined === val ? this._locals[name] : this._locals[name] = val; }; /** * Assign several locals with the given `obj`, * or return the locals. * * @param {Object} obj * @return {Object|Undefined} * @api public */ res.locals = res.helpers = function ( obj ) { if ( obj ) { for ( var key in obj ) { this.local( key, obj[key] ); } } else { return this._locals; } };