index.js 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. /*!
  2. * errorhandler
  3. * Copyright(c) 2010 Sencha Inc.
  4. * Copyright(c) 2011 TJ Holowaychuk
  5. * Copyright(c) 2014 Jonathan Ong
  6. * Copyright(c) 2014-2015 Douglas Christopher Wilson
  7. * MIT Licensed
  8. */
  9. 'use strict'
  10. /**
  11. * Module dependencies.
  12. * @private
  13. */
  14. var accepts = require('accepts')
  15. var escapeHtml = require('escape-html')
  16. var fs = require('fs')
  17. var path = require('path')
  18. var util = require('util')
  19. /**
  20. * Module variables.
  21. * @private
  22. */
  23. var DOUBLE_SPACE_REGEXP = /\x20{2}/g
  24. var NEW_LINE_REGEXP = /\n/g
  25. var STYLESHEET = fs.readFileSync(path.join(__dirname, '/public/style.css'), 'utf8')
  26. var TEMPLATE = fs.readFileSync(path.join(__dirname, '/public/error.html'), 'utf8')
  27. var inspect = util.inspect
  28. var toString = Object.prototype.toString
  29. /* istanbul ignore next */
  30. var defer = typeof setImmediate === 'function'
  31. ? setImmediate
  32. : function (fn) { process.nextTick(fn.bind.apply(fn, arguments)) }
  33. /**
  34. * Error handler:
  35. *
  36. * Development error handler, providing stack traces
  37. * and error message responses for requests accepting text, html,
  38. * or json.
  39. *
  40. * Text:
  41. *
  42. * By default, and when _text/plain_ is accepted a simple stack trace
  43. * or error message will be returned.
  44. *
  45. * JSON:
  46. *
  47. * When _application/json_ is accepted, connect will respond with
  48. * an object in the form of `{ "error": error }`.
  49. *
  50. * HTML:
  51. *
  52. * When accepted connect will output a nice html stack trace.
  53. *
  54. * @return {Function}
  55. * @api public
  56. */
  57. exports = module.exports = function errorHandler (options) {
  58. // get environment
  59. var env = process.env.NODE_ENV || 'development'
  60. // get options
  61. var opts = options || {}
  62. // get log option
  63. var log = opts.log === undefined
  64. ? env !== 'test'
  65. : opts.log
  66. if (typeof log !== 'function' && typeof log !== 'boolean') {
  67. throw new TypeError('option log must be function or boolean')
  68. }
  69. // default logging using console.error
  70. if (log === true) {
  71. log = logerror
  72. }
  73. return function errorHandler (err, req, res, next) {
  74. // respect err.statusCode
  75. if (err.statusCode) {
  76. res.statusCode = err.statusCode
  77. }
  78. // respect err.status
  79. if (err.status) {
  80. res.statusCode = err.status
  81. }
  82. // default status code to 500
  83. if (res.statusCode < 400) {
  84. res.statusCode = 500
  85. }
  86. // log the error
  87. var str = stringify(err)
  88. if (log) {
  89. defer(log, err, str, req, res)
  90. }
  91. // cannot actually respond
  92. if (res._header) {
  93. return req.socket.destroy()
  94. }
  95. // negotiate
  96. var accept = accepts(req)
  97. var type = accept.type('html', 'json', 'text')
  98. // Security header for content sniffing
  99. res.setHeader('X-Content-Type-Options', 'nosniff')
  100. // html
  101. if (type === 'html') {
  102. var isInspect = !err.stack && String(err) === toString.call(err)
  103. var errorHtml = !isInspect
  104. ? escapeHtmlBlock(str.split('\n', 1)[0] || 'Error')
  105. : 'Error'
  106. var stack = !isInspect
  107. ? String(str).split('\n').slice(1)
  108. : [str]
  109. var stackHtml = stack
  110. .map(function (v) { return '<li>' + escapeHtmlBlock(v) + '</li>' })
  111. .join('')
  112. var body = TEMPLATE
  113. .replace('{style}', STYLESHEET)
  114. .replace('{stack}', stackHtml)
  115. .replace('{title}', escapeHtml(exports.title))
  116. .replace('{statusCode}', res.statusCode)
  117. .replace(/\{error\}/g, errorHtml)
  118. res.setHeader('Content-Type', 'text/html; charset=utf-8')
  119. res.end(body)
  120. // json
  121. } else if (type === 'json') {
  122. var error = { message: err.message, stack: err.stack }
  123. for (var prop in err) error[prop] = err[prop]
  124. var json = JSON.stringify({ error: error }, null, 2)
  125. res.setHeader('Content-Type', 'application/json; charset=utf-8')
  126. res.end(json)
  127. // plain text
  128. } else {
  129. res.setHeader('Content-Type', 'text/plain; charset=utf-8')
  130. res.end(str)
  131. }
  132. }
  133. }
  134. /**
  135. * Template title, framework authors may override this value.
  136. */
  137. exports.title = 'Connect'
  138. /**
  139. * Escape a block of HTML, preserving whitespace.
  140. * @api private
  141. */
  142. function escapeHtmlBlock (str) {
  143. return escapeHtml(str)
  144. .replace(DOUBLE_SPACE_REGEXP, ' &nbsp;')
  145. .replace(NEW_LINE_REGEXP, '<br>')
  146. }
  147. /**
  148. * Stringify a value.
  149. * @api private
  150. */
  151. function stringify (val) {
  152. var stack = val.stack
  153. if (stack) {
  154. return String(stack)
  155. }
  156. var str = String(val)
  157. return str === toString.call(val)
  158. ? inspect(val)
  159. : str
  160. }
  161. /**
  162. * Log error to console.
  163. * @api private
  164. */
  165. function logerror (err, str) {
  166. console.error(str || err.stack)
  167. }