response.js 12 KB

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