PageRenderTime 64ms CodeModel.GetById 36ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/response.js

https://github.com/w00w00/express
JavaScript | 454 lines | 174 code | 56 blank | 224 comment | 56 complexity | beea3e0e516359201707eae12365ff14 MD5 | raw file
  1. /*!
  2. * Express - response
  3. * Copyright(c) 2010 TJ Holowaychuk <tj@vision-media.ca>
  4. * MIT Licensed
  5. */
  6. /**
  7. * Module dependencies.
  8. */
  9. var fs = require('fs')
  10. , http = require('http')
  11. , path = require('path')
  12. , connect = require('connect')
  13. , utils = connect.utils
  14. , statusCodes = http.STATUS_CODES
  15. , parseRange = require('./utils').parseRange
  16. , res = http.ServerResponse.prototype
  17. , send = connect.static.send
  18. , mime = require('mime')
  19. , basename = path.basename
  20. , join = path.join;
  21. /**
  22. * Send a response.
  23. *
  24. * Examples:
  25. *
  26. * res.send(new Buffer('wahoo'));
  27. * res.send({ some: 'json' });
  28. * res.send('<p>some html</p>');
  29. * res.send(404, 'Sorry, cant find that');
  30. * res.send(404);
  31. *
  32. * @param {Mixed} body or status
  33. * @param {Mixed} body
  34. * @return {ServerResponse}
  35. * @api public
  36. */
  37. res.send = function(body){
  38. var req = this.req
  39. , head = 'HEAD' == req.method;
  40. // allow status / body
  41. if (2 == arguments.length) {
  42. this.statusCode = body;
  43. body = arguments[1];
  44. }
  45. switch (typeof body) {
  46. // response status
  47. case 'number':
  48. this.header('Content-Type') || this.contentType('.txt');
  49. this.statusCode = body;
  50. body = http.STATUS_CODES[body];
  51. break;
  52. // string defaulting to html
  53. case 'string':
  54. if (!this.header('Content-Type')) {
  55. this.charset = this.charset || 'utf-8';
  56. this.contentType('.html');
  57. }
  58. break;
  59. case 'boolean':
  60. case 'object':
  61. if (null == body) {
  62. body = '';
  63. } else if (Buffer.isBuffer(body)) {
  64. this.header('Content-Type') || this.contentType('.bin');
  65. } else {
  66. return this.json(body);
  67. }
  68. break;
  69. }
  70. // populate Content-Length
  71. if (undefined !== body && !this.header('Content-Length')) {
  72. this.header('Content-Length', Buffer.isBuffer(body)
  73. ? body.length
  74. : Buffer.byteLength(body));
  75. }
  76. // strip irrelevant headers
  77. if (204 == this.statusCode || 304 == this.statusCode) {
  78. this.removeHeader('Content-Type');
  79. this.removeHeader('Content-Length');
  80. body = '';
  81. }
  82. // respond
  83. this.end(head ? null : body);
  84. return this;
  85. };
  86. /**
  87. * Send JSON response.
  88. *
  89. * Examples:
  90. *
  91. * res.json(null);
  92. * res.json({ user: 'tj' });
  93. * res.json(500, 'oh noes!');
  94. * res.json(404, 'I dont have that');
  95. *
  96. * @param {Mixed} obj or status
  97. * @param {Mixed} obj
  98. * @return {ServerResponse}
  99. * @api public
  100. */
  101. res.json = function(obj){
  102. // allow status / body
  103. if (2 == arguments.length) {
  104. this.statusCode = obj;
  105. obj = arguments[1];
  106. }
  107. var body = JSON.stringify(obj)
  108. , callback = this.req.query.callback
  109. , jsonp = this.app.enabled('jsonp callback');
  110. this.charset = this.charset || 'utf-8';
  111. this.header('Content-Type', 'application/json');
  112. if (callback && jsonp) {
  113. this.header('Content-Type', 'text/javascript');
  114. body = callback.replace(/[^\w$.]/g, '') + '(' + body + ');';
  115. }
  116. return this.send(body);
  117. };
  118. /**
  119. * Set status `code`.
  120. *
  121. * @param {Number} code
  122. * @return {ServerResponse}
  123. * @api public
  124. */
  125. res.status = function(code){
  126. this.statusCode = code;
  127. return this;
  128. };
  129. /**
  130. * Transfer the file at the given `path`.
  131. *
  132. * Automatically sets the _Content-Type_ response header field.
  133. * The callback `fn(err)` is invoked when the transfer is complete
  134. * or when an error occurs. Be sure to check `res.sentHeader`
  135. * if you wish to attempt responding, as the header and some data
  136. * may have already been transferred.
  137. *
  138. * Options:
  139. *
  140. * - `maxAge` defaulting to 0
  141. * - `root` root directory for relative filenames
  142. *
  143. * @param {String} path
  144. * @param {Object|Function} options or fn
  145. * @param {Function} fn
  146. * @api public
  147. */
  148. res.sendfile = function(path, options, fn){
  149. var self = this
  150. , req = self.req
  151. , next = this.req.next
  152. , options = options || {};
  153. // support function as second arg
  154. if ('function' == typeof options) {
  155. fn = options;
  156. options = {};
  157. }
  158. // callback
  159. options.callback = function(err){
  160. // TODO: remove when node adds this
  161. self.sentHeader = !! self._header;
  162. // error
  163. if (err) {
  164. // ditch content-disposition to prevent funky responses
  165. if (!self.sentHeader) self.removeHeader('Content-Disposition');
  166. // not found
  167. if ('ENOENT' == err.code) return req.next();
  168. // lost in limbo if there's no callback
  169. if (self.sentHeader) return fn && fn(err);
  170. // callback available
  171. if (fn) return fn(err);
  172. return req.next(err);
  173. }
  174. fn && fn();
  175. };
  176. // transfer
  177. options.path = encodeURIComponent(path);
  178. send(this.req, this, next, options);
  179. };
  180. /**
  181. * Transfer the file at the given `path` as an attachment.
  182. *
  183. * Optionally providing an alternate attachment `filename`,
  184. * and optional callback `fn(err)`. The callback is invoked
  185. * when the data transfer is complete, or when an error has
  186. * ocurred. Be sure to check `res.sendHeader` if you plan to respond.
  187. *
  188. * @param {String} path
  189. * @param {String|Function} filename or fn
  190. * @param {Function} fn
  191. * @api public
  192. */
  193. res.download = function(path, filename, fn){
  194. // support function as second arg
  195. if ('function' == typeof filename) {
  196. fn = filename;
  197. filename = null;
  198. }
  199. return this.attachment(filename || path).sendfile(path, fn);
  200. };
  201. /**
  202. * Set _Content-Type_ response header passed through `mime.lookup()`.
  203. *
  204. * Examples:
  205. *
  206. * var filename = 'path/to/image.png';
  207. * res.contentType(filename);
  208. * // res.headers['Content-Type'] is now "image/png"
  209. *
  210. * res.contentType('.html');
  211. * res.contentType('html');
  212. * res.contentType('json');
  213. * res.contentType('png');
  214. *
  215. * @param {String} type
  216. * @return {ServerResponse} for chaining
  217. * @api public
  218. */
  219. res.contentType = function(type){
  220. return this.header('Content-Type', mime.lookup(type));
  221. };
  222. /**
  223. * Set _Content-Disposition_ header to _attachment_ with optional `filename`.
  224. *
  225. * @param {String} filename
  226. * @return {ServerResponse}
  227. * @api public
  228. */
  229. res.attachment = function(filename){
  230. if (filename) this.contentType(filename);
  231. this.header('Content-Disposition', filename
  232. ? 'attachment; filename="' + basename(filename) + '"'
  233. : 'attachment');
  234. return this;
  235. };
  236. /**
  237. * Set header `field` to `val`.
  238. *
  239. * @param {String} field
  240. * @param {String} val
  241. * @return {ServerResponse} for chaining
  242. * @api public
  243. */
  244. res.set = function(field, val){
  245. this.setHeader(field, val);
  246. return this;
  247. };
  248. /**
  249. * Get value for header `field`.
  250. *
  251. * @param {String} field
  252. * @return {String}
  253. * @api public
  254. */
  255. res.get = function(field){
  256. return this.getHeader(field);
  257. };
  258. /**
  259. * Set or get response header `field` with optional `val`.
  260. *
  261. * @param {String} name
  262. * @param {String} val
  263. * @return {ServerResponse} for chaining
  264. * @api public
  265. */
  266. res.header = function(field, val){
  267. if (1 == arguments.length) return this.getHeader(field);
  268. this.setHeader(field, val);
  269. return this;
  270. };
  271. /**
  272. * Clear cookie `name`.
  273. *
  274. * @param {String} name
  275. * @param {Object} options
  276. * @param {ServerResponse} for chaining
  277. * @api public
  278. */
  279. res.clearCookie = function(name, options){
  280. var opts = { expires: new Date(1) };
  281. return this.cookie(name, '', options
  282. ? utils.merge(options, opts)
  283. : opts);
  284. };
  285. /**
  286. * Set cookie `name` to `val`, with the given `options`.
  287. *
  288. * Options:
  289. *
  290. * - `maxAge` max-age in milliseconds, converted to `expires`
  291. * - `path` defaults to the "basepath" setting which is typically "/"
  292. *
  293. * Examples:
  294. *
  295. * // "Remember Me" for 15 minutes
  296. * res.cookie('rememberme', '1', { expires: new Date(Date.now() + 900000), httpOnly: true });
  297. *
  298. * // save as above
  299. * res.cookie('rememberme', '1', { maxAge: 900000, httpOnly: true })
  300. *
  301. * @param {String} name
  302. * @param {String} val
  303. * @param {Options} options
  304. * @api public
  305. */
  306. res.cookie = function(name, val, options){
  307. options = options || {};
  308. if ('maxAge' in options) options.expires = new Date(Date.now() + options.maxAge);
  309. if (undefined === options.path) options.path = this.app.set('basepath');
  310. var cookie = utils.serializeCookie(name, val, options);
  311. this.header('Set-Cookie', cookie);
  312. return this;
  313. };
  314. /**
  315. * Redirect to the given `url` with optional response `status`
  316. * defauling to 302.
  317. *
  318. * The given `url` can also be the name of a mapped url, for
  319. * example by default express supports "back" which redirects
  320. * to the _Referrer_ or _Referer_ headers or the application's
  321. * "basepath" setting. Express also supports "basepath" out of the box,
  322. * which can be set via `app.set('basepath', '/blog');`.
  323. *
  324. * Redirect Mapping:
  325. *
  326. * To extend the redirect mapping capabilities that Express provides,
  327. * we may use the `app.redirect()` method:
  328. *
  329. * app.redirect('google', 'http://google.com');
  330. *
  331. * Now in a route we may call:
  332. *
  333. * res.redirect('google');
  334. *
  335. * We may also map dynamic redirects:
  336. *
  337. * app.redirect('comments', function(req, res){
  338. * return '/post/' + req.params.id + '/comments';
  339. * });
  340. *
  341. * So now we may do the following, and the redirect will dynamically adjust to
  342. * the context of the request. If we called this route with _GET /post/12_ our
  343. * redirect _Location_ would be _/post/12/comments_.
  344. *
  345. * app.get('/post/:id', function(req, res){
  346. * res.redirect('comments');
  347. * });
  348. *
  349. * Unless an absolute `url` is given, the app's mount-point
  350. * will be respected. For example if we redirect to `/posts`,
  351. * and our app is mounted at `/blog` we will redirect to `/blog/posts`.
  352. *
  353. * @param {String} url
  354. * @param {Number} code
  355. * @api public
  356. */
  357. res.redirect = function(url, status){
  358. var app = this.app
  359. , req = this.req
  360. , base = app.set('basepath') || app.route
  361. , status = status || 302
  362. , head = 'HEAD' == req.method
  363. , body;
  364. // Setup redirect map
  365. var map = {
  366. back: req.header('Referrer', base)
  367. , home: base
  368. };
  369. // Support custom redirect map
  370. map.__proto__ = app.redirects;
  371. // Attempt mapped redirect
  372. var mapped = 'function' == typeof map[url]
  373. ? map[url](req, this)
  374. : map[url];
  375. // Perform redirect
  376. url = mapped || url;
  377. // Relative
  378. if (!~url.indexOf('://')) {
  379. // Respect mount-point
  380. if ('/' != base && 0 != url.indexOf(base)) url = base + url;
  381. // Absolute
  382. var host = req.headers.host
  383. , tls = req.connection.encrypted;
  384. url = 'http' + (tls ? 's' : '') + '://' + host + url;
  385. }
  386. // Support text/{plain,html} by default
  387. if (req.accepts('html')) {
  388. body = '<p>' + statusCodes[status] + '. Redirecting to <a href="' + url + '">' + url + '</a></p>';
  389. this.header('Content-Type', 'text/html');
  390. } else {
  391. body = statusCodes[status] + '. Redirecting to ' + url;
  392. this.header('Content-Type', 'text/plain');
  393. }
  394. // Respond
  395. this.statusCode = status;
  396. this.header('Location', url);
  397. this.end(head ? null : body);
  398. };