index.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647
  1. 'use strict'
  2. /*
  3. * node-res
  4. *
  5. * (c) Harminder Virk <virk@adonisjs.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. const mime = require('mime-types')
  11. const etag = require('etag')
  12. const vary = require('vary')
  13. const onFinished = require('on-finished')
  14. const destroy = require('destroy')
  15. const methods = require('./methods')
  16. const returnContentAndType = function (body) {
  17. /**
  18. * Return the body and it's type when
  19. * body is a string.
  20. */
  21. if (typeof (body) === 'string') {
  22. return {
  23. body,
  24. type: /^\s*</.test(body) ? 'text/html' : 'text/plain'
  25. }
  26. }
  27. /**
  28. * If body is a buffer, return the exact copy
  29. * and type as bin.
  30. */
  31. if (Buffer.isBuffer(body)) {
  32. return { body, type: 'application/octet-stream' }
  33. }
  34. /**
  35. * If body is a number or boolean. Convert it to
  36. * a string and return the type as text.
  37. */
  38. if (typeof (body) === 'number' || typeof (body) === 'boolean') {
  39. return { body: String(body), type: 'text/plain' }
  40. }
  41. /**
  42. * Otherwise check whether body is an object or not. If yes
  43. * stringify it and otherwise return the exact copy.
  44. */
  45. return typeof (body) === 'object'
  46. ? { body: JSON.stringify(body), type: 'application/json' }
  47. : { body }
  48. }
  49. /**
  50. * A simple IO module to make consistent HTTP response, without
  51. * worrying about underlying details.
  52. *
  53. * @module Response
  54. */
  55. const Response = exports = module.exports = {}
  56. /**
  57. * Copying all the descriptive methods to the response object.
  58. */
  59. Response.descriptiveMethods = Object.keys(methods).map((method) => {
  60. Response[method] = function (req, res, body) {
  61. Response.status(res, methods[method])
  62. Response.send(req, res, body)
  63. }
  64. return method
  65. })
  66. /**
  67. * Returns the value of an existing header on
  68. * the response object
  69. *
  70. * @method getHeader
  71. *
  72. * @param {ServerResponse} res
  73. * @param {String} key
  74. *
  75. * @return {Array|String} Return type depends upon the header existing value
  76. *
  77. * @example
  78. * ```js
  79. * nodeRes.getHeader(res, 'Content-type')
  80. * ```
  81. */
  82. Response.getHeader = function (res, key) {
  83. return res.getHeader(key)
  84. }
  85. /**
  86. * Sets header on the response object. This method will wipe off
  87. * existing values. To append to existing values, use `append`.
  88. *
  89. * @method header
  90. *
  91. * @param {http.ServerResponse} res
  92. * @param {String} key
  93. * @param {String|Array} value
  94. *
  95. * @return {void}
  96. *
  97. * @example
  98. * ```js
  99. * nodeRes.header(res, 'Content-type', 'application/json')
  100. *
  101. * // or set an array of headers
  102. * nodeRes.header(res, 'Link', ['<http://localhost/>', '<http://localhost:3000/>'])
  103. * ```
  104. */
  105. Response.header = function (res, key, value) {
  106. const values = Array.isArray(value) ? value.map(String) : value
  107. res.setHeader(key, values)
  108. }
  109. /**
  110. * Appends value to the header existing values.
  111. *
  112. * @method append
  113. *
  114. * @param {http.ServerResponse} res
  115. * @param {String} key
  116. * @param {String|Array} value
  117. *
  118. * @return {void}
  119. *
  120. * @example
  121. * ```js
  122. * nodeRes.append(res, 'Content-type', 'application/json')
  123. *
  124. * // or append an array of headers
  125. * nodeRes.append(res, 'Link', ['<http://localhost/>', '<http://localhost:3000/>'])
  126. * ```
  127. */
  128. Response.append = function (res, key, value) {
  129. const previousValue = Response.getHeader(res, key)
  130. const headers = previousValue
  131. ? (Array.isArray(previousValue) ? previousValue.concat(value) : [previousValue].concat(value))
  132. : value
  133. Response.header(res, key, headers)
  134. }
  135. /**
  136. * Set status on the HTTP res object
  137. *
  138. * @method status
  139. *
  140. * @param {http.ServerResponse} res
  141. * @param {Number} code
  142. *
  143. * @return {void}
  144. *
  145. * @example
  146. * ```js
  147. * nodeRes.status(res, 200)
  148. * ```
  149. */
  150. Response.status = function (res, code) {
  151. res.statusCode = code
  152. }
  153. /**
  154. * Sets the header on response object, only if it
  155. * does not exists.
  156. *
  157. * @method safeHeader
  158. *
  159. * @param {http.ServerResponse} res
  160. * @param {String} key
  161. * @param {String|Array} value
  162. *
  163. * @return {void}
  164. *
  165. * @example
  166. * ```js
  167. * nodeRes.safeHeader(res, 'Content-type', 'application/json')
  168. * ```
  169. */
  170. Response.safeHeader = function (res, key, value) {
  171. if (!res.getHeader(key)) {
  172. Response.header(res, key, value)
  173. }
  174. }
  175. /**
  176. * Removes the header from response
  177. *
  178. * @method removeHeader
  179. *
  180. * @param {http.ServerResponse} res
  181. * @param {String} key
  182. *
  183. * @return {void}
  184. *
  185. * @example
  186. * ```js
  187. * nodeRes.removeHeader(res, 'Content-type')
  188. * ```
  189. */
  190. Response.removeHeader = function (res, key) {
  191. res.removeHeader(key)
  192. }
  193. /**
  194. * Write string or buffer to the response object.
  195. *
  196. * @method write
  197. *
  198. * @param {http.ServerResponse} res
  199. * @param {String|Buffer} body
  200. *
  201. * @return {void}
  202. *
  203. * @example
  204. * ```js
  205. * nodeRes.write(res, 'Hello world')
  206. * ```
  207. */
  208. Response.write = function (res, body) {
  209. res.write(body)
  210. }
  211. /**
  212. * Explictly end HTTP response
  213. *
  214. * @method end
  215. *
  216. * @param {http.ServerResponse} res
  217. * @param {String|Buffer} [payload]
  218. *
  219. * @return {void}
  220. *
  221. * @example
  222. * ```js
  223. * nodeRes.end(res, 'Hello world')
  224. * ```
  225. */
  226. Response.end = function (res, payload) {
  227. res.end(payload)
  228. }
  229. /**
  230. * Send body as the HTTP response and end it. Also
  231. * this method will set the appropriate `Content-type`
  232. * and `Content-length`.
  233. *
  234. * If body is set to null, this method will end the response
  235. * as 204.
  236. *
  237. * @method send
  238. *
  239. * @param {http.ServerRequest} req
  240. * @param {http.ServerResponse} res
  241. * @param {String|Buffer|Object|Stream} body
  242. * @param {Boolean} [generateEtag = true]
  243. *
  244. * @return {void}
  245. *
  246. * @example
  247. * ```js
  248. * nodeRes.send(req, res, 'Hello world')
  249. *
  250. * // or html
  251. * nodeRes.send(req, res, '<h2> Hello world </h2>')
  252. *
  253. * // or JSON
  254. * nodeRes.send(req, res, { greeting: 'Hello world' })
  255. *
  256. * // or Buffer
  257. * nodeRes.send(req, res, Buffer.from('Hello world', 'utf-8'))
  258. *
  259. * // Ignore etag
  260. * nodeRes.send(req, res, 'Hello world', false)
  261. * ```
  262. */
  263. Response.send = function (req, res, body = null, generateEtag = true) {
  264. /**
  265. * Handle streams
  266. */
  267. if (body && typeof (body.pipe) === 'function') {
  268. Response
  269. .stream(res, body)
  270. .catch((error) => {
  271. Response.status(res, error.code === 'ENOENT' ? 404 : 500)
  272. Response.send(req, res, error.message, generateEtag)
  273. })
  274. return
  275. }
  276. const chunk = Response.prepare(res, body)
  277. if (chunk === null || req.method === 'HEAD') {
  278. Response.end(res)
  279. return
  280. }
  281. /**
  282. * Generate etag when instructured for
  283. */
  284. if (generateEtag) {
  285. Response.etag(res, chunk)
  286. }
  287. Response.end(res, chunk)
  288. }
  289. /**
  290. * Sets the Etag header for a given body chunk
  291. *
  292. * @method etag
  293. *
  294. * @param {http.ServerResponse} res
  295. * @param {String|Buffer} body
  296. *
  297. * @return {void}
  298. *
  299. * @example
  300. * ```js
  301. * nodeRes.etag(res, 'Hello world')
  302. * ```
  303. */
  304. Response.etag = function (res, body) {
  305. Response.header(res, 'ETag', etag(body))
  306. }
  307. /**
  308. * Prepares the response body by encoding it properly. Also
  309. * sets appropriate headers based upon the body content type.
  310. *
  311. * This method is used internally by `send`, so you should
  312. * never use it when calling `send`.
  313. *
  314. * It is helpful when you want to get the final payload and end the
  315. * response at a later stage.
  316. *
  317. * @method prepare
  318. *
  319. * @param {http.ServerResponse} res
  320. * @param {Mixed} body
  321. *
  322. * @return {String}
  323. *
  324. * @example
  325. * ```js
  326. * const chunk = nodeRes.prepare(res, '<h2> Hello </h2>')
  327. *
  328. * if (chunk) {
  329. * nodeRes.etag(res, chunk)
  330. *
  331. * if (nodeReq.fresh(req, res)) {
  332. * chunk = null
  333. * nodeRes.status(304)
  334. * }
  335. *
  336. * nodeRes.end(chunk)
  337. * }
  338. * ```
  339. */
  340. Response.prepare = function (res, body) {
  341. if (body === null) {
  342. Response.status(res, 204)
  343. Response.removeHeader(res, 'Content-Type')
  344. Response.removeHeader(res, 'Content-Length')
  345. Response.removeHeader(res, 'Transfer-Encoding')
  346. return null
  347. }
  348. let { body: chunk, type } = returnContentAndType(body)
  349. /**
  350. * Remove unwanted headers when statuscode is 204 or 304
  351. */
  352. if (res.statusCode === 204 || res.statusCode === 304) {
  353. Response.removeHeader(res, 'Content-Type')
  354. Response.removeHeader(res, 'Content-Length')
  355. Response.removeHeader(res, 'Transfer-Encoding')
  356. return chunk
  357. }
  358. const headers = typeof res.getHeaders === 'function' ? res.getHeaders() : (res._headers || {})
  359. /**
  360. * Setting content type. Ideally we can use `Response.type`, which
  361. * sets the right charset too. But we will be doing extra
  362. * processing for no reasons.
  363. */
  364. if (type && !headers['content-type']) {
  365. Response.header(res, 'Content-Type', `${type}; charset=utf-8`)
  366. }
  367. /**
  368. * setting up content length as response header
  369. */
  370. if (chunk && !headers['content-length']) {
  371. Response.header(res, 'Content-Length', Buffer.byteLength(chunk))
  372. }
  373. return chunk
  374. }
  375. /**
  376. * Prepares response for JSONP
  377. *
  378. * @method prepareJsonp
  379. *
  380. * @param {http.ServerResponse} res
  381. * @param {Object} body
  382. * @param {String} callbackFn
  383. *
  384. * @return {String}
  385. *
  386. * @example
  387. * ```js
  388. * const chunk = nodeRes.prepareJsonp(res, '<h2> Hello </h2>', 'callback')
  389. *
  390. * if (chunk) {
  391. * nodeRes.etag(res, chunk)
  392. *
  393. * if (nodeReq.fresh(req, res)) {
  394. * chunk = null
  395. * nodeRes.status(304)
  396. * }
  397. *
  398. * nodeRes.end(chunk)
  399. * }
  400. * ```
  401. */
  402. Response.prepareJsonp = function (res, body, callbackFn) {
  403. Response.header(res, 'X-Content-Type-Options', 'nosniff')
  404. Response.safeHeader(res, 'Content-Type', 'text/javascript; charset=utf-8')
  405. const parsedBody = JSON
  406. .stringify(body)
  407. .replace(/\u2028/g, '\\u2028')
  408. .replace(/\u2029/g, '\\u2029')
  409. /**
  410. * setting up callbackFn on response body , typeof will make
  411. * sure not to throw error of client if callbackFn is not
  412. * a function
  413. */
  414. return '/**/ typeof ' + callbackFn + " === 'function' && " + callbackFn + '(' + parsedBody + ');'
  415. }
  416. /**
  417. * Returns the HTTP response with `Content-type`
  418. * set to `application/json`.
  419. *
  420. * @method json
  421. *
  422. * @param {http.IncomingMessage} req
  423. * @param {http.ServerResponse} res
  424. * @param {Object} body
  425. * @param {Boolean} [generateEtag = true]
  426. *
  427. * @return {void}
  428. *
  429. * @example
  430. * ```js
  431. * nodeRes.json(req, res, { name: 'virk' })
  432. * nodeRes.json(req, res, [ 'virk', 'joe' ])
  433. * ```
  434. */
  435. Response.json = function (req, res, body, generateEtag) {
  436. Response.safeHeader(res, 'Content-Type', 'application/json; charset=utf-8')
  437. Response.send(req, res, body, generateEtag)
  438. }
  439. /**
  440. * Make JSONP response with `Content-type` set to
  441. * `text/javascript`.
  442. *
  443. * @method jsonp
  444. *
  445. * @param {http.IncomingMessage} req
  446. * @param {http.ServerResponse} res
  447. * @param {Object} body
  448. * @param {String} [callbackFn = 'callback']
  449. * @param {Boolean} [generateEtag = true]
  450. *
  451. * @return {void}
  452. *
  453. * @example
  454. * ```js
  455. * nodeRes.jsonp(req, res, { name: 'virk' }, 'callback')
  456. * ```
  457. */
  458. Response.jsonp = function (req, res, body, callbackFn = 'callback', generateEtag) {
  459. Response.send(req, res, Response.prepareJsonp(res, body, callbackFn), generateEtag)
  460. }
  461. /**
  462. * Set `Location` header on the HTTP response.
  463. *
  464. * @method location
  465. *
  466. * @param {http.ServerResponse} res
  467. * @param {String} url
  468. *
  469. * @return {void}
  470. */
  471. Response.location = function (res, url) {
  472. Response.header(res, 'Location', url)
  473. }
  474. /**
  475. * Redirect the HTTP request to the given url.
  476. *
  477. * @method redirect
  478. *
  479. * @param {http.IncomingMessage} req
  480. * @param {http.ServerResponse} res
  481. * @param {String} url
  482. * @param {Number} [status = 302]
  483. *
  484. * @return {void}
  485. *
  486. * @example
  487. * ```js
  488. * nodeRes.redirect(req, res, '/')
  489. * ```
  490. */
  491. Response.redirect = function (req, res, url, status = 302) {
  492. const body = ''
  493. Response.status(res, status)
  494. Response.location(res, url)
  495. Response.send(req, res, body)
  496. }
  497. /**
  498. * Add vary header to the HTTP response.
  499. *
  500. * @method vary
  501. *
  502. * @param {http.ServerResponse} res
  503. * @param {String} field
  504. *
  505. * @return {void}
  506. */
  507. Response.vary = function (res, field) {
  508. vary(res, field)
  509. }
  510. /**
  511. * Set content type header by looking up the actual
  512. * type and setting charset to utf8.
  513. *
  514. * ### Note
  515. * When defining custom charset, you must set pass the complete
  516. * content type, otherwise `false` will be set as the
  517. * content-type header.
  518. *
  519. * @method type
  520. *
  521. * @param {http.IncomingMessage} req
  522. * @param {http.ServerResponse} res
  523. * @param {String} [charset]
  524. * @return {void}
  525. *
  526. * @example
  527. * ```js
  528. * nodeRes.type(res, 'html')
  529. *
  530. * nodeRes.type(res, 'json')
  531. *
  532. * nodeRes.type(res, 'text/html', 'ascii')
  533. * ```
  534. */
  535. Response.type = function (res, type, charset) {
  536. type = charset ? `${type}; charset=${charset}` : type
  537. Response.safeHeader(res, 'Content-Type', mime.contentType(type))
  538. }
  539. /**
  540. * Pipe stream to the response. Also this method will make sure
  541. * to destroy the stream, if request gets cancelled.
  542. *
  543. * The promise resolve when response finishes and rejects, when
  544. * stream raises errors.
  545. *
  546. * @method stream
  547. *
  548. * @param {Object} res
  549. * @param {Stream} body
  550. *
  551. * @returns {Promise}
  552. *
  553. * @example
  554. * ```js
  555. * Response.stream(res, fs.createReadStream('foo.txt'))
  556. *
  557. * // handle stream errors
  558. * Response
  559. * .stream(res, fs.createReadStream('foo.txt'))
  560. * .catch((error) => {
  561. * })
  562. * ```
  563. */
  564. Response.stream = function (res, body) {
  565. return new Promise((resolve, reject) => {
  566. if (typeof (body.pipe) !== 'function') {
  567. reject(new Error('Body is not a valid stream'))
  568. return
  569. }
  570. let finished = false
  571. /**
  572. * Error in stream
  573. */
  574. body.on('error', (error) => {
  575. if (finished) {
  576. return
  577. }
  578. finished = true
  579. destroy(body)
  580. reject(error)
  581. })
  582. /**
  583. * Consumed stream
  584. */
  585. body.on('end', resolve)
  586. /**
  587. * Written response
  588. */
  589. onFinished(res, function () {
  590. finished = true
  591. destroy(body)
  592. })
  593. /**
  594. * Pipe to res
  595. */
  596. body.pipe(res)
  597. })
  598. }