You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

971 lines
23 KiB

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