comparison node_modules/express/lib/response.js @ 73:0c3a2942ddee

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