UNPKG

19.4 kBJavaScriptView Raw
1/**
2 * Module dependencies.
3 */
4
5var http = require('http');
6var path = require('path');
7var mixin = require('utils-merge');
8var escapeHtml = require('escape-html');
9var sign = require('cookie-signature').sign;
10var normalizeType = require('./utils').normalizeType;
11var normalizeTypes = require('./utils').normalizeTypes;
12var setCharset = require('./utils').setCharset;
13var contentDisposition = require('./utils').contentDisposition;
14var deprecate = require('./utils').deprecate;
15var etag = require('./utils').etag;
16var statusCodes = http.STATUS_CODES;
17var cookie = require('cookie');
18var send = require('send');
19var basename = path.basename;
20var extname = path.extname;
21var mime = send.mime;
22
23/**
24 * Response prototype.
25 */
26
27var res = module.exports = {
28 __proto__: http.ServerResponse.prototype
29};
30
31/**
32 * Set status `code`.
33 *
34 * @param {Number} code
35 * @return {ServerResponse}
36 * @api public
37 */
38
39res.status = function(code){
40 this.statusCode = code;
41 return this;
42};
43
44/**
45 * Set Link header field with the given `links`.
46 *
47 * Examples:
48 *
49 * res.links({
50 * next: 'http://api.example.com/users?page=2',
51 * last: 'http://api.example.com/users?page=5'
52 * });
53 *
54 * @param {Object} links
55 * @return {ServerResponse}
56 * @api public
57 */
58
59res.links = function(links){
60 var link = this.get('Link') || '';
61 if (link) link += ', ';
62 return this.set('Link', link + Object.keys(links).map(function(rel){
63 return '<' + links[rel] + '>; rel="' + rel + '"';
64 }).join(', '));
65};
66
67/**
68 * Send a response.
69 *
70 * Examples:
71 *
72 * res.send(new Buffer('wahoo'));
73 * res.send({ some: 'json' });
74 * res.send('<p>some html</p>');
75 * res.send(404, 'Sorry, cant find that');
76 * res.send(404);
77 *
78 * @param {Mixed} body or status
79 * @param {Mixed} body
80 * @return {ServerResponse}
81 * @api public
82 */
83
84res.send = function(body){
85 var req = this.req;
86 var head = 'HEAD' == req.method;
87 var type;
88 var encoding;
89 var len;
90
91 // settings
92 var app = this.app;
93
94 // allow status / body
95 if (2 == arguments.length) {
96 // res.send(body, status) backwards compat
97 if ('number' != typeof body && 'number' == typeof arguments[1]) {
98 this.statusCode = arguments[1];
99 } else {
100 this.statusCode = body;
101 body = arguments[1];
102 }
103 }
104
105 switch (typeof body) {
106 // response status
107 case 'number':
108 this.get('Content-Type') || this.type('txt');
109 this.statusCode = body;
110 body = http.STATUS_CODES[body];
111 break;
112 // string defaulting to html
113 case 'string':
114 if (!this.get('Content-Type')) this.type('html');
115 break;
116 case 'boolean':
117 case 'object':
118 if (null == body) {
119 body = '';
120 } else if (Buffer.isBuffer(body)) {
121 this.get('Content-Type') || this.type('bin');
122 } else {
123 return this.json(body);
124 }
125 break;
126 }
127
128 // populate Content-Length
129 if (undefined !== body && !this.get('Content-Length')) {
130 this.set('Content-Length', len = Buffer.isBuffer(body)
131 ? body.length
132 : Buffer.byteLength(body));
133 }
134
135 // ETag support
136 // TODO: W/ support
137 if (app.settings.etag && len && ('GET' == req.method || 'HEAD' == req.method)) {
138 if (!this.get('ETag')) {
139 this.set('ETag', etag(body));
140 }
141 }
142
143 // write strings in utf-8
144 if ('string' === typeof body) {
145 encoding = 'utf8';
146 type = this.get('Content-Type');
147
148 // reflect this in content-type
149 if ('string' === typeof type) {
150 this.set('Content-Type', setCharset(type, 'utf-8'));
151 }
152 }
153
154 // freshness
155 if (req.fresh) this.statusCode = 304;
156
157 // strip irrelevant headers
158 if (204 == this.statusCode || 304 == this.statusCode) {
159 this.removeHeader('Content-Type');
160 this.removeHeader('Content-Length');
161 this.removeHeader('Transfer-Encoding');
162 body = '';
163 }
164
165 // respond
166 this.end((head ? null : body), encoding);
167
168 return this;
169};
170
171/**
172 * Send JSON response.
173 *
174 * Examples:
175 *
176 * res.json(null);
177 * res.json({ user: 'tj' });
178 * res.json(500, 'oh noes!');
179 * res.json(404, 'I dont have that');
180 *
181 * @param {Mixed} obj or status
182 * @param {Mixed} obj
183 * @return {ServerResponse}
184 * @api public
185 */
186
187res.json = function(obj){
188 // allow status / body
189 if (2 == arguments.length) {
190 // res.json(body, status) backwards compat
191 if ('number' == typeof arguments[1]) {
192 this.statusCode = arguments[1];
193 return 'number' === typeof obj
194 ? jsonNumDeprecated.call(this, obj)
195 : jsonDeprecated.call(this, obj);
196 } else {
197 this.statusCode = obj;
198 obj = arguments[1];
199 }
200 }
201
202 // settings
203 var app = this.app;
204 var replacer = app.get('json replacer');
205 var spaces = app.get('json spaces');
206 var body = JSON.stringify(obj, replacer, spaces);
207
208 // content-type
209 this.get('Content-Type') || this.set('Content-Type', 'application/json');
210
211 return this.send(body);
212};
213
214var jsonDeprecated = deprecate(res.json,
215 'res.json(obj, status): Use res.json(status, obj) instead');
216
217var jsonNumDeprecated = deprecate(res.json,
218 'res.json(num, status): Use res.status(status).json(num) instead');
219
220/**
221 * Send JSON response with JSONP callback support.
222 *
223 * Examples:
224 *
225 * res.jsonp(null);
226 * res.jsonp({ user: 'tj' });
227 * res.jsonp(500, 'oh noes!');
228 * res.jsonp(404, 'I dont have that');
229 *
230 * @param {Mixed} obj or status
231 * @param {Mixed} obj
232 * @return {ServerResponse}
233 * @api public
234 */
235
236res.jsonp = function(obj){
237 // allow status / body
238 if (2 == arguments.length) {
239 // res.json(body, status) backwards compat
240 if ('number' == typeof arguments[1]) {
241 this.statusCode = arguments[1];
242 return 'number' === typeof obj
243 ? jsonpNumDeprecated.call(this, obj)
244 : jsonpDeprecated.call(this, obj);
245 } else {
246 this.statusCode = obj;
247 obj = arguments[1];
248 }
249 }
250
251 // settings
252 var app = this.app;
253 var replacer = app.get('json replacer');
254 var spaces = app.get('json spaces');
255 var body = JSON.stringify(obj, replacer, spaces)
256 .replace(/\u2028/g, '\\u2028')
257 .replace(/\u2029/g, '\\u2029');
258 var callback = this.req.query[app.get('jsonp callback name')];
259
260 // content-type
261 this.get('Content-Type') || this.set('Content-Type', 'application/json');
262
263 // fixup callback
264 if (Array.isArray(callback)) {
265 callback = callback[0];
266 }
267
268 // jsonp
269 if (callback && 'string' === typeof callback) {
270 this.set('Content-Type', 'text/javascript');
271 var cb = callback.replace(/[^\[\]\w$.]/g, '');
272 body = 'typeof ' + cb + ' === \'function\' && ' + cb + '(' + body + ');';
273 }
274
275 return this.send(body);
276};
277
278var jsonpDeprecated = deprecate(res.json,
279 'res.jsonp(obj, status): Use res.jsonp(status, obj) instead');
280
281var jsonpNumDeprecated = deprecate(res.json,
282 'res.jsonp(num, status): Use res.status(status).jsonp(num) instead');
283
284/**
285 * Transfer the file at the given `path`.
286 *
287 * Automatically sets the _Content-Type_ response header field.
288 * The callback `fn(err)` is invoked when the transfer is complete
289 * or when an error occurs. Be sure to check `res.sentHeader`
290 * if you wish to attempt responding, as the header and some data
291 * may have already been transferred.
292 *
293 * Options:
294 *
295 * - `maxAge` defaulting to 0
296 * - `root` root directory for relative filenames
297 * - `hidden` serve hidden files, defaulting to false
298 *
299 * Other options are passed along to `send`.
300 *
301 * Examples:
302 *
303 * The following example illustrates how `res.sendfile()` may
304 * be used as an alternative for the `static()` middleware for
305 * dynamic situations. The code backing `res.sendfile()` is actually
306 * the same code, so HTTP cache support etc is identical.
307 *
308 * app.get('/user/:uid/photos/:file', function(req, res){
309 * var uid = req.params.uid
310 * , file = req.params.file;
311 *
312 * req.user.mayViewFilesFrom(uid, function(yes){
313 * if (yes) {
314 * res.sendfile('/uploads/' + uid + '/' + file);
315 * } else {
316 * res.send(403, 'Sorry! you cant see that.');
317 * }
318 * });
319 * });
320 *
321 * @param {String} path
322 * @param {Object|Function} options or fn
323 * @param {Function} fn
324 * @api public
325 */
326
327res.sendfile = function(path, options, fn){
328 options = options || {};
329 var self = this;
330 var req = self.req;
331 var next = this.req.next;
332 var done;
333
334
335 // support function as second arg
336 if ('function' == typeof options) {
337 fn = options;
338 options = {};
339 }
340
341 // socket errors
342 req.socket.on('error', error);
343
344 // errors
345 function error(err) {
346 if (done) return;
347 done = true;
348
349 // clean up
350 cleanup();
351 if (!self.headersSent) self.removeHeader('Content-Disposition');
352
353 // callback available
354 if (fn) return fn(err);
355
356 // list in limbo if there's no callback
357 if (self.headersSent) return;
358
359 // delegate
360 next(err);
361 }
362
363 // streaming
364 function stream(stream) {
365 if (done) return;
366 cleanup();
367 if (fn) stream.on('end', fn);
368 }
369
370 // cleanup
371 function cleanup() {
372 req.socket.removeListener('error', error);
373 }
374
375 // Back-compat
376 options.maxage = options.maxage || options.maxAge || 0;
377
378 // transfer
379 var file = send(req, path, options);
380 file.on('error', error);
381 file.on('directory', next);
382 file.on('stream', stream);
383 file.pipe(this);
384 this.on('finish', cleanup);
385};
386
387/**
388 * Transfer the file at the given `path` as an attachment.
389 *
390 * Optionally providing an alternate attachment `filename`,
391 * and optional callback `fn(err)`. The callback is invoked
392 * when the data transfer is complete, or when an error has
393 * ocurred. Be sure to check `res.headersSent` if you plan to respond.
394 *
395 * This method uses `res.sendfile()`.
396 *
397 * @param {String} path
398 * @param {String|Function} filename or fn
399 * @param {Function} fn
400 * @api public
401 */
402
403res.download = function(path, filename, fn){
404 // support function as second arg
405 if ('function' == typeof filename) {
406 fn = filename;
407 filename = null;
408 }
409
410 filename = filename || path;
411 this.set('Content-Disposition', contentDisposition(filename));
412 return this.sendfile(path, fn);
413};
414
415/**
416 * Set _Content-Type_ response header with `type` through `mime.lookup()`
417 * when it does not contain "/", or set the Content-Type to `type` otherwise.
418 *
419 * Examples:
420 *
421 * res.type('.html');
422 * res.type('html');
423 * res.type('json');
424 * res.type('application/json');
425 * res.type('png');
426 *
427 * @param {String} type
428 * @return {ServerResponse} for chaining
429 * @api public
430 */
431
432res.contentType =
433res.type = function(type){
434 return this.set('Content-Type', ~type.indexOf('/')
435 ? type
436 : mime.lookup(type));
437};
438
439/**
440 * Respond to the Acceptable formats using an `obj`
441 * of mime-type callbacks.
442 *
443 * This method uses `req.accepted`, an array of
444 * acceptable types ordered by their quality values.
445 * When "Accept" is not present the _first_ callback
446 * is invoked, otherwise the first match is used. When
447 * no match is performed the server responds with
448 * 406 "Not Acceptable".
449 *
450 * Content-Type is set for you, however if you choose
451 * you may alter this within the callback using `res.type()`
452 * or `res.set('Content-Type', ...)`.
453 *
454 * res.format({
455 * 'text/plain': function(){
456 * res.send('hey');
457 * },
458 *
459 * 'text/html': function(){
460 * res.send('<p>hey</p>');
461 * },
462 *
463 * 'appliation/json': function(){
464 * res.send({ message: 'hey' });
465 * }
466 * });
467 *
468 * In addition to canonicalized MIME types you may
469 * also use extnames mapped to these types:
470 *
471 * res.format({
472 * text: function(){
473 * res.send('hey');
474 * },
475 *
476 * html: function(){
477 * res.send('<p>hey</p>');
478 * },
479 *
480 * json: function(){
481 * res.send({ message: 'hey' });
482 * }
483 * });
484 *
485 * By default Express passes an `Error`
486 * with a `.status` of 406 to `next(err)`
487 * if a match is not made. If you provide
488 * a `.default` callback it will be invoked
489 * instead.
490 *
491 * @param {Object} obj
492 * @return {ServerResponse} for chaining
493 * @api public
494 */
495
496res.format = function(obj){
497 var req = this.req;
498 var next = req.next;
499
500 var fn = obj.default;
501 if (fn) delete obj.default;
502 var keys = Object.keys(obj);
503
504 var key = req.accepts(keys);
505
506 this.vary("Accept");
507
508 if (key) {
509 this.set('Content-Type', normalizeType(key).value);
510 obj[key](req, this, next);
511 } else if (fn) {
512 fn();
513 } else {
514 var err = new Error('Not Acceptable');
515 err.status = 406;
516 err.types = normalizeTypes(keys).map(function(o){ return o.value });
517 next(err);
518 }
519
520 return this;
521};
522
523/**
524 * Set _Content-Disposition_ header to _attachment_ with optional `filename`.
525 *
526 * @param {String} filename
527 * @return {ServerResponse}
528 * @api public
529 */
530
531res.attachment = function(filename){
532 if (filename) this.type(extname(filename));
533 this.set('Content-Disposition', contentDisposition(filename));
534 return this;
535};
536
537/**
538 * Set header `field` to `val`, or pass
539 * an object of header fields.
540 *
541 * Examples:
542 *
543 * res.set('Foo', ['bar', 'baz']);
544 * res.set('Accept', 'application/json');
545 * res.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' });
546 *
547 * Aliased as `res.header()`.
548 *
549 * @param {String|Object|Array} field
550 * @param {String} val
551 * @return {ServerResponse} for chaining
552 * @api public
553 */
554
555res.set =
556res.header = function(field, val){
557 if (2 == arguments.length) {
558 if (Array.isArray(val)) val = val.map(String);
559 else val = String(val);
560 if ('content-type' == field.toLowerCase() && !/;\s*charset\s*=/.test(val)) {
561 var charset = mime.charsets.lookup(val.split(';')[0]);
562 if (charset) val += '; charset=' + charset.toLowerCase();
563 }
564 this.setHeader(field, val);
565 } else {
566 for (var key in field) {
567 this.set(key, field[key]);
568 }
569 }
570 return this;
571};
572
573/**
574 * Get value for header `field`.
575 *
576 * @param {String} field
577 * @return {String}
578 * @api public
579 */
580
581res.get = function(field){
582 return this.getHeader(field);
583};
584
585/**
586 * Clear cookie `name`.
587 *
588 * @param {String} name
589 * @param {Object} options
590 * @param {ServerResponse} for chaining
591 * @api public
592 */
593
594res.clearCookie = function(name, options){
595 var opts = { expires: new Date(1), path: '/' };
596 return this.cookie(name, '', options
597 ? mixin(opts, options)
598 : opts);
599};
600
601/**
602 * Set cookie `name` to `val`, with the given `options`.
603 *
604 * Options:
605 *
606 * - `maxAge` max-age in milliseconds, converted to `expires`
607 * - `signed` sign the cookie
608 * - `path` defaults to "/"
609 *
610 * Examples:
611 *
612 * // "Remember Me" for 15 minutes
613 * res.cookie('rememberme', '1', { expires: new Date(Date.now() + 900000), httpOnly: true });
614 *
615 * // save as above
616 * res.cookie('rememberme', '1', { maxAge: 900000, httpOnly: true })
617 *
618 * @param {String} name
619 * @param {String|Object} val
620 * @param {Options} options
621 * @api public
622 */
623
624res.cookie = function(name, val, options){
625 options = mixin({}, options);
626 var secret = this.req.secret;
627 var signed = options.signed;
628 if (signed && !secret) throw new Error('cookieParser("secret") required for signed cookies');
629 if ('number' == typeof val) val = val.toString();
630 if ('object' == typeof val) val = 'j:' + JSON.stringify(val);
631 if (signed) val = 's:' + sign(val, secret);
632 if ('maxAge' in options) {
633 options.expires = new Date(Date.now() + options.maxAge);
634 options.maxAge /= 1000;
635 }
636 if (null == options.path) options.path = '/';
637 var headerVal = cookie.serialize(name, String(val), options);
638
639 // supports multiple 'res.cookie' calls by getting previous value
640 var prev = this.get('Set-Cookie');
641 if (prev) {
642 if (Array.isArray(prev)) {
643 headerVal = prev.concat(headerVal);
644 } else {
645 headerVal = [prev, headerVal];
646 }
647 }
648 this.set('Set-Cookie', headerVal);
649 return this;
650};
651
652
653/**
654 * Set the location header to `url`.
655 *
656 * The given `url` can also be "back", which redirects
657 * to the _Referrer_ or _Referer_ headers or "/".
658 *
659 * Examples:
660 *
661 * res.location('/foo/bar').;
662 * res.location('http://example.com');
663 * res.location('../login');
664 *
665 * @param {String} url
666 * @api public
667 */
668
669res.location = function(url){
670 var req = this.req;
671
672 // "back" is an alias for the referrer
673 if ('back' == url) url = req.get('Referrer') || '/';
674
675 // Respond
676 this.set('Location', url);
677 return this;
678};
679
680/**
681 * Redirect to the given `url` with optional response `status`
682 * defaulting to 302.
683 *
684 * The resulting `url` is determined by `res.location()`, so
685 * it will play nicely with mounted apps, relative paths,
686 * `"back"` etc.
687 *
688 * Examples:
689 *
690 * res.redirect('/foo/bar');
691 * res.redirect('http://example.com');
692 * res.redirect(301, 'http://example.com');
693 * res.redirect('http://example.com', 301);
694 * res.redirect('../login'); // /blog/post/1 -> /blog/login
695 *
696 * @param {String} url
697 * @param {Number} code
698 * @api public
699 */
700
701res.redirect = function(url){
702 var head = 'HEAD' == this.req.method;
703 var status = 302;
704 var body;
705
706 // allow status / url
707 if (2 == arguments.length) {
708 if ('number' == typeof url) {
709 status = url;
710 url = arguments[1];
711 } else {
712 status = arguments[1];
713 }
714 }
715
716 // Set location header
717 this.location(url);
718 url = this.get('Location');
719
720 // Support text/{plain,html} by default
721 this.format({
722 text: function(){
723 body = statusCodes[status] + '. Redirecting to ' + encodeURI(url);
724 },
725
726 html: function(){
727 var u = escapeHtml(url);
728 body = '<p>' + statusCodes[status] + '. Redirecting to <a href="' + u + '">' + u + '</a></p>';
729 },
730
731 default: function(){
732 body = '';
733 }
734 });
735
736 // Respond
737 this.statusCode = status;
738 this.set('Content-Length', Buffer.byteLength(body));
739 this.end(head ? null : body);
740};
741
742/**
743 * Add `field` to Vary. If already present in the Vary set, then
744 * this call is simply ignored.
745 *
746 * @param {Array|String} field
747 * @param {ServerResponse} for chaining
748 * @api public
749 */
750
751res.vary = function(field){
752 var self = this;
753
754 // nothing
755 if (!field) return this;
756
757 // array
758 if (Array.isArray(field)) {
759 field.forEach(function(field){
760 self.vary(field);
761 });
762 return;
763 }
764
765 var vary = this.get('Vary');
766
767 // append
768 if (vary) {
769 vary = vary.split(/ *, */);
770 if (!~vary.indexOf(field)) vary.push(field);
771 this.set('Vary', vary.join(', '));
772 return this;
773 }
774
775 // set
776 this.set('Vary', field);
777 return this;
778};
779
780/**
781 * Render `view` with the given `options` and optional callback `fn`.
782 * When a callback function is given a response will _not_ be made
783 * automatically, otherwise a response of _200_ and _text/html_ is given.
784 *
785 * Options:
786 *
787 * - `cache` boolean hinting to the engine it should cache
788 * - `filename` filename of the view being rendered
789 *
790 * @param {String} view
791 * @param {Object|Function} options or callback function
792 * @param {Function} fn
793 * @api public
794 */
795
796res.render = function(view, options, fn){
797 options = options || {};
798 var self = this;
799 var req = this.req;
800 var app = req.app;
801
802 // support callback function as second arg
803 if ('function' == typeof options) {
804 fn = options, options = {};
805 }
806
807 // merge res.locals
808 options._locals = self.locals;
809
810 // default callback to respond
811 fn = fn || function(err, str){
812 if (err) return req.next(err);
813 self.send(str);
814 };
815
816 // render
817 app.render(view, options, fn);
818};