fixes #1826: res.redirect('toString') fails with 500
[express.git] / lib / response.js
blob25c49eedf2b50e379ad2cd4bd684cb694f020a34
1 /**
2  * Module dependencies.
3  */
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')
16   , mime = connect.mime
17   , resolve = require('url').resolve
18   , basename = path.basename
19   , extname = path.extname;
21 /**
22  * Response prototype.
23  */
25 var res = module.exports = {
26   __proto__: http.ServerResponse.prototype
29 /**
30  * Set status `code`.
31  *
32  * @param {Number} code
33  * @return {ServerResponse}
34  * @api public
35  */
37 res.status = function(code){
38   this.statusCode = code;
39   return this;
42 /**
43  * Set Link header field with the given `links`.
44  *
45  * Examples:
46  *
47  *    res.links({
48  *      next: 'http://api.example.com/users?page=2',
49  *      last: 'http://api.example.com/users?page=5'
50  *    });
51  *
52  * @param {Object} links
53  * @return {ServerResponse}
54  * @api public
55  */
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 + '"';
62   }).join(', '));
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  */
82 res.send = function(body){
83   var req = this.req;
84   var head = 'HEAD' == req.method;
85   var len;
87   // settings
88   var app = this.app;
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];
95     } else {
96       this.statusCode = body;
97       body = arguments[1];
98     }
99   }
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   }
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   }
134   // ETag support
135   // TODO: W/ support
136   if (app.settings.etag && len && 'GET' == req.method) {
137     if (!this.get('ETag')) {
138       this.set('ETag', etag(body));
139     }
140   }
142   // freshness
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');
150     body = '';
151   }
153   // respond
154   this.end(head ? null : body);
155   return this;
159  * Send JSON response.
161  * Examples:
163  *     res.json(null);
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
169  * @param {Mixed} obj
170  * @return {ServerResponse}
171  * @api public
172  */
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];
180     } else {
181       this.statusCode = obj;
182       obj = arguments[1];
183     }
184   }
186   // settings
187   var app = this.app;
188   var replacer = app.get('json replacer');
189   var spaces = app.get('json spaces');
190   var body = JSON.stringify(obj, replacer, spaces);
192   // content-type
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.
202  * Examples:
204  *     res.jsonp(null);
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
210  * @param {Mixed} obj
211  * @return {ServerResponse}
212  * @api public
213  */
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];
221     } else {
222       this.statusCode = obj;
223       obj = arguments[1];
224     }
225   }
227   // settings
228   var app = this.app;
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')];
236   // content-type
237   this.charset = this.charset || 'utf-8';
238   this.set('Content-Type', 'application/json');
240   // jsonp
241   if (callback) {
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 + ');';
246   }
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.
260  * Options:
262  *   - `maxAge` defaulting to 0
263  *   - `root`   root directory for relative filenames
265  * Examples:
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){
277  *         if (yes) {
278  *           res.sendfile('/uploads/' + uid + '/' + file);
279  *         } else {
280  *           res.send(403, 'Sorry! you cant see that.');
281  *         }
282  *       });
283  *     });
285  * @param {String} path
286  * @param {Object|Function} options or fn
287  * @param {Function} fn
288  * @api public
289  */
291 res.sendfile = function(path, options, fn){
292   var self = this
293     , req = self.req
294     , next = this.req.next
295     , options = options || {}
296     , done;
298   // support function as second arg
299   if ('function' == typeof options) {
300     fn = options;
301     options = {};
302   }
304   // socket errors
305   req.socket.on('error', error);
307   // errors
308   function error(err) {
309     if (done) return;
310     done = true;
312     // clean up
313     cleanup();
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;
322     // delegate
323     next(err);
324   }
326   // streaming
327   function stream(stream) {
328     if (done) return;
329     cleanup();
330     if (fn) stream.on('end', fn);
331   }
333   // cleanup
334   function cleanup() {
335     req.socket.removeListener('error', error);
336   }
338   // transfer
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);
345   file.pipe(this);
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
362  * @api public
363  */
365 res.download = function(path, filename, fn){
366   // support function as second arg
367   if ('function' == typeof filename) {
368     fn = filename;
369     filename = null;
370   }
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.
381  * Examples:
383  *     res.type('.html');
384  *     res.type('html');
385  *     res.type('json');
386  *     res.type('application/json');
387  *     res.type('png');
389  * @param {String} type
390  * @return {ServerResponse} for chaining
391  * @api public
392  */
394 res.contentType =
395 res.type = function(type){
396   return this.set('Content-Type', ~type.indexOf('/')
397     ? type
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', ...)`.
416  *    res.format({
417  *      'text/plain': function(){
418  *        res.send('hey');
419  *      },
421  *      'text/html': function(){
422  *        res.send('<p>hey</p>');
423  *      },
425  *      'appliation/json': function(){
426  *        res.send({ message: 'hey' });
427  *      }
428  *    });
430  * In addition to canonicalized MIME types you may
431  * also use extnames mapped to these types:
433  *    res.format({
434  *      text: function(){
435  *        res.send('hey');
436  *      },
438  *      html: function(){
439  *        res.send('<p>hey</p>');
440  *      },
442  *      json: function(){
443  *        res.send({ message: 'hey' });
444  *      }
445  *    });
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
451  * instead.
453  * @param {Object} obj
454  * @return {ServerResponse} for chaining
455  * @api public
456  */
458 res.format = function(obj){
459   var req = this.req
460     , next = req.next;
462   var fn = obj.default;
463   if (fn) delete obj.default;
464   var keys = Object.keys(obj);
466   var key = req.accepts(keys);
468   this.vary("Accept");
470   if (key) {
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);
476   } else if (fn) {
477     fn();
478   } else {
479     var err = new Error('Not Acceptable');
480     err.status = 406;
481     err.types = normalizeTypes(keys).map(function(o){ return o.value });
482     next(err);
483   }
485   return this;
489  * Set _Content-Disposition_ header to _attachment_ with optional `filename`.
491  * @param {String} filename
492  * @return {ServerResponse}
493  * @api public
494  */
496 res.attachment = function(filename){
497   if (filename) this.type(extname(filename));
498   this.set('Content-Disposition', filename
499     ? 'attachment; filename="' + basename(filename) + '"'
500     : 'attachment');
501   return this;
505  * Set header `field` to `val`, or pass
506  * an object of header fields.
508  * Examples:
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
519  * @api public
520  */
522 res.set =
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);
528   } else {
529     for (var key in field) {
530       this.set(key, field[key]);
531     }
532   }
533   return this;
537  * Get value for header `field`.
539  * @param {String} field
540  * @return {String}
541  * @api public
542  */
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
554  * @api public
555  */
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)
561     : opts);
565  * Set cookie `name` to `val`, with the given `options`.
567  * Options:
569  *    - `maxAge`   max-age in milliseconds, converted to `expires`
570  *    - `signed`   sign the cookie
571  *    - `path`     defaults to "/"
573  * Examples:
575  *    // "Remember Me" for 15 minutes
576  *    res.cookie('rememberme', '1', { expires: new Date(Date.now() + 900000), httpOnly: true });
578  *    // save as above
579  *    res.cookie('rememberme', '1', { maxAge: 900000, httpOnly: true })
581  * @param {String} name
582  * @param {String|Object} val
583  * @param {Options} options
584  * @api public
585  */
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;
598   }
599   if (null == options.path) options.path = '/';
600   this.set('Set-Cookie', cookie.serialize(name, String(val), options));
601   return this;
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 "/".
611  * Examples:
613  *    res.location('/foo/bar').;
614  *    res.location('http://example.com');
615  *    res.location('../login'); // /blog/post/1 -> /blog/login
617  * Mounting:
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
631  * @api public
632  */
634 res.location = function(url){
635   var app = this.app
636     , req = this.req
637     , path;
639   // "back" is an alias for the referrer
640   if(url === 'back') url = req.get('Referrer') || '/';
642   // relative
643   if (!~url.indexOf('://') && 0 != url.indexOf('//')) {
644     // relative to path
645     if ('.' == url[0]) {
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]) {
651       path = app.path();
652       url = path + '/' + url;
653     }
654   }
656   // Respond
657   this.set('Location', url);
658   return this;
662  * Redirect to the given `url` with optional response `status`
663  * defaulting to 302.
665  * The resulting `url` is determined by `res.location()`, so
666  * it will play nicely with mounted apps, relative paths,
667  * `"back"` etc.
669  * Examples:
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
679  * @api public
680  */
682 res.redirect = function(url){
683   var head = 'HEAD' == this.req.method
684     , status = 302
685     , body;
687   // allow status / url
688   if (2 == arguments.length) {
689     if ('number' == typeof url) {
690       status = url;
691       url = arguments[1];
692     } else {
693       status = arguments[1];
694     }
695   }
697   // Set location header
698   this.location(url);
699   url = this.get('Location');
701   // Support text/{plain,html} by default
702   this.format({
703     text: function(){
704       body = statusCodes[status] + '. Redirecting to ' + encodeURI(url);
705     },
707     html: function(){
708       var u = utils.escape(url);
709       body = '<p>' + statusCodes[status] + '. Redirecting to <a href="' + u + '">' + u + '</a></p>';
710     },
712     default: function(){
713       body = '';
714     }
715   });
717   // Respond
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
729  * @api public
730  */
732 res.vary = function(field){
733   var self = this;
735   // nothing
736   if (!field) return this;
738   // array
739   if (Array.isArray(field)) {
740     field.forEach(function(field){
741       self.vary(field);
742     });
743     return;
744   }
746   var vary = this.get('Vary');
748   // append
749   if (vary) {
750     vary = vary.split(/ *, */);
751     if (!~vary.indexOf(field)) vary.push(field);
752     this.set('Vary', vary.join(', '));
753     return this;
754   }
756   // set
757   this.set('Vary', field);
758   return this;
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.
766  * Options:
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
774  * @api public
775  */
777 res.render = function(view, options, fn){
778   var self = this
779     , options = options || {}
780     , req = this.req
781     , app = req.app;
783   // support callback function as second arg
784   if ('function' == typeof options) {
785     fn = options, options = {};
786   }
788   // merge res.locals
789   options._locals = self.locals;
791   // default callback to respond
792   fn = fn || function(err, str){
793     if (err) return req.next(err);
794     self.send(str);
795   };
797   // render
798   app.render(view, options, fn);