/*! * 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 '
  • ' + escapeHtmlBlock(v) + '
  • ' }) .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, '
    ') } /** * 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) }