express/lib/response.js
Tj Holowaychuk d3ccdbcf72 build res.download() on top of res.sendfile()
like it should have always been
2011-08-18 15:43:19 -07:00

447 lines
10 KiB
JavaScript

/*!
* Express - response
* Copyright(c) 2010 TJ Holowaychuk <tj@vision-media.ca>
* MIT Licensed
*/
/**
* Module dependencies.
*/
var fs = require('fs')
, http = require('http')
, path = require('path')
, connect = require('connect')
, utils = connect.utils
, statusCodes = http.STATUS_CODES
, parseRange = require('./utils').parseRange
, res = http.ServerResponse.prototype
, send = connect.static.send
, mime = require('mime')
, basename = path.basename
, join = path.join;
/**
* Send a response.
*
* Examples:
*
* res.send(new Buffer('wahoo'));
* res.send({ some: 'json' });
* res.send('<p>some html</p>');
* res.send(404, 'Sorry, cant find that');
* res.send(404);
*
* @param {Mixed} body or status
* @param {Mixed} body
* @return {ServerResponse}
* @api public
*/
res.send = function(body){
// allow status / body
if (2 == arguments.length) {
this.statusCode = body;
body = arguments[1];
}
switch (typeof body) {
// response status
case 'number':
this.header('Content-Type') || this.contentType('.txt');
this.statusCode = body;
body = http.STATUS_CODES[body];
break;
// string defaulting to html
case 'string':
if (!this.header('Content-Type')) {
this.charset = this.charset || 'utf-8';
this.contentType('.html');
}
break;
case 'boolean':
case 'object':
if (null == body) {
body = '';
} else if (Buffer.isBuffer(body)) {
this.header('Content-Type') || this.contentType('.bin');
} else {
return this.json(body);
}
break;
}
// populate Content-Length
if (undefined !== body && !this.header('Content-Length')) {
this.header('Content-Length', Buffer.isBuffer(body)
? body.length
: Buffer.byteLength(body));
}
// strip irrelevant headers
if (204 == this.statusCode || 304 == this.statusCode) {
this.removeHeader('Content-Type');
this.removeHeader('Content-Length');
body = '';
}
// respond
this.end('HEAD' == this.req.method ? undefined : body);
return this;
};
/**
* Send JSON response.
*
* Examples:
*
* res.json(null);
* res.json({ user: 'tj' });
* res.json(500, 'oh noes!');
* res.json(404, 'I dont have that');
*
* @param {Mixed} obj or status
* @param {Mixed} obj
* @return {ServerResponse}
* @api public
*/
res.json = function(obj){
// allow status / body
if (2 == arguments.length) {
this.statusCode = obj;
obj = arguments[1];
}
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);
};
/**
* 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.
* The callback `fn(err)` is invoked when the transfer is complete
* or when an error occurs. Be sure to check `res.sentHeader`
* if you wish to attempt responding, as the header and some data
* may have already been transferred.
*
* 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 self = this
, next = this.req.next
, options = options || {};
// support function as second arg
if ('function' == typeof options) {
fn = options;
options = {};
}
// callback
options.callback = function(err){
// TODO: remove when node adds this
self.sentHeader = !! self._header;
// error
if (err) {
// ditch content-disposition to prevent funky responses
if (!self.sentHeader) self.removeHeader('Content-Disposition');
// lost in limbo if there's no callback
if (self.sentHeader) return fn && fn(err);
// callback available
if (fn) return fn(err);
return self.req.next(err);
}
fn && fn();
};
// transfer
options.path = encodeURIComponent(path);
send(this.req, this, next, options);
};
/**
* Transfer the file at the given `path` as an attachment.
*
* Optionally providing an alternate attachment `filename`,
* and optional callback `fn(err)`. The callback is invoked
* when the data transfer is complete, or when an error has
* ocurred. Be sure to check `res.sendHeader` if you plan to respond.
*
* @param {String} path
* @param {String|Function} filename or fn
* @param {Function} fn
* @api public
*/
res.download = function(path, filename, fn){
// support function as second arg
if ('function' == typeof filename) {
fn = filename;
filename = null;
}
return this.attachment(filename || path).sendfile(path, fn);
};
/**
* 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 {ServerResponse} for chaining
* @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;
};
/**
* Set header `field` to `val`.
*
* @param {String} field
* @param {String} val
* @return {ServerResponse} for chaining
* @api public
*/
res.set = function(field, val){
this.setHeader(field, val);
return this;
};
/**
* Get value for header `field`.
*
* @param {String} field
* @return {String}
* @api public
*/
res.get = function(field){
return this.getHeader(field);
};
/**
* Set or get response header `field` with optional `val`.
*
* @param {String} name
* @param {String} val
* @return {ServerResponse} for chaining
* @api public
*/
res.header = function(field, val){
if (1 == arguments.length) return this.getHeader(field);
this.setHeader(field, val);
return this;
};
/**
* Clear cookie `name`.
*
* @param {String} name
* @param {Object} options
* @param {ServerResponse} for chaining
* @api public
*/
res.clearCookie = function(name, options){
var opts = { expires: new Date(1) };
return 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);
return this;
};
/**
* 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');`.
*
* 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
, 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 = '<p>' + statusCodes[status] + '. Redirecting to <a href="' + url + '">' + url + '</a></p>';
this.header('Content-Type', 'text/html');
} else {
body = statusCodes[status] + '. Redirecting to ' + url;
this.header('Content-Type', 'text/plain');
}
// Respond
this.statusCode = status;
this.header('Location', url);
this.end(body);
};