199 lines
4.5 KiB
JavaScript
199 lines
4.5 KiB
JavaScript
|
/*!
|
||
|
* errorhandler
|
||
|
* Copyright(c) 2010 Sencha Inc.
|
||
|
* Copyright(c) 2011 TJ Holowaychuk
|
||
|
* Copyright(c) 2014 Jonathan Ong
|
||
|
* Copyright(c) 2014-2015 Douglas Christopher Wilson
|
||
|
* MIT Licensed
|
||
|
*/
|
||
|
|
||
|
'use strict'
|
||
|
|
||
|
/**
|
||
|
* Module dependencies.
|
||
|
* @private
|
||
|
*/
|
||
|
|
||
|
var accepts = require('accepts')
|
||
|
var escapeHtml = require('escape-html')
|
||
|
var fs = require('fs')
|
||
|
var path = require('path')
|
||
|
var util = require('util')
|
||
|
|
||
|
/**
|
||
|
* Module variables.
|
||
|
* @private
|
||
|
*/
|
||
|
|
||
|
var DOUBLE_SPACE_REGEXP = /\x20{2}/g
|
||
|
var NEW_LINE_REGEXP = /\n/g
|
||
|
var STYLESHEET = fs.readFileSync(path.join(__dirname, '/public/style.css'), 'utf8')
|
||
|
var TEMPLATE = fs.readFileSync(path.join(__dirname, '/public/error.html'), 'utf8')
|
||
|
var inspect = util.inspect
|
||
|
var toString = Object.prototype.toString
|
||
|
|
||
|
/* istanbul ignore next */
|
||
|
var defer = typeof setImmediate === 'function'
|
||
|
? setImmediate
|
||
|
: function (fn) { process.nextTick(fn.bind.apply(fn, arguments)) }
|
||
|
|
||
|
/**
|
||
|
* Error handler:
|
||
|
*
|
||
|
* Development error handler, providing stack traces
|
||
|
* and error message responses for requests accepting text, html,
|
||
|
* or json.
|
||
|
*
|
||
|
* Text:
|
||
|
*
|
||
|
* By default, and when _text/plain_ is accepted a simple stack trace
|
||
|
* or error message will be returned.
|
||
|
*
|
||
|
* JSON:
|
||
|
*
|
||
|
* When _application/json_ is accepted, connect will respond with
|
||
|
* an object in the form of `{ "error": error }`.
|
||
|
*
|
||
|
* HTML:
|
||
|
*
|
||
|
* When accepted connect will output a nice html stack trace.
|
||
|
*
|
||
|
* @return {Function}
|
||
|
* @api public
|
||
|
*/
|
||
|
|
||
|
exports = module.exports = function errorHandler (options) {
|
||
|
// get environment
|
||
|
var env = process.env.NODE_ENV || 'development'
|
||
|
|
||
|
// get options
|
||
|
var opts = options || {}
|
||
|
|
||
|
// get log option
|
||
|
var log = opts.log === undefined
|
||
|
? env !== 'test'
|
||
|
: opts.log
|
||
|
|
||
|
if (typeof log !== 'function' && typeof log !== 'boolean') {
|
||
|
throw new TypeError('option log must be function or boolean')
|
||
|
}
|
||
|
|
||
|
// default logging using console.error
|
||
|
if (log === true) {
|
||
|
log = logerror
|
||
|
}
|
||
|
|
||
|
return function errorHandler (err, req, res, next) {
|
||
|
// respect err.statusCode
|
||
|
if (err.statusCode) {
|
||
|
res.statusCode = err.statusCode
|
||
|
}
|
||
|
|
||
|
// respect err.status
|
||
|
if (err.status) {
|
||
|
res.statusCode = err.status
|
||
|
}
|
||
|
|
||
|
// default status code to 500
|
||
|
if (res.statusCode < 400) {
|
||
|
res.statusCode = 500
|
||
|
}
|
||
|
|
||
|
// log the error
|
||
|
var str = stringify(err)
|
||
|
if (log) {
|
||
|
defer(log, err, str, req, res)
|
||
|
}
|
||
|
|
||
|
// cannot actually respond
|
||
|
if (res._header) {
|
||
|
return req.socket.destroy()
|
||
|
}
|
||
|
|
||
|
// negotiate
|
||
|
var accept = accepts(req)
|
||
|
var type = accept.type('html', 'json', 'text')
|
||
|
|
||
|
// Security header for content sniffing
|
||
|
res.setHeader('X-Content-Type-Options', 'nosniff')
|
||
|
|
||
|
// html
|
||
|
if (type === 'html') {
|
||
|
var isInspect = !err.stack && String(err) === toString.call(err)
|
||
|
var errorHtml = !isInspect
|
||
|
? escapeHtmlBlock(str.split('\n', 1)[0] || 'Error')
|
||
|
: 'Error'
|
||
|
var stack = !isInspect
|
||
|
? String(str).split('\n').slice(1)
|
||
|
: [str]
|
||
|
var stackHtml = stack
|
||
|
.map(function (v) { return '<li>' + escapeHtmlBlock(v) + '</li>' })
|
||
|
.join('')
|
||
|
var body = TEMPLATE
|
||
|
.replace('{style}', STYLESHEET)
|
||
|
.replace('{stack}', stackHtml)
|
||
|
.replace('{title}', escapeHtml(exports.title))
|
||
|
.replace('{statusCode}', res.statusCode)
|
||
|
.replace(/\{error\}/g, errorHtml)
|
||
|
res.setHeader('Content-Type', 'text/html; charset=utf-8')
|
||
|
res.end(body)
|
||
|
// json
|
||
|
} else if (type === 'json') {
|
||
|
var error = { message: err.message, stack: err.stack }
|
||
|
for (var prop in err) error[prop] = err[prop]
|
||
|
var json = JSON.stringify({ error: error }, null, 2)
|
||
|
res.setHeader('Content-Type', 'application/json; charset=utf-8')
|
||
|
res.end(json)
|
||
|
// plain text
|
||
|
} else {
|
||
|
res.setHeader('Content-Type', 'text/plain; charset=utf-8')
|
||
|
res.end(str)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Template title, framework authors may override this value.
|
||
|
*/
|
||
|
|
||
|
exports.title = 'Connect'
|
||
|
|
||
|
/**
|
||
|
* Escape a block of HTML, preserving whitespace.
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
function escapeHtmlBlock (str) {
|
||
|
return escapeHtml(str)
|
||
|
.replace(DOUBLE_SPACE_REGEXP, ' ')
|
||
|
.replace(NEW_LINE_REGEXP, '<br>')
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Stringify a value.
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
function stringify (val) {
|
||
|
var stack = val.stack
|
||
|
|
||
|
if (stack) {
|
||
|
return String(stack)
|
||
|
}
|
||
|
|
||
|
var str = String(val)
|
||
|
|
||
|
return str === toString.call(val)
|
||
|
? inspect(val)
|
||
|
: str
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Log error to console.
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
function logerror (err, str) {
|
||
|
console.error(str || err.stack)
|
||
|
}
|