4 * Copyright(c) 2010 TJ Holowaychuk <tj@vision-media.ca>
12 var fs = require('fs')
13 , http = require('http')
14 , path = require('path')
15 , connect = require('connect')
16 , utils = connect.utils
17 , parseRange = require('./utils').parseRange
18 , res = http.ServerResponse.prototype
19 , send = connect.static.send
20 , join = require('path').join
21 , mime = require('mime');
24 * Send a response with the given `body` and optional `headers` and `status` code.
29 * res.send(new Buffer('wahoo'));
30 * res.send({ some: 'json' });
31 * res.send('<p>some html</p>');
32 * res.send('Sorry, cant find that', 404);
33 * res.send('text', { 'Content-Type': 'text/plain' }, 201);
36 * @param {String|Object|Number|Buffer} body or status
37 * @param {Object|Number} headers or status
38 * @param {Number} status
39 * @return {ServerResponse}
43 res.send = function(body, headers, status){
44 // allow status as second arg
45 if ('number' == typeof headers) {
51 status = status || this.statusCode;
53 // allow 0 args as 204
54 if (!arguments.length || undefined === body) body = status = 204;
56 // determine content type
57 switch (typeof body) {
59 if (!this.header('Content-Type')) {
60 this.contentType('.txt');
62 body = http.STATUS_CODES[status = body];
65 if (!this.header('Content-Type')) {
66 this.charset = this.charset || 'utf-8';
67 this.contentType('.html');
72 if (Buffer.isBuffer(body)) {
73 if (!this.header('Content-Type')) {
74 this.contentType('.bin');
77 if (!this.header('Content-Type')) {
78 this.charset = this.charset || 'utf-8';
79 this.contentType('.json');
81 body = JSON.stringify(body);
82 if (this.req.query.callback && this.app.set('jsonp callback')) {
83 this.charset = this.charset || 'utf-8';
84 this.header('Content-Type', 'text/javascript');
85 body = this.req.query.callback.replace(/[^\w$.]/g, '') + '(' + body + ');';
91 // populate Content-Length
92 if (!this.header('Content-Length')) {
93 this.header('Content-Length', Buffer.isBuffer(body)
95 : Buffer.byteLength(body));
98 // merge headers passed
100 var fields = Object.keys(headers);
101 for (var i = 0, len = fields.length; i < len; ++i) {
102 var field = fields[i];
103 this.header(field, headers[field]);
107 // strip irrelevant headers
108 if (204 === status) {
109 this.removeHeader('Content-Type');
110 this.removeHeader('Content-Length');
114 this.statusCode = status;
115 this.end('HEAD' == this.req.method ? undefined : body);
119 * Transfer the file at the given `path`. Automatically sets
120 * the _Content-Type_ response header field. `next()` is called
121 * when `path` is a directory, or when an error occurs.
125 * - `maxAge` defaulting to 0
126 * - `root` root directory for relative filenames
128 * @param {String} path
129 * @param {Object|Function} options or fn
130 * @param {Function} fn
134 res.sendfile = function(path, options, fn){
135 var next = this.req.next;
136 options = options || {};
138 // support function as second arg
139 if ('function' == typeof options) {
145 options.callback = fn;
146 send(this.req, this, next, options);
150 * Set _Content-Type_ response header passed through `mime.lookup()`.
154 * var filename = 'path/to/image.png';
155 * res.contentType(filename);
156 * // res.headers['Content-Type'] is now "image/png"
158 * res.contentType('.html');
159 * res.contentType('html');
160 * res.contentType('json');
161 * res.contentType('png');
163 * @param {String} type
164 * @return {String} the resolved mime type
168 res.contentType = function(type){
169 return this.header('Content-Type', mime.lookup(type));
173 * Set _Content-Disposition_ header to _attachment_ with optional `filename`.
175 * @param {String} filename
176 * @return {ServerResponse}
180 res.attachment = function(filename){
181 if (filename) this.contentType(filename);
182 this.header('Content-Disposition', filename
183 ? 'attachment; filename="' + path.basename(filename) + '"'
189 * Transfer the file at the given `path`, with optional
190 * `filename` as an attachment and optional callback `fn(err)`,
191 * and optional `fn2(err)` which is invoked when an error has
192 * occurred after headers have been sent.
194 * @param {String} path
195 * @param {String|Function} filename or fn
196 * @param {Function} fn
197 * @param {Function} fn2
201 res.download = function(path, filename, fn, fn2){
204 // support callback as second arg
205 if ('function' == typeof filename) {
212 this.attachment(filename || path).sendfile(path, function(err){
213 var sentHeader = self._header;
215 if (!sentHeader) self.removeHeader('Content-Disposition');
230 * Set or get response header `name` with optional `val`.
232 * @param {String} name
233 * @param {String} val
238 res.header = function(name, val){
239 if (val === undefined) {
240 return this.getHeader(name);
242 this.setHeader(name, val);
248 * Clear cookie `name`.
250 * @param {String} name
251 * @param {Object} options
255 res.clearCookie = function(name, options){
256 var opts = { expires: new Date(1) };
257 this.cookie(name, '', options
258 ? utils.merge(options, opts)
263 * Set cookie `name` to `val`, with the given `options`.
267 * - `maxAge` max-age in milliseconds, converted to `expires`
271 * // "Remember Me" for 15 minutes
272 * res.cookie('rememberme', '1', { expires: new Date(Date.now() + 900000), httpOnly: true });
275 * res.cookie('rememberme', '1', { maxAge: 900000, httpOnly: true })
277 * @param {String} name
278 * @param {String} val
279 * @param {Options} options
283 res.cookie = function(name, val, options){
284 options = options || {};
285 if ('maxAge' in options) options.expires = new Date(Date.now() + options.maxAge);
286 var cookie = utils.serializeCookie(name, val, options);
287 this.header('Set-Cookie', cookie);
291 * Redirect to the given `url` with optional response `status`
294 * The given `url` can also be the name of a mapped url, for
295 * example by default express supports "back" which redirects
296 * to the _Referrer_ or _Referer_ headers or the application's
297 * "home" setting. Express also supports "home" out of the box,
298 * which can be set via `app.set('home', '/blog');`, and defaults
303 * To extend the redirect mapping capabilities that Express provides,
304 * we may use the `app.redirect()` method:
306 * app.redirect('google', 'http://google.com');
308 * Now in a route we may call:
310 * res.redirect('google');
312 * We may also map dynamic redirects:
314 * app.redirect('comments', function(req, res){
315 * return '/post/' + req.params.id + '/comments';
318 * So now we may do the following, and the redirect will dynamically adjust to
319 * the context of the request. If we called this route with _GET /post/12_ our
320 * redirect _Location_ would be _/post/12/comments_.
322 * app.get('/post/:id', function(req, res){
323 * res.redirect('comments');
326 * Unless an absolute `url` is given, the app's mount-point
327 * will be respected. For example if we redirect to `/posts`,
328 * and our app is mounted at `/blog` we will redirect to `/blog/posts`.
330 * @param {String} url
331 * @param {Number} code
335 res.redirect = function(url, status){
338 , base = app.set('home') || '/'
339 , status = status || 302
342 // Setup redirect map
344 back: req.header('Referrer', base)
348 // Support custom redirect map
349 map.__proto__ = app.redirects;
351 // Attempt mapped redirect
352 var mapped = 'function' == typeof map[url]
353 ? map[url](req, this)
360 if (!~url.indexOf('://')) {
361 // Respect mount-point
363 url = join(app.route, url);
367 var host = req.headers.host
368 , tls = req.connection.encrypted;
369 url = 'http' + (tls ? 's' : '') + '://' + host + url;
373 // Support text/{plain,html} by default
374 if (req.accepts('html')) {
375 body = '<p>' + http.STATUS_CODES[status] + '. Redirecting to <a href="' + url + '">' + url + '</a></p>';
376 this.header('Content-Type', 'text/html');
378 body = http.STATUS_CODES[status] + '. Redirecting to ' + url;
379 this.header('Content-Type', 'text/plain');
383 this.statusCode = status;
384 this.header('Location', url);
389 * Assign the view local variable `name` to `val` or return the
390 * local previously assigned to `name`.
392 * @param {String} name
394 * @return {Mixed} val
398 res.local = function(name, val){
399 this._locals = this._locals || {};
400 return undefined === val
402 : this._locals[name] = val;
406 * Assign several locals with the given `obj`,
407 * or return the locals.
409 * @param {Object} obj
410 * @return {Object|Undefined}
415 res.helpers = function(obj){
417 for (var key in obj) {
418 this.local(key, obj[key]);