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 */