response.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  1. 'use strict';
  2. /**
  3. * Module dependencies.
  4. */
  5. const contentDisposition = require('content-disposition');
  6. const ensureErrorHandler = require('error-inject');
  7. const getType = require('mime-types').contentType;
  8. const onFinish = require('on-finished');
  9. const isJSON = require('koa-is-json');
  10. const escape = require('escape-html');
  11. const typeis = require('type-is').is;
  12. const statuses = require('statuses');
  13. const destroy = require('destroy');
  14. const assert = require('assert');
  15. const extname = require('path').extname;
  16. const vary = require('vary');
  17. const only = require('only');
  18. /**
  19. * Prototype.
  20. */
  21. module.exports = {
  22. /**
  23. * Return the request socket.
  24. *
  25. * @return {Connection}
  26. * @api public
  27. */
  28. get socket() {
  29. return this.ctx.req.socket;
  30. },
  31. /**
  32. * Return response header.
  33. *
  34. * @return {Object}
  35. * @api public
  36. */
  37. get header() {
  38. const { res } = this;
  39. return typeof res.getHeaders === 'function'
  40. ? res.getHeaders()
  41. : res._headers || {}; // Node < 7.7
  42. },
  43. /**
  44. * Return response header, alias as response.header
  45. *
  46. * @return {Object}
  47. * @api public
  48. */
  49. get headers() {
  50. return this.header;
  51. },
  52. /**
  53. * Get response status code.
  54. *
  55. * @return {Number}
  56. * @api public
  57. */
  58. get status() {
  59. return this.res.statusCode;
  60. },
  61. /**
  62. * Set response status code.
  63. *
  64. * @param {Number} code
  65. * @api public
  66. */
  67. set status(code) {
  68. assert('number' == typeof code, 'status code must be a number');
  69. assert(statuses[code], `invalid status code: ${code}`);
  70. assert(!this.res.headersSent, 'headers have already been sent');
  71. this._explicitStatus = true;
  72. this.res.statusCode = code;
  73. this.res.statusMessage = statuses[code];
  74. if (this.body && statuses.empty[code]) this.body = null;
  75. },
  76. /**
  77. * Get response status message
  78. *
  79. * @return {String}
  80. * @api public
  81. */
  82. get message() {
  83. return this.res.statusMessage || statuses[this.status];
  84. },
  85. /**
  86. * Set response status message
  87. *
  88. * @param {String} msg
  89. * @api public
  90. */
  91. set message(msg) {
  92. this.res.statusMessage = msg;
  93. },
  94. /**
  95. * Get response body.
  96. *
  97. * @return {Mixed}
  98. * @api public
  99. */
  100. get body() {
  101. return this._body;
  102. },
  103. /**
  104. * Set response body.
  105. *
  106. * @param {String|Buffer|Object|Stream} val
  107. * @api public
  108. */
  109. set body(val) {
  110. const original = this._body;
  111. this._body = val;
  112. if (this.res.headersSent) return;
  113. // no content
  114. if (null == val) {
  115. if (!statuses.empty[this.status]) this.status = 204;
  116. this.remove('Content-Type');
  117. this.remove('Content-Length');
  118. this.remove('Transfer-Encoding');
  119. return;
  120. }
  121. // set the status
  122. if (!this._explicitStatus) this.status = 200;
  123. // set the content-type only if not yet set
  124. const setType = !this.header['content-type'];
  125. // string
  126. if ('string' == typeof val) {
  127. if (setType) this.type = /^\s*</.test(val) ? 'html' : 'text';
  128. this.length = Buffer.byteLength(val);
  129. return;
  130. }
  131. // buffer
  132. if (Buffer.isBuffer(val)) {
  133. if (setType) this.type = 'bin';
  134. this.length = val.length;
  135. return;
  136. }
  137. // stream
  138. if ('function' == typeof val.pipe) {
  139. onFinish(this.res, destroy.bind(null, val));
  140. ensureErrorHandler(val, err => this.ctx.onerror(err));
  141. // overwriting
  142. if (null != original && original != val) this.remove('Content-Length');
  143. if (setType) this.type = 'bin';
  144. return;
  145. }
  146. // json
  147. this.remove('Content-Length');
  148. this.type = 'json';
  149. },
  150. /**
  151. * Set Content-Length field to `n`.
  152. *
  153. * @param {Number} n
  154. * @api public
  155. */
  156. set length(n) {
  157. this.set('Content-Length', n);
  158. },
  159. /**
  160. * Return parsed response Content-Length when present.
  161. *
  162. * @return {Number}
  163. * @api public
  164. */
  165. get length() {
  166. const len = this.header['content-length'];
  167. const body = this.body;
  168. if (null == len) {
  169. if (!body) return;
  170. if ('string' == typeof body) return Buffer.byteLength(body);
  171. if (Buffer.isBuffer(body)) return body.length;
  172. if (isJSON(body)) return Buffer.byteLength(JSON.stringify(body));
  173. return;
  174. }
  175. return ~~len;
  176. },
  177. /**
  178. * Check if a header has been written to the socket.
  179. *
  180. * @return {Boolean}
  181. * @api public
  182. */
  183. get headerSent() {
  184. return this.res.headersSent;
  185. },
  186. /**
  187. * Vary on `field`.
  188. *
  189. * @param {String} field
  190. * @api public
  191. */
  192. vary(field) {
  193. vary(this.res, field);
  194. },
  195. /**
  196. * Perform a 302 redirect to `url`.
  197. *
  198. * The string "back" is special-cased
  199. * to provide Referrer support, when Referrer
  200. * is not present `alt` or "/" is used.
  201. *
  202. * Examples:
  203. *
  204. * this.redirect('back');
  205. * this.redirect('back', '/index.html');
  206. * this.redirect('/login');
  207. * this.redirect('http://google.com');
  208. *
  209. * @param {String} url
  210. * @param {String} [alt]
  211. * @api public
  212. */
  213. redirect(url, alt) {
  214. // location
  215. if ('back' == url) url = this.ctx.get('Referrer') || alt || '/';
  216. this.set('Location', url);
  217. // status
  218. if (!statuses.redirect[this.status]) this.status = 302;
  219. // html
  220. if (this.ctx.accepts('html')) {
  221. url = escape(url);
  222. this.type = 'text/html; charset=utf-8';
  223. this.body = `Redirecting to <a href="${url}">${url}</a>.`;
  224. return;
  225. }
  226. // text
  227. this.type = 'text/plain; charset=utf-8';
  228. this.body = `Redirecting to ${url}.`;
  229. },
  230. /**
  231. * Set Content-Disposition header to "attachment" with optional `filename`.
  232. *
  233. * @param {String} filename
  234. * @api public
  235. */
  236. attachment(filename) {
  237. if (filename) this.type = extname(filename);
  238. this.set('Content-Disposition', contentDisposition(filename));
  239. },
  240. /**
  241. * Set Content-Type response header with `type` through `mime.lookup()`
  242. * when it does not contain a charset.
  243. *
  244. * Examples:
  245. *
  246. * this.type = '.html';
  247. * this.type = 'html';
  248. * this.type = 'json';
  249. * this.type = 'application/json';
  250. * this.type = 'png';
  251. *
  252. * @param {String} type
  253. * @api public
  254. */
  255. set type(type) {
  256. type = getType(type);
  257. if (type) {
  258. this.set('Content-Type', type);
  259. } else {
  260. this.remove('Content-Type');
  261. }
  262. },
  263. /**
  264. * Set the Last-Modified date using a string or a Date.
  265. *
  266. * this.response.lastModified = new Date();
  267. * this.response.lastModified = '2013-09-13';
  268. *
  269. * @param {String|Date} type
  270. * @api public
  271. */
  272. set lastModified(val) {
  273. if ('string' == typeof val) val = new Date(val);
  274. this.set('Last-Modified', val.toUTCString());
  275. },
  276. /**
  277. * Get the Last-Modified date in Date form, if it exists.
  278. *
  279. * @return {Date}
  280. * @api public
  281. */
  282. get lastModified() {
  283. const date = this.get('last-modified');
  284. if (date) return new Date(date);
  285. },
  286. /**
  287. * Set the ETag of a response.
  288. * This will normalize the quotes if necessary.
  289. *
  290. * this.response.etag = 'md5hashsum';
  291. * this.response.etag = '"md5hashsum"';
  292. * this.response.etag = 'W/"123456789"';
  293. *
  294. * @param {String} etag
  295. * @api public
  296. */
  297. set etag(val) {
  298. if (!/^(W\/)?"/.test(val)) val = `"${val}"`;
  299. this.set('ETag', val);
  300. },
  301. /**
  302. * Get the ETag of a response.
  303. *
  304. * @return {String}
  305. * @api public
  306. */
  307. get etag() {
  308. return this.get('ETag');
  309. },
  310. /**
  311. * Return the response mime type void of
  312. * parameters such as "charset".
  313. *
  314. * @return {String}
  315. * @api public
  316. */
  317. get type() {
  318. const type = this.get('Content-Type');
  319. if (!type) return '';
  320. return type.split(';')[0];
  321. },
  322. /**
  323. * Check whether the response is one of the listed types.
  324. * Pretty much the same as `this.request.is()`.
  325. *
  326. * @param {String|Array} types...
  327. * @return {String|false}
  328. * @api public
  329. */
  330. is(types) {
  331. const type = this.type;
  332. if (!types) return type || false;
  333. if (!Array.isArray(types)) types = [].slice.call(arguments);
  334. return typeis(type, types);
  335. },
  336. /**
  337. * Return response header.
  338. *
  339. * Examples:
  340. *
  341. * this.get('Content-Type');
  342. * // => "text/plain"
  343. *
  344. * this.get('content-type');
  345. * // => "text/plain"
  346. *
  347. * @param {String} field
  348. * @return {String}
  349. * @api public
  350. */
  351. get(field) {
  352. return this.header[field.toLowerCase()] || '';
  353. },
  354. /**
  355. * Set header `field` to `val`, or pass
  356. * an object of header fields.
  357. *
  358. * Examples:
  359. *
  360. * this.set('Foo', ['bar', 'baz']);
  361. * this.set('Accept', 'application/json');
  362. * this.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' });
  363. *
  364. * @param {String|Object|Array} field
  365. * @param {String} val
  366. * @api public
  367. */
  368. set(field, val) {
  369. if (2 == arguments.length) {
  370. if (Array.isArray(val)) val = val.map(String);
  371. else val = String(val);
  372. this.res.setHeader(field, val);
  373. } else {
  374. for (const key in field) {
  375. this.set(key, field[key]);
  376. }
  377. }
  378. },
  379. /**
  380. * Append additional header `field` with value `val`.
  381. *
  382. * Examples:
  383. *
  384. * ```
  385. * this.append('Link', ['<http://localhost/>', '<http://localhost:3000/>']);
  386. * this.append('Set-Cookie', 'foo=bar; Path=/; HttpOnly');
  387. * this.append('Warning', '199 Miscellaneous warning');
  388. * ```
  389. *
  390. * @param {String} field
  391. * @param {String|Array} val
  392. * @api public
  393. */
  394. append(field, val) {
  395. const prev = this.get(field);
  396. if (prev) {
  397. val = Array.isArray(prev)
  398. ? prev.concat(val)
  399. : [prev].concat(val);
  400. }
  401. return this.set(field, val);
  402. },
  403. /**
  404. * Remove header `field`.
  405. *
  406. * @param {String} name
  407. * @api public
  408. */
  409. remove(field) {
  410. this.res.removeHeader(field);
  411. },
  412. /**
  413. * Checks if the request is writable.
  414. * Tests for the existence of the socket
  415. * as node sometimes does not set it.
  416. *
  417. * @return {Boolean}
  418. * @api private
  419. */
  420. get writable() {
  421. // can't write any more after response finished
  422. if (this.res.finished) return false;
  423. const socket = this.res.socket;
  424. // There are already pending outgoing res, but still writable
  425. // https://github.com/nodejs/node/blob/v4.4.7/lib/_http_server.js#L486
  426. if (!socket) return true;
  427. return socket.writable;
  428. },
  429. /**
  430. * Inspect implementation.
  431. *
  432. * @return {Object}
  433. * @api public
  434. */
  435. inspect() {
  436. if (!this.res) return;
  437. const o = this.toJSON();
  438. o.body = this.body;
  439. return o;
  440. },
  441. /**
  442. * Return JSON representation.
  443. *
  444. * @return {Object}
  445. * @api public
  446. */
  447. toJSON() {
  448. return only(this, [
  449. 'status',
  450. 'message',
  451. 'header'
  452. ]);
  453. },
  454. /**
  455. * Flush any set headers, and begin the body
  456. */
  457. flushHeaders() {
  458. this.res.flushHeaders();
  459. }
  460. };