3 * Copyright(c) 2009-2013 TJ Holowaychuk
4 * Copyright(c) 2014-2015 Douglas Christopher Wilson
11 * Module dependencies.
15 var contentDisposition = require('content-disposition');
16 var createError = require('http-errors')
17 var encodeUrl = require('encodeurl');
18 var escapeHtml = require('escape-html');
19 var http = require('http');
20 var onFinished = require('on-finished');
21 var mime = require('mime-types')
22 var path = require('path');
23 var pathIsAbsolute = require('path').isAbsolute;
24 var statuses = require('statuses')
25 var merge = require('utils-merge');
26 var sign = require('cookie-signature').sign;
27 var normalizeType = require('./utils').normalizeType;
28 var normalizeTypes = require('./utils').normalizeTypes;
29 var setCharset = require('./utils').setCharset;
30 var cookie = require('cookie');
31 var send = require('send');
32 var extname = path.extname;
33 var resolve = path.resolve;
34 var vary = require('vary');
41 var res = Object.create(http.ServerResponse.prototype)
51 * Set the HTTP status code for the response.
53 * Expects an integer value between 100 and 999 inclusive.
54 * Throws an error if the provided status code is not an integer or if it's outside the allowable range.
56 * @param {number} code - The HTTP status code to set.
57 * @return {ServerResponse} - Returns itself for chaining methods.
58 * @throws {TypeError} If `code` is not an integer.
59 * @throws {RangeError} If `code` is outside the range 100 to 999.
63 res.status = function status(code) {
64 // Check if the status code is not an integer
65 if (!Number.isInteger(code)) {
66 throw new TypeError(`Invalid status code: ${JSON.stringify(code)}. Status code must be an integer.`);
68 // Check if the status code is outside of Node's valid range
69 if (code < 100 || code > 999) {
70 throw new RangeError(`Invalid status code: ${JSON.stringify(code)}. Status code must be greater than 99 and less than 1000.`);
73 this.statusCode = code;
78 * Set Link header field with the given `links`.
83 * next: 'http://api.example.com/users?page=2',
84 * last: 'http://api.example.com/users?page=5'
87 * @param {Object} links
88 * @return {ServerResponse}
92 res.links = function(links){
93 var link = this.get('Link') || '';
94 if (link) link += ', ';
95 return this.set('Link', link + Object.keys(links).map(function(rel){
96 return '<' + links[rel] + '>; rel="' + rel + '"';
105 * res.send(Buffer.from('wahoo'));
106 * res.send({ some: 'json' });
107 * res.send('<p>some html</p>');
109 * @param {string|number|boolean|object|Buffer} body
113 res.send = function send(body) {
122 switch (typeof chunk) {
123 // string defaulting to html
125 if (!this.get('Content-Type')) {
132 if (chunk === null) {
134 } else if (Buffer.isBuffer(chunk)) {
135 if (!this.get('Content-Type')) {
139 return this.json(chunk);
144 // write strings in utf-8
145 if (typeof chunk === 'string') {
147 type = this.get('Content-Type');
149 // reflect this in content-type
150 if (typeof type === 'string') {
151 this.set('Content-Type', setCharset(type, 'utf-8'));
155 // determine if ETag should be generated
156 var etagFn = app.get('etag fn')
157 var generateETag = !this.get('ETag') && typeof etagFn === 'function'
159 // populate Content-Length
161 if (chunk !== undefined) {
162 if (Buffer.isBuffer(chunk)) {
163 // get length of Buffer
165 } else if (!generateETag && chunk.length < 1000) {
166 // just calculate length when no ETag + small chunk
167 len = Buffer.byteLength(chunk, encoding)
169 // convert chunk to Buffer and calculate
170 chunk = Buffer.from(chunk, encoding)
171 encoding = undefined;
175 this.set('Content-Length', len);
180 if (generateETag && len !== undefined) {
181 if ((etag = etagFn(chunk, encoding))) {
182 this.set('ETag', etag);
187 if (req.fresh) this.status(304);
189 // strip irrelevant headers
190 if (204 === this.statusCode || 304 === this.statusCode) {
191 this.removeHeader('Content-Type');
192 this.removeHeader('Content-Length');
193 this.removeHeader('Transfer-Encoding');
197 // alter headers for 205
198 if (this.statusCode === 205) {
199 this.set('Content-Length', '0')
200 this.removeHeader('Transfer-Encoding')
204 if (req.method === 'HEAD') {
205 // skip body for HEAD
209 this.end(chunk, encoding);
216 * Send JSON response.
221 * res.json({ user: 'tj' });
223 * @param {string|number|boolean|object} obj
227 res.json = function json(obj) {
230 var escape = app.get('json escape')
231 var replacer = app.get('json replacer');
232 var spaces = app.get('json spaces');
233 var body = stringify(obj, replacer, spaces, escape)
236 if (!this.get('Content-Type')) {
237 this.set('Content-Type', 'application/json');
240 return this.send(body);
244 * Send JSON response with JSONP callback support.
249 * res.jsonp({ user: 'tj' });
251 * @param {string|number|boolean|object} obj
255 res.jsonp = function jsonp(obj) {
258 var escape = app.get('json escape')
259 var replacer = app.get('json replacer');
260 var spaces = app.get('json spaces');
261 var body = stringify(obj, replacer, spaces, escape)
262 var callback = this.req.query[app.get('jsonp callback name')];
265 if (!this.get('Content-Type')) {
266 this.set('X-Content-Type-Options', 'nosniff');
267 this.set('Content-Type', 'application/json');
271 if (Array.isArray(callback)) {
272 callback = callback[0];
276 if (typeof callback === 'string' && callback.length !== 0) {
277 this.set('X-Content-Type-Options', 'nosniff');
278 this.set('Content-Type', 'text/javascript');
280 // restrict callback charset
281 callback = callback.replace(/[^\[\]\w$.]/g, '');
283 if (body === undefined) {
286 } else if (typeof body === 'string') {
287 // replace chars not allowed in JavaScript that are in JSON
289 .replace(/\u2028/g, '\\u2028')
290 .replace(/\u2029/g, '\\u2029')
293 // the /**/ is a specific security mitigation for "Rosetta Flash JSONP abuse"
294 // the typeof check is just to reduce client error noise
295 body = '/**/ typeof ' + callback + ' === \'function\' && ' + callback + '(' + body + ');';
298 return this.send(body);
302 * Send given HTTP status code.
304 * Sets the response status to `statusCode` and the body of the
305 * response to the standard description from node's http.STATUS_CODES
306 * or the statusCode number if no description.
310 * res.sendStatus(200);
312 * @param {number} statusCode
316 res.sendStatus = function sendStatus(statusCode) {
317 var body = statuses.message[statusCode] || String(statusCode)
319 this.status(statusCode);
322 return this.send(body);
326 * Transfer the file at the given `path`.
328 * Automatically sets the _Content-Type_ response header field.
329 * The callback `callback(err)` is invoked when the transfer is complete
330 * or when an error occurs. Be sure to check `res.headersSent`
331 * if you wish to attempt responding, as the header and some data
332 * may have already been transferred.
336 * - `maxAge` defaulting to 0 (can be string converted by `ms`)
337 * - `root` root directory for relative filenames
338 * - `headers` object of headers to serve with file
339 * - `dotfiles` serve dotfiles, defaulting to false; can be `"allow"` to send them
341 * Other options are passed along to `send`.
345 * The following example illustrates how `res.sendFile()` may
346 * be used as an alternative for the `static()` middleware for
347 * dynamic situations. The code backing `res.sendFile()` is actually
348 * the same code, so HTTP cache support etc is identical.
350 * app.get('/user/:uid/photos/:file', function(req, res){
351 * var uid = req.params.uid
352 * , file = req.params.file;
354 * req.user.mayViewFilesFrom(uid, function(yes){
356 * res.sendFile('/uploads/' + uid + '/' + file);
358 * res.send(403, 'Sorry! you cant see that.');
366 res.sendFile = function sendFile(path, options, callback) {
371 var opts = options || {};
374 throw new TypeError('path argument is required to res.sendFile');
377 if (typeof path !== 'string') {
378 throw new TypeError('path must be a string to res.sendFile')
381 // support function as second arg
382 if (typeof options === 'function') {
387 if (!opts.root && !pathIsAbsolute(path)) {
388 throw new TypeError('path must be absolute or specify root to res.sendFile');
391 // create file stream
392 var pathname = encodeURI(path);
393 var file = send(req, pathname, opts);
396 sendfile(res, file, opts, function (err) {
397 if (done) return done(err);
398 if (err && err.code === 'EISDIR') return next();
400 // next() all but write errors
401 if (err && err.code !== 'ECONNABORTED' && err.syscall !== 'write') {
408 * Transfer the file at the given `path` as an attachment.
410 * Optionally providing an alternate attachment `filename`,
411 * and optional callback `callback(err)`. The callback is invoked
412 * when the data transfer is complete, or when an error has
413 * occurred. Be sure to check `res.headersSent` if you plan to respond.
415 * Optionally providing an `options` object to use with `res.sendFile()`.
416 * This function will set the `Content-Disposition` header, overriding
417 * any `Content-Disposition` header passed as header options in order
418 * to set the attachment and filename.
420 * This method uses `res.sendFile()`.
425 res.download = function download (path, filename, options, callback) {
428 var opts = options || null
430 // support function as second or third arg
431 if (typeof filename === 'function') {
435 } else if (typeof options === 'function') {
440 // support optional filename, where options may be in it's place
441 if (typeof filename === 'object' &&
442 (typeof options === 'function' || options === undefined)) {
447 // set Content-Disposition when file is sent
449 'Content-Disposition': contentDisposition(name || path)
452 // merge user-provided headers
453 if (opts && opts.headers) {
454 var keys = Object.keys(opts.headers)
455 for (var i = 0; i < keys.length; i++) {
457 if (key.toLowerCase() !== 'content-disposition') {
458 headers[key] = opts.headers[key]
463 // merge user-provided options
464 opts = Object.create(opts)
465 opts.headers = headers
467 // Resolve the full path for sendFile
468 var fullPath = !opts.root
473 return this.sendFile(fullPath, opts, done)
477 * Set _Content-Type_ response header with `type` through `mime.contentType()`
478 * when it does not contain "/", or set the Content-Type to `type` otherwise.
479 * When no mapping is found though `mime.contentType()`, the type is set to
480 * "application/octet-stream".
487 * res.type('application/json');
490 * @param {String} type
491 * @return {ServerResponse} for chaining
496 res.type = function contentType(type) {
497 var ct = type.indexOf('/') === -1
498 ? (mime.contentType(type) || 'application/octet-stream')
501 return this.set('Content-Type', ct);
505 * Respond to the Acceptable formats using an `obj`
506 * of mime-type callbacks.
508 * This method uses `req.accepted`, an array of
509 * acceptable types ordered by their quality values.
510 * When "Accept" is not present the _first_ callback
511 * is invoked, otherwise the first match is used. When
512 * no match is performed the server responds with
513 * 406 "Not Acceptable".
515 * Content-Type is set for you, however if you choose
516 * you may alter this within the callback using `res.type()`
517 * or `res.set('Content-Type', ...)`.
520 * 'text/plain': function(){
524 * 'text/html': function(){
525 * res.send('<p>hey</p>');
528 * 'application/json': function () {
529 * res.send({ message: 'hey' });
533 * In addition to canonicalized MIME types you may
534 * also use extnames mapped to these types:
542 * res.send('<p>hey</p>');
546 * res.send({ message: 'hey' });
550 * By default Express passes an `Error`
551 * with a `.status` of 406 to `next(err)`
552 * if a match is not made. If you provide
553 * a `.default` callback it will be invoked
556 * @param {Object} obj
557 * @return {ServerResponse} for chaining
561 res.format = function(obj){
565 var keys = Object.keys(obj)
566 .filter(function (v) { return v !== 'default' })
568 var key = keys.length > 0
575 this.set('Content-Type', normalizeType(key).value);
576 obj[key](req, this, next);
577 } else if (obj.default) {
578 obj.default(req, this, next)
580 next(createError(406, {
581 types: normalizeTypes(keys).map(function (o) { return o.value })
589 * Set _Content-Disposition_ header to _attachment_ with optional `filename`.
591 * @param {String} filename
592 * @return {ServerResponse}
596 res.attachment = function attachment(filename) {
598 this.type(extname(filename));
601 this.set('Content-Disposition', contentDisposition(filename));
607 * Append additional header `field` with value `val`.
611 * res.append('Link', ['<http://localhost/>', '<http://localhost:3000/>']);
612 * res.append('Set-Cookie', 'foo=bar; Path=/; HttpOnly');
613 * res.append('Warning', '199 Miscellaneous warning');
615 * @param {String} field
616 * @param {String|Array} val
617 * @return {ServerResponse} for chaining
621 res.append = function append(field, val) {
622 var prev = this.get(field);
626 // concat the new and prev vals
627 value = Array.isArray(prev) ? prev.concat(val)
628 : Array.isArray(val) ? [prev].concat(val)
632 return this.set(field, value);
636 * Set header `field` to `val`, or pass
637 * an object of header fields.
641 * res.set('Foo', ['bar', 'baz']);
642 * res.set('Accept', 'application/json');
643 * res.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' });
645 * Aliased as `res.header()`.
647 * When the set header is "Content-Type", the type is expanded to include
648 * the charset if not present using `mime.contentType()`.
650 * @param {String|Object} field
651 * @param {String|Array} val
652 * @return {ServerResponse} for chaining
657 res.header = function header(field, val) {
658 if (arguments.length === 2) {
659 var value = Array.isArray(val)
663 // add charset to content-type
664 if (field.toLowerCase() === 'content-type') {
665 if (Array.isArray(value)) {
666 throw new TypeError('Content-Type cannot be set to an Array');
668 value = mime.contentType(value)
671 this.setHeader(field, value);
673 for (var key in field) {
674 this.set(key, field[key]);
681 * Get value for header `field`.
683 * @param {String} field
688 res.get = function(field){
689 return this.getHeader(field);
693 * Clear cookie `name`.
695 * @param {String} name
696 * @param {Object} [options]
697 * @return {ServerResponse} for chaining
701 res.clearCookie = function clearCookie(name, options) {
702 // Force cookie expiration by setting expires to the past
703 const opts = { path: '/', ...options, expires: new Date(1)};
704 // ensure maxAge is not passed
707 return this.cookie(name, '', opts);
711 * Set cookie `name` to `value`, with the given `options`.
715 * - `maxAge` max-age in milliseconds, converted to `expires`
716 * - `signed` sign the cookie
717 * - `path` defaults to "/"
721 * // "Remember Me" for 15 minutes
722 * res.cookie('rememberme', '1', { expires: new Date(Date.now() + 900000), httpOnly: true });
725 * res.cookie('rememberme', '1', { maxAge: 900000, httpOnly: true })
727 * @param {String} name
728 * @param {String|Object} value
729 * @param {Object} [options]
730 * @return {ServerResponse} for chaining
734 res.cookie = function (name, value, options) {
735 var opts = merge({}, options);
736 var secret = this.req.secret;
737 var signed = opts.signed;
739 if (signed && !secret) {
740 throw new Error('cookieParser("secret") required for signed cookies');
743 var val = typeof value === 'object'
744 ? 'j:' + JSON.stringify(value)
748 val = 's:' + sign(val, secret);
751 if (opts.maxAge != null) {
752 var maxAge = opts.maxAge - 0
754 if (!isNaN(maxAge)) {
755 opts.expires = new Date(Date.now() + maxAge)
756 opts.maxAge = Math.floor(maxAge / 1000)
760 if (opts.path == null) {
764 this.append('Set-Cookie', cookie.serialize(name, String(val), opts));
770 * Set the location header to `url`.
772 * The given `url` can also be "back", which redirects
773 * to the _Referrer_ or _Referer_ headers or "/".
777 * res.location('/foo/bar').;
778 * res.location('http://example.com');
779 * res.location('../login');
781 * @param {String} url
782 * @return {ServerResponse} for chaining
786 res.location = function location(url) {
787 return this.set('Location', encodeUrl(url));
791 * Redirect to the given `url` with optional response `status`
796 * res.redirect('/foo/bar');
797 * res.redirect('http://example.com');
798 * res.redirect(301, 'http://example.com');
799 * res.redirect('../login'); // /blog/post/1 -> /blog/login
804 res.redirect = function redirect(url) {
809 // allow status / url
810 if (arguments.length === 2) {
811 status = arguments[0]
812 address = arguments[1]
815 // Set location header
816 address = this.location(address).get('Location');
818 // Support text/{plain,html} by default
821 body = statuses.message[status] + '. Redirecting to ' + address
825 var u = escapeHtml(address);
826 body = '<p>' + statuses.message[status] + '. Redirecting to ' + u + '</p>'
836 this.set('Content-Length', Buffer.byteLength(body));
838 if (this.req.method === 'HEAD') {
846 * Add `field` to Vary. If already present in the Vary set, then
847 * this call is simply ignored.
849 * @param {Array|String} field
850 * @return {ServerResponse} for chaining
854 res.vary = function(field){
861 * Render `view` with the given `options` and optional callback `fn`.
862 * When a callback function is given a response will _not_ be made
863 * automatically, otherwise a response of _200_ and _text/html_ is given.
867 * - `cache` boolean hinting to the engine it should cache
868 * - `filename` filename of the view being rendered
873 res.render = function render(view, options, callback) {
874 var app = this.req.app;
876 var opts = options || {};
880 // support callback function as second arg
881 if (typeof options === 'function') {
887 opts._locals = self.locals;
889 // default callback to respond
890 done = done || function (err, str) {
891 if (err) return req.next(err);
896 app.render(view, opts, done);
899 // pipe the send file stream
900 function sendfile(res, file, options, callback) {
905 function onaborted() {
909 var err = new Error('Request aborted');
910 err.code = 'ECONNABORTED';
915 function ondirectory() {
919 var err = new Error('EISDIR, read');
925 function onerror(err) {
944 function onfinish(err) {
945 if (err && err.code === 'ECONNRESET') return onaborted();
946 if (err) return onerror(err);
949 setImmediate(function () {
950 if (streaming !== false && !done) {
962 function onstream() {
966 file.on('directory', ondirectory);
967 file.on('end', onend);
968 file.on('error', onerror);
969 file.on('file', onfile);
970 file.on('stream', onstream);
971 onFinished(res, onfinish);
973 if (options.headers) {
974 // set headers on successful transfer
975 file.on('headers', function headers(res) {
976 var obj = options.headers;
977 var keys = Object.keys(obj);
979 for (var i = 0; i < keys.length; i++) {
981 res.setHeader(k, obj[k]);
991 * Stringify JSON, like JSON.stringify, but v8 optimized, with the
992 * ability to escape characters that can trigger HTML sniffing.
995 * @param {function} replacer
996 * @param {number} spaces
997 * @param {boolean} escape
1002 function stringify (value, replacer, spaces, escape) {
1003 // v8 checks arguments.length for optimizing simple call
1004 // https://bugs.chromium.org/p/v8/issues/detail?id=4730
1005 var json = replacer || spaces
1006 ? JSON.stringify(value, replacer, spaces)
1007 : JSON.stringify(value);
1009 if (escape && typeof json === 'string') {
1010 json = json.replace(/[<>&]/g, function (c) {
1011 switch (c.charCodeAt(0)) {
1018 /* istanbul ignore next: unreachable default */