'use strict'; const http = require('http'); const path = require('path'); const fs = require('fs'); const escapeHtml = require('escape-html'); const sendToWormhole = require('stream-wormhole'); const env = process.env.NODE_ENV || 'development'; const isDev = env === 'development'; const templatePath = isDev ? path.join(__dirname, 'templates/dev_error.html') : path.join(__dirname, 'templates/prod_error.html'); const defaultTemplate = fs.readFileSync(templatePath, 'utf8'); const defaultOptions = { text, json, html, redirect: null, template: path.join(__dirname, 'error.html'), accepts: null, }; module.exports = function onerror(app, options) { options = Object.assign({}, defaultOptions, options); app.context.onerror = function(err) { // don't do anything if there is no error. // this allows you to pass `this.onerror` // to node-style callbacks. if (err == null) return; // ignore all pedding request stream if (this.req) sendToWormhole(this.req); // wrap non-error object if (!(err instanceof Error)) { let errMsg = err; if (typeof err === 'object') { try { errMsg = JSON.stringify(err); // eslint-disable-next-line no-empty } catch (e) {} } const newError = new Error('non-error thrown: ' + errMsg); // err maybe an object, try to copy the name, message and stack to the new error instance if (err) { if (err.name) newError.name = err.name; if (err.message) newError.message = err.message; if (err.stack) newError.stack = err.stack; if (err.status) newError.status = err.status; if (err.headers) newError.headers = err.headers; } err = newError; } const headerSent = this.headerSent || !this.writable; if (headerSent) err.headerSent = true; // delegate this.app.emit('error', err, this); // nothing we can do here other // than delegate to the app-level // handler and log. if (headerSent) return; // ENOENT support if (err.code === 'ENOENT') err.status = 404; if (typeof err.status !== 'number' || !http.STATUS_CODES[err.status]) { err.status = 500; } this.status = err.status; this.set(err.headers); let type = 'text'; if (options.accepts) { type = options.accepts.call(this, 'html', 'text', 'json'); } else { type = this.accepts('html', 'text', 'json'); } type = type || 'text'; if (options.all) { options.all.call(this, err, this); } else { if (options.redirect && type !== 'json') { this.redirect(options.redirect); } else { options[type].call(this, err, this); this.type = type; } } if (type === 'json') { this.body = JSON.stringify(this.body); } this.res.end(this.body); }; return app; }; /** * default text error handler * @param {Error} err */ function text(err, ctx) { // unset all headers, and set those specified ctx.res._headers = {}; ctx.set(err.headers); ctx.body = (isDev || err.expose) && err.message ? err.message : http.STATUS_CODES[this.status]; } /** * default json error handler * @param {Error} err */ function json(err, ctx) { const message = (isDev || err.expose) && err.message ? err.message : http.STATUS_CODES[this.status]; ctx.body = { error: message }; } /** * default html error handler * @param {Error} err */ function html(err, ctx) { ctx.body = defaultTemplate .replace('{{status}}', escapeHtml(err.status)) .replace('{{stack}}', escapeHtml(err.stack)); ctx.type = 'html'; }