response.js 12 KB

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