/** * Module dependencies. */ var escapeHtml = require('escape-html'); var http = require('http'); var path = require('path'); var mixin = require('utils-merge'); var sign = require('cookie-signature').sign; var normalizeType = require('./utils').normalizeType; var normalizeTypes = require('./utils').normalizeTypes; var setCharset = require('./utils').setCharset; var contentDisposition = require('./utils').contentDisposition; var deprecate = require('./utils').deprecate; var statusCodes = http.STATUS_CODES; var cookie = require('cookie'); var send = require('send'); var basename = path.basename; var extname = path.extname; var mime = send.mime; var vary = require('vary'); /** * Response prototype. */ var res = module.exports = { __proto__: http.ServerResponse.prototype }; /** * Set status `code`. * * @param {Number} code * @return {ServerResponse} * @api public */ res.status = function(code){ this.statusCode = code; return this; }; /** * Set Link header field with the given `links`. * * Examples: * * res.links({ * next: 'http://api.example.com/users?page=2', * last: 'http://api.example.com/users?page=5' * }); * * @param {Object} links * @return {ServerResponse} * @api public */ res.links = function(links){ var link = this.get('Link') || ''; if (link) link += ', '; return this.set('Link', link + Object.keys(links).map(function(rel){ return '<' + links[rel] + '>; rel="' + rel + '"'; }).join(', ')); }; /** * Send a response. * * Examples: * * res.send(new Buffer('wahoo')); * res.send({ some: 'json' }); * res.send('
some html
'); * res.send(404, 'Sorry, cant find that'); * res.send(404); * * @api public */ res.send = function(body){ var req = this.req; var head = 'HEAD' == req.method; var type; var encoding; var len; // settings var app = this.app; // allow status / body if (2 == arguments.length) { // res.send(body, status) backwards compat if ('number' != typeof body && 'number' == typeof arguments[1]) { this.statusCode = arguments[1]; } else { this.statusCode = body; body = arguments[1]; } } switch (typeof body) { // response status case 'number': this.get('Content-Type') || this.type('txt'); this.statusCode = body; body = http.STATUS_CODES[body]; break; // string defaulting to html case 'string': if (!this.get('Content-Type')) this.type('html'); break; case 'boolean': case 'object': if (null == body) { body = ''; } else if (Buffer.isBuffer(body)) { this.get('Content-Type') || this.type('bin'); } else { return this.json(body); } break; } // write strings in utf-8 if ('string' === typeof body) { encoding = 'utf8'; type = this.get('Content-Type'); // reflect this in content-type if ('string' === typeof type) { this.set('Content-Type', setCharset(type, 'utf-8')); } } // populate Content-Length if (undefined !== body && !this.get('Content-Length')) { len = Buffer.isBuffer(body) ? body.length : Buffer.byteLength(body, encoding); this.set('Content-Length', len); } // ETag support var etag = len !== undefined && app.get('etag fn'); if (etag && ('GET' === req.method || 'HEAD' === req.method)) { if (!this.get('ETag')) { etag = etag(body, encoding); etag && this.set('ETag', etag); } } // freshness if (req.fresh) this.statusCode = 304; // strip irrelevant headers if (204 == this.statusCode || 304 == this.statusCode) { this.removeHeader('Content-Type'); this.removeHeader('Content-Length'); this.removeHeader('Transfer-Encoding'); body = ''; } // respond this.end((head ? null : body), encoding); 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'); * * @api public */ res.json = function(obj){ // allow status / body if (2 == arguments.length) { // res.json(body, status) backwards compat if ('number' == typeof arguments[1]) { this.statusCode = arguments[1]; return 'number' === typeof obj ? jsonNumDeprecated.call(this, obj) : jsonDeprecated.call(this, obj); } else { this.statusCode = obj; obj = arguments[1]; } } // settings var app = this.app; var replacer = app.get('json replacer'); var spaces = app.get('json spaces'); var body = JSON.stringify(obj, replacer, spaces); // content-type this.get('Content-Type') || this.set('Content-Type', 'application/json'); return this.send(body); }; var jsonDeprecated = deprecate(res.json, 'res.json(obj, status): Use res.json(status, obj) instead'); var jsonNumDeprecated = deprecate(res.json, 'res.json(num, status): Use res.status(status).json(num) instead'); /** * Send JSON response with JSONP callback support. * * Examples: * * res.jsonp(null); * res.jsonp({ user: 'tj' }); * res.jsonp(500, 'oh noes!'); * res.jsonp(404, 'I dont have that'); * * @api public */ res.jsonp = function(obj){ // allow status / body if (2 == arguments.length) { // res.json(body, status) backwards compat if ('number' == typeof arguments[1]) { this.statusCode = arguments[1]; return 'number' === typeof obj ? jsonpNumDeprecated.call(this, obj) : jsonpDeprecated.call(this, obj); } else { this.statusCode = obj; obj = arguments[1]; } } // settings var app = this.app; var replacer = app.get('json replacer'); var spaces = app.get('json spaces'); var body = JSON.stringify(obj, replacer, spaces) .replace(/\u2028/g, '\\u2028') .replace(/\u2029/g, '\\u2029'); var callback = this.req.query[app.get('jsonp callback name')]; // content-type this.get('Content-Type') || this.set('Content-Type', 'application/json'); // fixup callback if (Array.isArray(callback)) { callback = callback[0]; } // jsonp if (callback && 'string' === typeof callback) { this.set('Content-Type', 'text/javascript'); var cb = callback.replace(/[^\[\]\w$.]/g, ''); body = 'typeof ' + cb + ' === \'function\' && ' + cb + '(' + body + ');'; } return this.send(body); }; var jsonpDeprecated = deprecate(res.json, 'res.jsonp(obj, status): Use res.jsonp(status, obj) instead'); var jsonpNumDeprecated = deprecate(res.json, 'res.jsonp(num, status): Use res.status(status).jsonp(num) instead'); /** * 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 * - `hidden` serve hidden files, defaulting to false * * Other options are passed along to `send`. * * Examples: * * The following example illustrates how `res.sendfile()` may * be used as an alternative for the `static()` middleware for * dynamic situations. The code backing `res.sendfile()` is actually * the same code, so HTTP cache support etc is identical. * * app.get('/user/:uid/photos/:file', function(req, res){ * var uid = req.params.uid * , file = req.params.file; * * req.user.mayViewFilesFrom(uid, function(yes){ * if (yes) { * res.sendfile('/uploads/' + uid + '/' + file); * } else { * res.send(403, 'Sorry! you cant see that.'); * } * }); * }); * * @api public */ res.sendfile = function(path, options, fn){ options = options || {}; var self = this; var req = self.req; var next = this.req.next; var done; // support function as second arg if ('function' == typeof options) { fn = options; options = {}; } // socket errors req.socket.on('error', error); // errors function error(err) { if (done) return; done = true; // clean up cleanup(); if (!self.headersSent) self.removeHeader('Content-Disposition'); // callback available if (fn) return fn(err); // list in limbo if there's no callback if (self.headersSent) return; // delegate next(err); } // streaming function stream(stream) { if (done) return; cleanup(); if (fn) stream.on('end', fn); } // cleanup function cleanup() { req.socket.removeListener('error', error); } // Back-compat options.maxage = options.maxage || options.maxAge || 0; // transfer var file = send(req, path, options); file.on('error', error); file.on('directory', next); file.on('stream', stream); file.pipe(this); this.on('finish', cleanup); }; /** * 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.headersSent` if you plan to respond. * * This method uses `res.sendfile()`. * * @api public */ res.download = function(path, filename, fn){ // support function as second arg if ('function' == typeof filename) { fn = filename; filename = null; } filename = filename || path; this.set('Content-Disposition', contentDisposition(filename)); return this.sendfile(path, fn); }; /** * Set _Content-Type_ response header with `type` through `mime.lookup()` * when it does not contain "/", or set the Content-Type to `type` otherwise. * * Examples: * * res.type('.html'); * res.type('html'); * res.type('json'); * res.type('application/json'); * res.type('png'); * * @param {String} type * @return {ServerResponse} for chaining * @api public */ res.contentType = res.type = function(type){ return this.set('Content-Type', ~type.indexOf('/') ? type : mime.lookup(type)); }; /** * Respond to the Acceptable formats using an `obj` * of mime-type callbacks. * * This method uses `req.accepted`, an array of * acceptable types ordered by their quality values. * When "Accept" is not present the _first_ callback * is invoked, otherwise the first match is used. When * no match is performed the server responds with * 406 "Not Acceptable". * * Content-Type is set for you, however if you choose * you may alter this within the callback using `res.type()` * or `res.set('Content-Type', ...)`. * * res.format({ * 'text/plain': function(){ * res.send('hey'); * }, * * 'text/html': function(){ * res.send('hey
'); * }, * * 'appliation/json': function(){ * res.send({ message: 'hey' }); * } * }); * * In addition to canonicalized MIME types you may * also use extnames mapped to these types: * * res.format({ * text: function(){ * res.send('hey'); * }, * * html: function(){ * res.send('hey
'); * }, * * json: function(){ * res.send({ message: 'hey' }); * } * }); * * By default Express passes an `Error` * with a `.status` of 406 to `next(err)` * if a match is not made. If you provide * a `.default` callback it will be invoked * instead. * * @param {Object} obj * @return {ServerResponse} for chaining * @api public */ res.format = function(obj){ var req = this.req; var next = req.next; var fn = obj.default; if (fn) delete obj.default; var keys = Object.keys(obj); var key = req.accepts(keys); this.vary("Accept"); if (key) { this.set('Content-Type', normalizeType(key).value); obj[key](req, this, next); } else if (fn) { fn(); } else { var err = new Error('Not Acceptable'); err.status = 406; err.types = normalizeTypes(keys).map(function(o){ return o.value }); next(err); } return this; }; /** * Set _Content-Disposition_ header to _attachment_ with optional `filename`. * * @param {String} filename * @return {ServerResponse} * @api public */ res.attachment = function(filename){ if (filename) this.type(extname(filename)); this.set('Content-Disposition', contentDisposition(filename)); return this; }; /** * Set header `field` to `val`, or pass * an object of header fields. * * Examples: * * res.set('Foo', ['bar', 'baz']); * res.set('Accept', 'application/json'); * res.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' }); * * Aliased as `res.header()`. * * @param {String|Object|Array} field * @param {String} val * @return {ServerResponse} for chaining * @api public */ res.set = res.header = function(field, val){ if (2 == arguments.length) { if (Array.isArray(val)) val = val.map(String); else val = String(val); if ('content-type' == field.toLowerCase() && !/;\s*charset\s*=/.test(val)) { var charset = mime.charsets.lookup(val.split(';')[0]); if (charset) val += '; charset=' + charset.toLowerCase(); } this.setHeader(field, val); } else { for (var key in field) { this.set(key, field[key]); } } return this; }; /** * Get value for header `field`. * * @param {String} field * @return {String} * @api public */ res.get = function(field){ return this.getHeader(field); }; /** * 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), path: '/' }; return this.cookie(name, '', options ? mixin(opts, options) : opts); }; /** * Set cookie `name` to `val`, with the given `options`. * * Options: * * - `maxAge` max-age in milliseconds, converted to `expires` * - `signed` sign the cookie * - `path` defaults to "/" * * 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|Object} val * @param {Options} options * @api public */ res.cookie = function(name, val, options){ options = mixin({}, options); var secret = this.req.secret; var signed = options.signed; if (signed && !secret) throw new Error('cookieParser("secret") required for signed cookies'); if ('number' == typeof val) val = val.toString(); if ('object' == typeof val) val = 'j:' + JSON.stringify(val); if (signed) val = 's:' + sign(val, secret); if ('maxAge' in options) { options.expires = new Date(Date.now() + options.maxAge); options.maxAge /= 1000; } if (null == options.path) options.path = '/'; var headerVal = cookie.serialize(name, String(val), options); // supports multiple 'res.cookie' calls by getting previous value var prev = this.get('Set-Cookie'); if (prev) { if (Array.isArray(prev)) { headerVal = prev.concat(headerVal); } else { headerVal = [prev, headerVal]; } } this.set('Set-Cookie', headerVal); return this; }; /** * Set the location header to `url`. * * The given `url` can also be "back", which redirects * to the _Referrer_ or _Referer_ headers or "/". * * Examples: * * res.location('/foo/bar').; * res.location('http://example.com'); * res.location('../login'); * * @param {String} url * @api public */ res.location = function(url){ var req = this.req; // "back" is an alias for the referrer if ('back' == url) url = req.get('Referrer') || '/'; // Respond this.set('Location', url); return this; }; /** * Redirect to the given `url` with optional response `status` * defaulting to 302. * * The resulting `url` is determined by `res.location()`, so * it will play nicely with mounted apps, relative paths, * `"back"` etc. * * Examples: * * res.redirect('/foo/bar'); * res.redirect('http://example.com'); * res.redirect(301, 'http://example.com'); * res.redirect('http://example.com', 301); * res.redirect('../login'); // /blog/post/1 -> /blog/login * * @param {String} url * @param {Number} code * @api public */ res.redirect = function(url){ var head = 'HEAD' == this.req.method; var status = 302; var body; // allow status / url if (2 == arguments.length) { if ('number' == typeof url) { status = url; url = arguments[1]; } else { status = arguments[1]; } } // Set location header this.location(url); url = this.get('Location'); // Support text/{plain,html} by default this.format({ text: function(){ body = statusCodes[status] + '. Redirecting to ' + encodeURI(url); }, html: function(){ var u = escapeHtml(url); body = '' + statusCodes[status] + '. Redirecting to ' + u + '
'; }, default: function(){ body = ''; } }); // Respond this.statusCode = status; this.set('Content-Length', Buffer.byteLength(body)); this.end(head ? null : body); }; /** * Add `field` to Vary. If already present in the Vary set, then * this call is simply ignored. * * @param {Array|String} field * @param {ServerResponse} for chaining * @api public */ res.vary = function(field){ // checks for back-compat if (!field) return this; if (Array.isArray(field) && !field.length) return this; vary(this, field); return this; }; /** * Render `view` with the given `options` and optional callback `fn`. * When a callback function is given a response will _not_ be made * automatically, otherwise a response of _200_ and _text/html_ is given. * * Options: * * - `cache` boolean hinting to the engine it should cache * - `filename` filename of the view being rendered * * @api public */ res.render = function(view, options, fn){ options = options || {}; var self = this; var req = this.req; var app = req.app; // support callback function as second arg if ('function' == typeof options) { fn = options, options = {}; } // merge res.locals options._locals = self.locals; // default callback to respond fn = fn || function(err, str){ if (err) return req.next(err); self.send(str); }; // render app.render(view, options, fn); };