3 * Copyright(c) 2009-2013 TJ Holowaychuk
4 * Copyright(c) 2014-2015 Douglas Christopher Wilson
11 * Module dependencies.
15 var Buffer = require('safe-buffer').Buffer
16 var contentDisposition = require('content-disposition');
17 var createError = require('http-errors')
18 var encodeUrl = require('encodeurl');
19 var escapeHtml = require('escape-html');
20 var http = require('http');
21 var onFinished = require('on-finished');
22 var mime = require('mime-types')
23 var path = require('path');
24 var pathIsAbsolute = require('path').isAbsolute;
25 var statuses = require('statuses')
26 var merge = require('utils-merge');
27 var sign = require('cookie-signature').sign;
28 var normalizeType = require('./utils').normalizeType;
29 var normalizeTypes = require('./utils').normalizeTypes;
30 var setCharset = require('./utils').setCharset;
31 var cookie = require('cookie');
32 var send = require('send');
33 var extname = path.extname;
34 var resolve = path.resolve;
35 var vary = require('vary');
42 var res = Object.create(http.ServerResponse.prototype)
52 * Set the HTTP status code for the response.
54 * Expects an integer value between 100 and 999 inclusive.
55 * Throws an error if the provided status code is not an integer or if it's outside the allowable range.
57 * @param {number} code - The HTTP status code to set.
58 * @return {ServerResponse} - Returns itself for chaining methods.
59 * @throws {TypeError} If `code` is not an integer.
60 * @throws {RangeError} If `code` is outside the range 100 to 999.
64 res.status = function status(code) {
65 // Check if the status code is not an integer
66 if (!Number.isInteger(code)) {
67 throw new TypeError(`Invalid status code: ${JSON.stringify(code)}. Status code must be an integer.`);
69 // Check if the status code is outside of Node's valid range
70 if (code < 100 || code > 999) {
71 throw new RangeError(`Invalid status code: ${JSON.stringify(code)}. Status code must be greater than 99 and less than 1000.`);
74 this.statusCode = code;
79 * Set Link header field with the given `links`.
84 * next: 'http://api.example.com/users?page=2',
85 * last: 'http://api.example.com/users?page=5'
88 * @param {Object} links
89 * @return {ServerResponse}
93 res.links = function(links){
94 var link = this.get('Link') || '';
95 if (link) link += ', ';
96 return this.set('Link', link + Object.keys(links).map(function(rel){
97 return '<' + links[rel] + '>; rel="' + rel + '"';
106 * res.send(Buffer.from('wahoo'));
107 * res.send({ some: 'json' });
108 * res.send('<p>some html</p>');
110 * @param {string|number|boolean|object|Buffer} body
114 res.send = function send(body) {
123 switch (typeof chunk) {
124 // string defaulting to html
126 if (!this.get('Content-Type')) {
133 if (chunk === null) {
135 } else if (Buffer.isBuffer(chunk)) {
136 if (!this.get('Content-Type')) {
140 return this.json(chunk);
145 // write strings in utf-8
146 if (typeof chunk === 'string') {
148 type = this.get('Content-Type');
150 // reflect this in content-type
151 if (typeof type === 'string') {
152 this.set('Content-Type', setCharset(type, 'utf-8'));
156 // determine if ETag should be generated
157 var etagFn = app.get('etag fn')
158 var generateETag = !this.get('ETag') && typeof etagFn === 'function'
160 // populate Content-Length
162 if (chunk !== undefined) {
163 if (Buffer.isBuffer(chunk)) {
164 // get length of Buffer
166 } else if (!generateETag && chunk.length < 1000) {
167 // just calculate length when no ETag + small chunk
168 len = Buffer.byteLength(chunk, encoding)
170 // convert chunk to Buffer and calculate
171 chunk = Buffer.from(chunk, encoding)
172 encoding = undefined;
176 this.set('Content-Length', len);
181 if (generateETag && len !== undefined) {
182 if ((etag = etagFn(chunk, encoding))) {
183 this.set('ETag', etag);
188 if (req.fresh) this.status(304);
190 // strip irrelevant headers
191 if (204 === this.statusCode || 304 === this.statusCode) {
192 this.removeHeader('Content-Type');
193 this.removeHeader('Content-Length');
194 this.removeHeader('Transfer-Encoding');
198 // alter headers for 205
199 if (this.statusCode === 205) {
200 this.set('Content-Length', '0')
201 this.removeHeader('Transfer-Encoding')
205 if (req.method === 'HEAD') {
206 // skip body for HEAD
210 this.end(chunk, encoding);
217 * Send JSON response.
222 * res.json({ user: 'tj' });
224 * @param {string|number|boolean|object} obj
228 res.json = function json(obj) {
231 var escape = app.get('json escape')
232 var replacer = app.get('json replacer');
233 var spaces = app.get('json spaces');
234 var body = stringify(obj, replacer, spaces, escape)
237 if (!this.get('Content-Type')) {
238 this.set('Content-Type', 'application/json');
241 return this.send(body);
245 * Send JSON response with JSONP callback support.
250 * res.jsonp({ user: 'tj' });
252 * @param {string|number|boolean|object} obj
256 res.jsonp = function jsonp(obj) {
259 var escape = app.get('json escape')
260 var replacer = app.get('json replacer');
261 var spaces = app.get('json spaces');
262 var body = stringify(obj, replacer, spaces, escape)
263 var callback = this.req.query[app.get('jsonp callback name')];
266 if (!this.get('Content-Type')) {
267 this.set('X-Content-Type-Options', 'nosniff');
268 this.set('Content-Type', 'application/json');
272 if (Array.isArray(callback)) {
273 callback = callback[0];
277 if (typeof callback === 'string' && callback.length !== 0) {
278 this.set('X-Content-Type-Options', 'nosniff');
279 this.set('Content-Type', 'text/javascript');
281 // restrict callback charset
282 callback = callback.replace(/[^\[\]\w$.]/g, '');
284 if (body === undefined) {
287 } else if (typeof body === 'string') {
288 // replace chars not allowed in JavaScript that are in JSON
290 .replace(/\u2028/g, '\\u2028')
291 .replace(/\u2029/g, '\\u2029')
294 // the /**/ is a specific security mitigation for "Rosetta Flash JSONP abuse"
295 // the typeof check is just to reduce client error noise
296 body = '/**/ typeof ' + callback + ' === \'function\' && ' + callback + '(' + body + ');';
299 return this.send(body);
303 * Send given HTTP status code.
305 * Sets the response status to `statusCode` and the body of the
306 * response to the standard description from node's http.STATUS_CODES
307 * or the statusCode number if no description.
311 * res.sendStatus(200);
313 * @param {number} statusCode
317 res.sendStatus = function sendStatus(statusCode) {
318 var body = statuses.message[statusCode] || String(statusCode)
320 this.status(statusCode);
323 return this.send(body);
327 * Transfer the file at the given `path`.
329 * Automatically sets the _Content-Type_ response header field.
330 * The callback `callback(err)` is invoked when the transfer is complete
331 * or when an error occurs. Be sure to check `res.headersSent`
332 * if you wish to attempt responding, as the header and some data
333 * may have already been transferred.
337 * - `maxAge` defaulting to 0 (can be string converted by `ms`)
338 * - `root` root directory for relative filenames
339 * - `headers` object of headers to serve with file
340 * - `dotfiles` serve dotfiles, defaulting to false; can be `"allow"` to send them
342 * Other options are passed along to `send`.
346 * The following example illustrates how `res.sendFile()` may
347 * be used as an alternative for the `static()` middleware for
348 * dynamic situations. The code backing `res.sendFile()` is actually
349 * the same code, so HTTP cache support etc is identical.
351 * app.get('/user/:uid/photos/:file', function(req, res){
352 * var uid = req.params.uid
353 * , file = req.params.file;
355 * req.user.mayViewFilesFrom(uid, function(yes){
357 * res.sendFile('/uploads/' + uid + '/' + file);
359 * res.send(403, 'Sorry! you cant see that.');
367 res.sendFile = function sendFile(path, options, callback) {
372 var opts = options || {};
375 throw new TypeError('path argument is required to res.sendFile');
378 if (typeof path !== 'string') {
379 throw new TypeError('path must be a string to res.sendFile')
382 // support function as second arg
383 if (typeof options === 'function') {
388 if (!opts.root && !pathIsAbsolute(path)) {
389 throw new TypeError('path must be absolute or specify root to res.sendFile');
392 // create file stream
393 var pathname = encodeURI(path);
394 var file = send(req, pathname, opts);
397 sendfile(res, file, opts, function (err) {
398 if (done) return done(err);
399 if (err && err.code === 'EISDIR') return next();
401 // next() all but write errors
402 if (err && err.code !== 'ECONNABORTED' && err.syscall !== 'write') {
409 * Transfer the file at the given `path` as an attachment.
411 * Optionally providing an alternate attachment `filename`,
412 * and optional callback `callback(err)`. The callback is invoked
413 * when the data transfer is complete, or when an error has
414 * occurred. Be sure to check `res.headersSent` if you plan to respond.
416 * Optionally providing an `options` object to use with `res.sendFile()`.
417 * This function will set the `Content-Disposition` header, overriding
418 * any `Content-Disposition` header passed as header options in order
419 * to set the attachment and filename.
421 * This method uses `res.sendFile()`.
426 res.download = function download (path, filename, options, callback) {
429 var opts = options || null
431 // support function as second or third arg
432 if (typeof filename === 'function') {
436 } else if (typeof options === 'function') {
441 // support optional filename, where options may be in it's place
442 if (typeof filename === 'object' &&
443 (typeof options === 'function' || options === undefined)) {
448 // set Content-Disposition when file is sent
450 'Content-Disposition': contentDisposition(name || path)
453 // merge user-provided headers
454 if (opts && opts.headers) {
455 var keys = Object.keys(opts.headers)
456 for (var i = 0; i < keys.length; i++) {
458 if (key.toLowerCase() !== 'content-disposition') {
459 headers[key] = opts.headers[key]
464 // merge user-provided options
465 opts = Object.create(opts)
466 opts.headers = headers
468 // Resolve the full path for sendFile
469 var fullPath = !opts.root
474 return this.sendFile(fullPath, opts, done)
478 * Set _Content-Type_ response header with `type` through `mime.contentType()`
479 * when it does not contain "/", or set the Content-Type to `type` otherwise.
480 * When no mapping is found though `mime.contentType()`, the type is set to
481 * "application/octet-stream".
488 * res.type('application/json');
491 * @param {String} type
492 * @return {ServerResponse} for chaining
497 res.type = function contentType(type) {
498 var ct = type.indexOf('/') === -1
499 ? (mime.contentType(type) || 'application/octet-stream')
502 return this.set('Content-Type', ct);
506 * Respond to the Acceptable formats using an `obj`
507 * of mime-type callbacks.
509 * This method uses `req.accepted`, an array of
510 * acceptable types ordered by their quality values.
511 * When "Accept" is not present the _first_ callback
512 * is invoked, otherwise the first match is used. When
513 * no match is performed the server responds with
514 * 406 "Not Acceptable".
516 * Content-Type is set for you, however if you choose
517 * you may alter this within the callback using `res.type()`
518 * or `res.set('Content-Type', ...)`.
521 * 'text/plain': function(){
525 * 'text/html': function(){
526 * res.send('<p>hey</p>');
529 * 'application/json': function () {
530 * res.send({ message: 'hey' });
534 * In addition to canonicalized MIME types you may
535 * also use extnames mapped to these types:
543 * res.send('<p>hey</p>');
547 * res.send({ message: 'hey' });
551 * By default Express passes an `Error`
552 * with a `.status` of 406 to `next(err)`
553 * if a match is not made. If you provide
554 * a `.default` callback it will be invoked
557 * @param {Object} obj
558 * @return {ServerResponse} for chaining
562 res.format = function(obj){
566 var keys = Object.keys(obj)
567 .filter(function (v) { return v !== 'default' })
569 var key = keys.length > 0
576 this.set('Content-Type', normalizeType(key).value);
577 obj[key](req, this, next);
578 } else if (obj.default) {
579 obj.default(req, this, next)
581 next(createError(406, {
582 types: normalizeTypes(keys).map(function (o) { return o.value })
590 * Set _Content-Disposition_ header to _attachment_ with optional `filename`.
592 * @param {String} filename
593 * @return {ServerResponse}
597 res.attachment = function attachment(filename) {
599 this.type(extname(filename));
602 this.set('Content-Disposition', contentDisposition(filename));
608 * Append additional header `field` with value `val`.
612 * res.append('Link', ['<http://localhost/>', '<http://localhost:3000/>']);
613 * res.append('Set-Cookie', 'foo=bar; Path=/; HttpOnly');
614 * res.append('Warning', '199 Miscellaneous warning');
616 * @param {String} field
617 * @param {String|Array} val
618 * @return {ServerResponse} for chaining
622 res.append = function append(field, val) {
623 var prev = this.get(field);
627 // concat the new and prev vals
628 value = Array.isArray(prev) ? prev.concat(val)
629 : Array.isArray(val) ? [prev].concat(val)
633 return this.set(field, value);
637 * Set header `field` to `val`, or pass
638 * an object of header fields.
642 * res.set('Foo', ['bar', 'baz']);
643 * res.set('Accept', 'application/json');
644 * res.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' });
646 * Aliased as `res.header()`.
648 * When the set header is "Content-Type", the type is expanded to include
649 * the charset if not present using `mime.contentType()`.
651 * @param {String|Object} field
652 * @param {String|Array} val
653 * @return {ServerResponse} for chaining
658 res.header = function header(field, val) {
659 if (arguments.length === 2) {
660 var value = Array.isArray(val)
664 // add charset to content-type
665 if (field.toLowerCase() === 'content-type') {
666 if (Array.isArray(value)) {
667 throw new TypeError('Content-Type cannot be set to an Array');
669 value = mime.contentType(value)
672 this.setHeader(field, value);
674 for (var key in field) {
675 this.set(key, field[key]);
682 * Get value for header `field`.
684 * @param {String} field
689 res.get = function(field){
690 return this.getHeader(field);
694 * Clear cookie `name`.
696 * @param {String} name
697 * @param {Object} [options]
698 * @return {ServerResponse} for chaining
702 res.clearCookie = function clearCookie(name, options) {
703 // Force cookie expiration by setting expires to the past
704 const opts = { path: '/', ...options, expires: new Date(1)};
705 // ensure maxAge is not passed
708 return this.cookie(name, '', opts);
712 * Set cookie `name` to `value`, with the given `options`.
716 * - `maxAge` max-age in milliseconds, converted to `expires`
717 * - `signed` sign the cookie
718 * - `path` defaults to "/"
722 * // "Remember Me" for 15 minutes
723 * res.cookie('rememberme', '1', { expires: new Date(Date.now() + 900000), httpOnly: true });
726 * res.cookie('rememberme', '1', { maxAge: 900000, httpOnly: true })
728 * @param {String} name
729 * @param {String|Object} value
730 * @param {Object} [options]
731 * @return {ServerResponse} for chaining
735 res.cookie = function (name, value, options) {
736 var opts = merge({}, options);
737 var secret = this.req.secret;
738 var signed = opts.signed;
740 if (signed && !secret) {
741 throw new Error('cookieParser("secret") required for signed cookies');
744 var val = typeof value === 'object'
745 ? 'j:' + JSON.stringify(value)
749 val = 's:' + sign(val, secret);
752 if (opts.maxAge != null) {
753 var maxAge = opts.maxAge - 0
755 if (!isNaN(maxAge)) {
756 opts.expires = new Date(Date.now() + maxAge)
757 opts.maxAge = Math.floor(maxAge / 1000)
761 if (opts.path == null) {
765 this.append('Set-Cookie', cookie.serialize(name, String(val), opts));
771 * Set the location header to `url`.
773 * The given `url` can also be "back", which redirects
774 * to the _Referrer_ or _Referer_ headers or "/".
778 * res.location('/foo/bar').;
779 * res.location('http://example.com');
780 * res.location('../login');
782 * @param {String} url
783 * @return {ServerResponse} for chaining
787 res.location = function location(url) {
788 return this.set('Location', encodeUrl(url));
792 * Redirect to the given `url` with optional response `status`
797 * res.redirect('/foo/bar');
798 * res.redirect('http://example.com');
799 * res.redirect(301, 'http://example.com');
800 * res.redirect('../login'); // /blog/post/1 -> /blog/login
805 res.redirect = function redirect(url) {
810 // allow status / url
811 if (arguments.length === 2) {
812 status = arguments[0]
813 address = arguments[1]
816 // Set location header
817 address = this.location(address).get('Location');
819 // Support text/{plain,html} by default
822 body = statuses.message[status] + '. Redirecting to ' + address
826 var u = escapeHtml(address);
827 body = '<p>' + statuses.message[status] + '. Redirecting to ' + u + '</p>'
837 this.set('Content-Length', Buffer.byteLength(body));
839 if (this.req.method === 'HEAD') {
847 * Add `field` to Vary. If already present in the Vary set, then
848 * this call is simply ignored.
850 * @param {Array|String} field
851 * @return {ServerResponse} for chaining
855 res.vary = function(field){
862 * Render `view` with the given `options` and optional callback `fn`.
863 * When a callback function is given a response will _not_ be made
864 * automatically, otherwise a response of _200_ and _text/html_ is given.
868 * - `cache` boolean hinting to the engine it should cache
869 * - `filename` filename of the view being rendered
874 res.render = function render(view, options, callback) {
875 var app = this.req.app;
877 var opts = options || {};
881 // support callback function as second arg
882 if (typeof options === 'function') {
888 opts._locals = self.locals;
890 // default callback to respond
891 done = done || function (err, str) {
892 if (err) return req.next(err);
897 app.render(view, opts, done);
900 // pipe the send file stream
901 function sendfile(res, file, options, callback) {
906 function onaborted() {
910 var err = new Error('Request aborted');
911 err.code = 'ECONNABORTED';
916 function ondirectory() {
920 var err = new Error('EISDIR, read');
926 function onerror(err) {
945 function onfinish(err) {
946 if (err && err.code === 'ECONNRESET') return onaborted();
947 if (err) return onerror(err);
950 setImmediate(function () {
951 if (streaming !== false && !done) {
963 function onstream() {
967 file.on('directory', ondirectory);
968 file.on('end', onend);
969 file.on('error', onerror);
970 file.on('file', onfile);
971 file.on('stream', onstream);
972 onFinished(res, onfinish);
974 if (options.headers) {
975 // set headers on successful transfer
976 file.on('headers', function headers(res) {
977 var obj = options.headers;
978 var keys = Object.keys(obj);
980 for (var i = 0; i < keys.length; i++) {
982 res.setHeader(k, obj[k]);
992 * Stringify JSON, like JSON.stringify, but v8 optimized, with the
993 * ability to escape characters that can trigger HTML sniffing.
996 * @param {function} replacer
997 * @param {number} spaces
998 * @param {boolean} escape
1003 function stringify (value, replacer, spaces, escape) {
1004 // v8 checks arguments.length for optimizing simple call
1005 // https://bugs.chromium.org/p/v8/issues/detail?id=4730
1006 var json = replacer || spaces
1007 ? JSON.stringify(value, replacer, spaces)
1008 : JSON.stringify(value);
1010 if (escape && typeof json === 'string') {
1011 json = json.replace(/[<>&]/g, function (c) {
1012 switch (c.charCodeAt(0)) {
1019 /* istanbul ignore next: unreachable default */