├── .eslintrc ├── test ├── fixtures │ ├── onerror │ │ ├── package.json │ │ ├── app │ │ │ ├── controller │ │ │ │ ├── user.js │ │ │ │ └── home.js │ │ │ └── router.js │ │ └── config │ │ │ └── config.default.js │ ├── agent-error │ │ ├── package.json │ │ └── agent.js │ ├── onerror-4xx │ │ ├── package.json │ │ ├── config │ │ │ └── config.default.js │ │ └── app │ │ │ ├── router.js │ │ │ └── controller │ │ │ ├── user.js │ │ │ └── home.js │ ├── mock-test-error │ │ └── package.json │ ├── onerror-ctx-error │ │ ├── package.json │ │ ├── app │ │ │ ├── extend │ │ │ │ └── context.js │ │ │ └── middleware │ │ │ │ └── trigger.js │ │ └── config │ │ │ └── config.default.js │ ├── onerror-custom-500 │ │ ├── package.json │ │ ├── config │ │ │ └── config.default.js │ │ └── app │ │ │ └── router.js │ ├── onerror-customize │ │ ├── package.json │ │ ├── app │ │ │ ├── controller │ │ │ │ ├── user.js │ │ │ │ └── home.js │ │ │ └── router.js │ │ └── config │ │ │ └── config.default.js │ ├── onerror-no-errorpage │ │ ├── package.json │ │ ├── config │ │ │ └── config.default.js │ │ └── app │ │ │ ├── router.js │ │ │ └── controller │ │ │ ├── user.js │ │ │ └── home.js │ ├── custom-listener-onerror │ │ ├── package.json │ │ ├── app │ │ │ └── router.js │ │ └── config │ │ │ └── config.default.js │ └── onerror-custom-template │ │ ├── package.json │ │ ├── app │ │ ├── controller │ │ │ ├── user.js │ │ │ └── home.js │ │ └── router.js │ │ ├── config │ │ └── config.default.js │ │ └── template.mustache └── onerror.test.js ├── .gitignore ├── agent.js ├── .github └── workflows │ ├── release.yml │ └── nodejs.yml ├── config └── config.default.js ├── lib ├── utils.js ├── error_view.js └── onerror_page.mustache ├── LICENSE ├── package.json ├── README.md ├── app.js └── CHANGELOG.md /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-egg" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/onerror/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "onerror" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/agent-error/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "agent-error" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/onerror-4xx/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "onerror-4xx" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs/ 2 | npm-debug.log 3 | node_modules/ 4 | coverage/ 5 | run/ 6 | -------------------------------------------------------------------------------- /test/fixtures/mock-test-error/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mock-test-error" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/onerror-ctx-error/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "onerror-ctx-error" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/onerror-custom-500/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "onerror-custom-500" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/onerror-customize/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "onerror-customize" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/onerror-no-errorpage/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "onerror-no-errorpage" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/custom-listener-onerror/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "custom-listener-onerror" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/onerror-custom-template/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "onerror-customize-template" 3 | } 4 | -------------------------------------------------------------------------------- /agent.js: -------------------------------------------------------------------------------- 1 | module.exports = agent => { 2 | // should watch error event 3 | agent.on('error', err => { 4 | agent.coreLogger.error(err); 5 | }); 6 | }; 7 | -------------------------------------------------------------------------------- /test/fixtures/onerror-ctx-error/app/extend/context.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | get userId() { 3 | throw new Error('you can`t get userId.'); 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /test/fixtures/agent-error/agent.js: -------------------------------------------------------------------------------- 1 | module.exports = agent => { 2 | const done = agent.readyCallback(); 3 | setTimeout(() => { 4 | done(new Error('emit error')); 5 | }, 500); 6 | }; 7 | -------------------------------------------------------------------------------- /test/fixtures/onerror-ctx-error/app/middleware/trigger.js: -------------------------------------------------------------------------------- 1 | module.exports = () => { 2 | return async function(ctx, next) { 3 | await next(); 4 | ctx.logger.info('log something, then error happend.'); 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /test/fixtures/onerror-ctx-error/config/config.default.js: -------------------------------------------------------------------------------- 1 | exports.middleware = [ 2 | 'trigger', 3 | ]; 4 | 5 | exports.keys = 'foo,bar'; 6 | 7 | exports.logger = { 8 | level: 'NONE', 9 | consoleLevel: 'NONE', 10 | }; 11 | -------------------------------------------------------------------------------- /test/fixtures/custom-listener-onerror/app/router.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | app.get('/', async ctx => { 3 | const err = new Error('mock error'); 4 | err.name = ctx.query.name || 'Error'; 5 | throw err; 6 | }); 7 | }; 8 | -------------------------------------------------------------------------------- /test/fixtures/onerror-no-errorpage/config/config.default.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.onerror = { 4 | }; 5 | 6 | exports.logger = { 7 | level: 'NONE', 8 | consoleLevel: 'NONE', 9 | }; 10 | 11 | exports.keys = 'foo,bar'; 12 | -------------------------------------------------------------------------------- /test/fixtures/onerror-custom-500/config/config.default.js: -------------------------------------------------------------------------------- 1 | exports.onerror = { 2 | errorPageUrl: (_, ctx) => ctx.errorPageUrl || '/500', 3 | }; 4 | 5 | exports.keys = 'foo,bar'; 6 | 7 | exports.logger = { 8 | level: 'NONE', 9 | consoleLevel: 'NONE', 10 | }; 11 | -------------------------------------------------------------------------------- /test/fixtures/onerror-no-errorpage/app/router.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | app.get('/', app.controller.home.index); 3 | app.get('/csrf', app.controller.home.csrf); 4 | app.post('/test', app.controller.home.test); 5 | app.get('/user.json', app.controller.user); 6 | }; 7 | -------------------------------------------------------------------------------- /test/fixtures/onerror-4xx/config/config.default.js: -------------------------------------------------------------------------------- 1 | exports.onerror = { 2 | errorPageUrl: 'https://eggjs.com/500.html', 3 | }; 4 | 5 | exports.logger = { 6 | consoleLevel: 'NONE', 7 | }; 8 | 9 | exports.keys = 'foo,bar'; 10 | 11 | exports.security = { 12 | csrf: false, 13 | }; 14 | -------------------------------------------------------------------------------- /test/fixtures/onerror-4xx/app/router.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | app.get('/', app.controller.home.index); 3 | app.get('/csrf', app.controller.home.csrf); 4 | app.post('/test', app.controller.home.test); 5 | app.get('/user', app.controller.user); 6 | app.get('/user.json', app.controller.user); 7 | }; 8 | -------------------------------------------------------------------------------- /test/fixtures/onerror/app/controller/user.js: -------------------------------------------------------------------------------- 1 | module.exports = async ctx => { 2 | const err = new Error('test error'); 3 | if (ctx.query.status) { 4 | err.status = Number(ctx.query.status); 5 | } 6 | if (ctx.query.errors) { 7 | err.errors = ctx.query.errors; 8 | } 9 | throw err; 10 | }; 11 | -------------------------------------------------------------------------------- /test/fixtures/onerror/config/config.default.js: -------------------------------------------------------------------------------- 1 | exports.onerror = { 2 | errorPageUrl: 'https://eggjs.com/500.html', 3 | }; 4 | 5 | exports.logger = { 6 | level: 'NONE', 7 | consoleLevel: 'NONE', 8 | }; 9 | 10 | exports.keys = 'foo,bar'; 11 | 12 | exports.security = { 13 | csrf: false, 14 | }; 15 | 16 | -------------------------------------------------------------------------------- /test/fixtures/onerror-4xx/app/controller/user.js: -------------------------------------------------------------------------------- 1 | module.exports = async ctx => { 2 | const err = new Error('test error'); 3 | if (ctx.query.status) { 4 | err.status = Number(ctx.query.status); 5 | } 6 | if (ctx.query.errors) { 7 | err.errors = ctx.query.errors; 8 | } 9 | throw err; 10 | }; 11 | -------------------------------------------------------------------------------- /test/fixtures/onerror-customize/app/controller/user.js: -------------------------------------------------------------------------------- 1 | module.exports = async ctx => { 2 | const err = new Error('test error'); 3 | if (ctx.query.status) { 4 | err.status = Number(ctx.query.status); 5 | } 6 | if (ctx.query.errors) { 7 | err.errors = ctx.query.errors; 8 | } 9 | throw err; 10 | }; 11 | -------------------------------------------------------------------------------- /test/fixtures/onerror-no-errorpage/app/controller/user.js: -------------------------------------------------------------------------------- 1 | module.exports = async ctx => { 2 | const err = new Error('test error'); 3 | if (ctx.query.status) { 4 | err.status = Number(ctx.query.status); 5 | } 6 | if (ctx.query.errors) { 7 | err.errors = ctx.query.errors; 8 | } 9 | throw err; 10 | }; 11 | -------------------------------------------------------------------------------- /test/fixtures/onerror-custom-template/app/controller/user.js: -------------------------------------------------------------------------------- 1 | module.exports = async ctx => { 2 | const err = new Error('test error'); 3 | if (ctx.query.status) { 4 | err.status = Number(ctx.query.status); 5 | } 6 | if (ctx.query.errors) { 7 | err.errors = ctx.query.errors; 8 | } 9 | throw err; 10 | }; 11 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release for 2.x 2 | 3 | on: 4 | push: 5 | branches: [ 2.x ] 6 | 7 | jobs: 8 | release: 9 | name: Node.js 10 | uses: eggjs/github-actions/.github/workflows/node-release.yml@master 11 | secrets: 12 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 13 | GIT_TOKEN: ${{ secrets.GIT_TOKEN }} 14 | -------------------------------------------------------------------------------- /test/fixtures/onerror-custom-template/config/config.default.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | exports.onerror = { 4 | templatePath: path.join(__dirname, '../template.mustache'), 5 | }; 6 | 7 | exports.logger = { 8 | level: 'NONE', 9 | consoleLevel: 'NONE', 10 | }; 11 | 12 | exports.keys = 'foo,bar'; 13 | 14 | exports.security = { 15 | csrf: false, 16 | }; 17 | -------------------------------------------------------------------------------- /test/fixtures/onerror-customize/app/router.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | app.get('/', app.controller.home.index); 3 | app.get('/csrf', app.controller.home.csrf); 4 | app.post('/test', app.controller.home.test); 5 | app.get('/user', app.controller.user); 6 | app.get('/user.json', app.controller.user); 7 | app.get('/jsonp', app.jsonp(), app.controller.home.jsonp); 8 | }; 9 | -------------------------------------------------------------------------------- /test/fixtures/onerror-custom-template/app/router.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | app.get('/', app.controller.home.index); 3 | app.get('/csrf', app.controller.home.csrf); 4 | app.post('/test', app.controller.home.test); 5 | app.get('/user', app.controller.user); 6 | app.get('/user.json', app.controller.user); 7 | app.get('/jsonp', app.jsonp(), app.controller.home.jsonp); 8 | }; 9 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: CI for 2.x 2 | 3 | on: 4 | push: 5 | branches: [ 2.x ] 6 | pull_request: 7 | branches: [ 2.x ] 8 | 9 | jobs: 10 | Job: 11 | name: Node.js 12 | uses: node-modules/github-actions/.github/workflows/node-test.yml@master 13 | with: 14 | os: 'ubuntu-latest, macos-latest, windows-latest' 15 | version: '14, 16, 18, 20, 22' 16 | -------------------------------------------------------------------------------- /test/fixtures/onerror-customize/config/config.default.js: -------------------------------------------------------------------------------- 1 | exports.onerror = { 2 | errorPageUrl: 'https://eggjs.com/500.html', 3 | json(err, ctx) { 4 | ctx.body = { msg: 'error' }; 5 | ctx.status = 500; 6 | }, 7 | }; 8 | 9 | exports.logger = { 10 | level: 'NONE', 11 | consoleLevel: 'NONE', 12 | }; 13 | 14 | exports.keys = 'foo,bar'; 15 | 16 | exports.security = { 17 | csrf: false, 18 | }; 19 | -------------------------------------------------------------------------------- /test/fixtures/onerror/app/router.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | app.get('/', app.controller.home.index); 3 | app.get('/unknownFile', app.controller.home.unknownFile); 4 | app.get('/csrf', app.controller.home.csrf); 5 | app.post('/test', app.controller.home.test); 6 | app.get('/user', app.controller.user); 7 | app.get('/user.json', app.controller.user); 8 | app.get('/jsonp', app.jsonp(), app.controller.home.jsonp); 9 | }; 10 | -------------------------------------------------------------------------------- /test/fixtures/custom-listener-onerror/config/config.default.js: -------------------------------------------------------------------------------- 1 | exports.onerror = { 2 | errorPageUrl: 'https://eggjs.com/500.html', 3 | appErrorFilter(err, ctx) { 4 | if (err.name === 'IgnoreError') return false; 5 | if (err.name === 'CustomError') { 6 | ctx.app.logger.error('error happened'); 7 | return false; 8 | } 9 | return true; 10 | }, 11 | }; 12 | 13 | exports.keys = 'foo,bar'; 14 | 15 | exports.logger = { 16 | level: 'NONE', 17 | consoleLevel: 'NONE', 18 | }; 19 | -------------------------------------------------------------------------------- /config/config.default.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | exports.onerror = { 4 | // 5xx error will redirect to ${errorPageUrl} 5 | // won't redirect in local env 6 | errorPageUrl: '', 7 | // will excute `appErrorFilter` when emit an error in `app` 8 | // If `appErrorFilter` return false, egg-onerror won't log this error. 9 | // You can logging in `appErrorFilter` and return false to override the default error logging. 10 | appErrorFilter: null, 11 | // default template path 12 | templatePath: path.join(__dirname, '../lib/onerror_page.mustache'), 13 | }; 14 | -------------------------------------------------------------------------------- /test/fixtures/onerror-custom-500/app/router.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | app.get('/mockerror', async () => { 3 | // eslint-disable-next-line 4 | hi.foo(); 5 | }); 6 | 7 | app.get('/mock4xx', async () => { 8 | const err = new Error('4xx error'); 9 | err.status = 400; 10 | throw err; 11 | }); 12 | 13 | app.get('/500', async ctx => { 14 | ctx.status = 500; 15 | ctx.body = 'hi, this custom 500 page'; 16 | }); 17 | 18 | app.get('/special', async ctx => { 19 | ctx.errorPageUrl = '/specialerror'; 20 | // eslint-disable-next-line 21 | hi.foo(); 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /test/fixtures/onerror-4xx/app/controller/home.js: -------------------------------------------------------------------------------- 1 | exports.index = async ctx => { 2 | const err = new Error('test error'); 3 | if (ctx.query.code) { 4 | err.code = ctx.query.code; 5 | } 6 | if (ctx.query.status) { 7 | err.status = Number(ctx.query.status); 8 | } 9 | if (ctx.query.message) { 10 | err.message = ctx.query.message; 11 | } 12 | throw err; 13 | }; 14 | 15 | exports.csrf = async ctx => { 16 | ctx.set('x-csrf', ctx.csrf); 17 | ctx.body = 'test'; 18 | }; 19 | 20 | exports.test = async ctx => { 21 | const err = new SyntaxError('syntax error'); 22 | if (ctx.query.status) { 23 | err.status = Number(ctx.query.status); 24 | } 25 | throw err; 26 | }; 27 | -------------------------------------------------------------------------------- /test/fixtures/onerror-no-errorpage/app/controller/home.js: -------------------------------------------------------------------------------- 1 | exports.index = async ctx => { 2 | const err = new Error('test error'); 3 | if (ctx.query.code) { 4 | err.code = ctx.query.code; 5 | } 6 | if (ctx.query.status) { 7 | err.status = Number(ctx.query.status); 8 | } 9 | if (ctx.query.message) { 10 | err.message = ctx.query.message; 11 | } 12 | throw err; 13 | }; 14 | 15 | exports.csrf = async ctx => { 16 | ctx.set('x-csrf', ctx.csrf); 17 | ctx.body = 'test'; 18 | }; 19 | 20 | exports.test = async ctx => { 21 | const err = new SyntaxError('syntax error'); 22 | if (ctx.query.status) { 23 | err.status = Number(ctx.query.status); 24 | } 25 | throw err; 26 | }; 27 | -------------------------------------------------------------------------------- /test/fixtures/onerror-customize/app/controller/home.js: -------------------------------------------------------------------------------- 1 | exports.index = async ctx => { 2 | const err = new Error('test error'); 3 | if (ctx.query.code) { 4 | err.code = ctx.query.code; 5 | } 6 | if (ctx.query.status) { 7 | err.status = Number(ctx.query.status); 8 | } 9 | if (ctx.query.message) { 10 | err.message = ctx.query.message; 11 | } 12 | throw err; 13 | }; 14 | 15 | exports.csrf = async ctx => { 16 | ctx.set('x-csrf', ctx.csrf); 17 | ctx.body = 'test'; 18 | }; 19 | 20 | exports.test = async ctx => { 21 | const err = new SyntaxError('syntax error'); 22 | if (ctx.query.status) { 23 | err.status = Number(ctx.query.status); 24 | } 25 | throw err; 26 | }; 27 | 28 | exports.jsonp = async () => { 29 | throw new Error('jsonp error'); 30 | }; 31 | -------------------------------------------------------------------------------- /test/fixtures/onerror-custom-template/app/controller/home.js: -------------------------------------------------------------------------------- 1 | exports.index = async ctx => { 2 | const err = new Error('test error'); 3 | if (ctx.query.code) { 4 | err.code = ctx.query.code; 5 | } 6 | if (ctx.query.status) { 7 | err.status = Number(ctx.query.status); 8 | } 9 | if (ctx.query.message) { 10 | err.message = ctx.query.message; 11 | } 12 | throw err; 13 | }; 14 | 15 | exports.csrf = async ctx => { 16 | ctx.set('x-csrf', ctx.csrf); 17 | ctx.body = 'test'; 18 | }; 19 | 20 | exports.test = async ctx => { 21 | const err = new SyntaxError('syntax error'); 22 | if (ctx.query.status) { 23 | err.status = Number(ctx.query.status); 24 | } 25 | throw err; 26 | }; 27 | 28 | exports.jsonp = async () => { 29 | throw new Error('jsonp error'); 30 | }; 31 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | exports.detectErrorMessage = function(ctx, err) { 2 | // detect json parse error 3 | if (err.status === 400 && 4 | err.name === 'SyntaxError' && 5 | ctx.request.is('application/json', 'application/vnd.api+json', 'application/csp-report')) { 6 | return 'Problems parsing JSON'; 7 | } 8 | return err.message; 9 | }; 10 | 11 | exports.detectStatus = function(err) { 12 | // detect status 13 | let status = err.status || 500; 14 | if (status < 200) { 15 | // invalid status consider as 500, like urllib will return -1 status 16 | status = 500; 17 | } 18 | return status; 19 | }; 20 | 21 | exports.accepts = function(ctx) { 22 | if (ctx.acceptJSON) return 'json'; 23 | if (ctx.acceptJSONP) return 'js'; 24 | return 'html'; 25 | }; 26 | 27 | exports.isProd = function(app) { 28 | return app.config.env !== 'local' && app.config.env !== 'unittest'; 29 | }; 30 | -------------------------------------------------------------------------------- /test/fixtures/onerror/app/controller/home.js: -------------------------------------------------------------------------------- 1 | exports.index = async ctx => { 2 | const err = new Error('test error'); 3 | if (ctx.query.code) { 4 | err.code = ctx.query.code; 5 | } 6 | if (ctx.query.status) { 7 | err.status = Number(ctx.query.status); 8 | } 9 | if (ctx.query.message) { 10 | err.message = ctx.query.message; 11 | } 12 | throw err; 13 | }; 14 | 15 | exports.unknownFile = async () => { 16 | const err = new Error('test error'); 17 | err.stack = err.stack.replace(/(controller\/home\.)js/, '$1ts'); 18 | throw err; 19 | }; 20 | 21 | exports.csrf = async ctx => { 22 | ctx.set('x-csrf', ctx.csrf); 23 | ctx.body = 'test'; 24 | }; 25 | 26 | exports.test = async ctx => { 27 | const err = new SyntaxError('syntax error'); 28 | if (ctx.query.status) { 29 | err.status = Number(ctx.query.status); 30 | } 31 | throw err; 32 | }; 33 | 34 | exports.jsonp = async () => { 35 | throw new Error('jsonp error'); 36 | }; 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-present Alibaba Group Holding Limited and other contributors. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "egg-onerror", 3 | "version": "2.4.0", 4 | "description": "error handler for egg", 5 | "eggPlugin": { 6 | "name": "onerror", 7 | "optionalDependencies": [ 8 | "jsonp" 9 | ] 10 | }, 11 | "files": [ 12 | "config", 13 | "lib", 14 | "app.js", 15 | "agent.js" 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/eggjs/onerror.git" 20 | }, 21 | "keywords": [ 22 | "egg", 23 | "egg-plugin", 24 | "onerror" 25 | ], 26 | "dependencies": { 27 | "cookie": "^0.7.2", 28 | "koa-onerror": "^4.0.0", 29 | "mustache": "^2.3.0", 30 | "stack-trace": "^0.0.10" 31 | }, 32 | "devDependencies": { 33 | "egg": "^3.7.0", 34 | "egg-bin": "^5.5.0", 35 | "egg-mock": "^5.3.0", 36 | "eslint": "^8.29.0", 37 | "eslint-config-egg": "^12.1.0", 38 | "mocha": "^10.7.3" 39 | }, 40 | "engines": { 41 | "node": ">=8.0.0" 42 | }, 43 | "scripts": { 44 | "test": "npm run lint -- --fix && npm run test-local", 45 | "test-local": "egg-bin test", 46 | "cov": "egg-bin cov", 47 | "lint": "eslint .", 48 | "ci": "npm run lint && npm run cov" 49 | }, 50 | "author": "dead_horse" 51 | } 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # egg-onerror 2 | 3 | [![NPM version][npm-image]][npm-url] 4 | [![Node.js CI](https://github.com/eggjs/egg-onerror/actions/workflows/nodejs.yml/badge.svg)](https://github.com/eggjs/egg-onerror/actions/workflows/nodejs.yml) 5 | [![Test coverage][codecov-image]][codecov-url] 6 | [![Known Vulnerabilities][snyk-image]][snyk-url] 7 | [![npm download][download-image]][download-url] 8 | 9 | [npm-image]: https://img.shields.io/npm/v/egg-onerror.svg?style=flat-square 10 | [npm-url]: https://npmjs.org/package/egg-onerror 11 | [codecov-image]: https://codecov.io/github/eggjs/egg-onerror/coverage.svg?branch=master 12 | [codecov-url]: https://codecov.io/github/eggjs/egg-onerror?branch=master 13 | [snyk-image]: https://snyk.io/test/npm/egg-onerror/badge.svg?style=flat-square 14 | [snyk-url]: https://snyk.io/test/npm/egg-onerror 15 | [download-image]: https://img.shields.io/npm/dm/egg-onerror.svg?style=flat-square 16 | [download-url]: https://npmjs.org/package/egg-onerror 17 | 18 | Default error handling plugin for egg. 19 | 20 | ## Install 21 | 22 | ```bash 23 | npm i egg-onerror@2 24 | ``` 25 | 26 | ## Usage 27 | 28 | `egg-onerror` is on by default in egg. But you still can configure its properties to fits your scenarios. 29 | 30 | - `errorPageUrl: String or Function` - If user request html pages in production environment and unexpected error happened, it will redirect user to `errorPageUrl`. 31 | - `accepts: Function` - detect user's request accept `json` or `html`. 32 | - `all: Function` - customize error handler, if `all` present, negotiation will be ignored. 33 | - `html: Function` - customize html error handler. 34 | - `text: Function` - customize text error handler. 35 | - `json: Function` - customize json error handler. 36 | - `jsonp: Function` - customize jsonp error handler. 37 | 38 | ```js 39 | // config.default.js 40 | // errorPageUrl support function 41 | exports.onerror = { 42 | errorPageUrl: (err, ctx) => ctx.errorPageUrl || '/500', 43 | }; 44 | 45 | // an accept detect function that mark all request with `x-requested-with=XMLHttpRequest` header accepts json. 46 | function accepts(ctx) { 47 | if (ctx.get('x-requested-with') === 'XMLHttpRequest') return 'json'; 48 | return 'html'; 49 | } 50 | ``` 51 | 52 | ## Questions & Suggestions 53 | 54 | Please open an issue [here](https://github.com/eggjs/egg/issues). 55 | 56 | ## License 57 | 58 | [MIT](https://github.com/eggjs/egg-onerror/blob/master/LICENSE) 59 | 60 | ## Contributors 61 | 62 | [![Contributors](https://contrib.rocks/image?repo=eggjs/egg-onerror)](https://github.com/eggjs/egg-onerror/graphs/contributors) 63 | 64 | Made with [contributors-img](https://contrib.rocks). 65 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const fs = require('fs'); 3 | const onerror = require('koa-onerror'); 4 | const ErrorView = require('./lib/error_view'); 5 | const { 6 | isProd, 7 | detectStatus, 8 | detectErrorMessage, 9 | accepts, 10 | } = require('./lib/utils'); 11 | 12 | module.exports = app => { 13 | // logging error 14 | const config = app.config.onerror; 15 | const viewTemplate = fs.readFileSync(config.templatePath, 'utf8'); 16 | 17 | app.on('error', (err, ctx) => { 18 | if (!ctx) { 19 | ctx = app.currentContext || app.createAnonymousContext(); 20 | } 21 | if (config.appErrorFilter && !config.appErrorFilter(err, ctx)) return; 22 | 23 | const status = detectStatus(err); 24 | // 5xx 25 | if (status >= 500) { 26 | try { 27 | ctx.logger.error(err); 28 | } catch (ex) { 29 | app.logger.error(err); 30 | app.logger.error(ex); 31 | } 32 | return; 33 | } 34 | 35 | // 4xx 36 | try { 37 | ctx.logger.warn(err); 38 | } catch (ex) { 39 | app.logger.warn(err); 40 | app.logger.error(ex); 41 | } 42 | }); 43 | 44 | const errorOptions = { 45 | // support customize accepts function 46 | accepts() { 47 | const fn = config.accepts || accepts; 48 | return fn(this); 49 | }, 50 | 51 | html(err) { 52 | const status = detectStatus(err); 53 | const errorPageUrl = typeof config.errorPageUrl === 'function' 54 | ? config.errorPageUrl(err, this) 55 | : config.errorPageUrl; 56 | 57 | // keep the real response status 58 | this.realStatus = status; 59 | // don't respond any error message in production env 60 | if (isProd(app)) { 61 | // 5xx 62 | if (status >= 500) { 63 | if (errorPageUrl) { 64 | const statusQuery = 65 | (errorPageUrl.indexOf('?') > 0 ? '&' : '?') + 66 | `real_status=${status}`; 67 | return this.redirect(errorPageUrl + statusQuery); 68 | } 69 | this.status = 500; 70 | this.body = `

Internal Server Error, real status: ${status}

`; 71 | return; 72 | } 73 | // 4xx 74 | this.status = status; 75 | this.body = `

${status} ${http.STATUS_CODES[status]}

`; 76 | return; 77 | } 78 | // show simple error format for unittest 79 | if (app.config.env === 'unittest') { 80 | this.status = status; 81 | this.body = `${err.name}: ${err.message}\n${err.stack}`; 82 | return; 83 | } 84 | 85 | const errorView = new ErrorView(this, err, viewTemplate); 86 | this.body = errorView.toHTML(); 87 | }, 88 | 89 | json(err) { 90 | const status = detectStatus(err); 91 | let errorJson = {}; 92 | 93 | this.status = status; 94 | const code = err.code || err.type; 95 | const message = detectErrorMessage(this, err); 96 | 97 | if (isProd(app)) { 98 | // 5xx server side error 99 | if (status >= 500) { 100 | errorJson = { 101 | code, 102 | // don't respond any error message in production env 103 | message: http.STATUS_CODES[status], 104 | }; 105 | } else { 106 | // 4xx client side error 107 | // addition `errors` 108 | errorJson = { 109 | code, 110 | message, 111 | errors: err.errors, 112 | }; 113 | } 114 | } else { 115 | errorJson = { 116 | code, 117 | message, 118 | errors: err.errors, 119 | }; 120 | 121 | if (status >= 500) { 122 | // provide detail error stack in local env 123 | errorJson.stack = err.stack; 124 | errorJson.name = err.name; 125 | for (const key in err) { 126 | if (!errorJson[key]) { 127 | errorJson[key] = err[key]; 128 | } 129 | } 130 | } 131 | } 132 | 133 | this.body = errorJson; 134 | }, 135 | 136 | js(err) { 137 | errorOptions.json.call(this, err, this); 138 | 139 | if (this.createJsonpBody) { 140 | this.createJsonpBody(this.body); 141 | } 142 | }, 143 | }; 144 | 145 | // support customize error response 146 | [ 'all', 'html', 'json', 'text', 'js' ].forEach(type => { 147 | if (config[type]) errorOptions[type] = config[type]; 148 | }); 149 | onerror(app, errorOptions); 150 | }; 151 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [2.4.0](https://github.com/eggjs/egg-onerror/compare/v2.3.1...v2.4.0) (2024-10-13) 4 | 5 | 6 | ### Features 7 | 8 | * ignore secure config ([#34](https://github.com/eggjs/egg-onerror/issues/34)) ([bf61d5e](https://github.com/eggjs/egg-onerror/commit/bf61d5ee0edf128cc6ab082839c5bacbf8fa496f)) 9 | 10 | ## [2.3.1](https://github.com/eggjs/egg-onerror/compare/v2.3.0...v2.3.1) (2024-10-13) 11 | 12 | 13 | ### Bug Fixes 14 | 15 | * fixed show all frame ([#32](https://github.com/eggjs/egg-onerror/issues/32)) ([779fdfd](https://github.com/eggjs/egg-onerror/commit/779fdfdca6cb0dc04e79a4426d586c7d0b97e3f6)) 16 | 17 | ## [2.3.0](https://github.com/eggjs/egg-onerror/compare/v2.2.0...v2.3.0) (2024-10-13) 18 | 19 | 20 | ### Features 21 | 22 | * use cookie@0.7.2 ([#39](https://github.com/eggjs/egg-onerror/issues/39)) ([fc57345](https://github.com/eggjs/egg-onerror/commit/fc57345661ab6771d74ca5678438bfe7983929a1)) 23 | 24 | 2.2.0 / 2022-12-11 25 | ================== 26 | 27 | **features** 28 | * [[`a31167c`](http://github.com/eggjs/egg-onerror/commit/a31167ccf6ecc35d51b129299271588b32a51350)] - 👌 IMPROVE: Use currentContext first (#36) (fengmk2 <>) 29 | 30 | 2.1.1 / 2022-08-18 31 | ================== 32 | 33 | **fixes** 34 | * [[`00d2aa2`](http://github.com/eggjs/egg-onerror/commit/00d2aa2a073048dec9b9fc0fd0d868ecc0446830)] - fix: check file exists (#35) (吖猩 <>) 35 | 36 | **others** 37 | * [[`54e12ba`](http://github.com/eggjs/egg-onerror/commit/54e12baa2eab9b47a2acd6ba0e6f6a0e55c92fc0)] - Create codeql-analysis.yml (fengmk2 <>) 38 | * [[`55d27e6`](http://github.com/eggjs/egg-onerror/commit/55d27e60a9f1094e1a4555e82b47cab4799a57f8)] - chore: update travis (#31) (TZ | 天猪 <>) 39 | 40 | 2.1.0 / 2018-06-12 41 | ================== 42 | 43 | **features** 44 | * [[`63cca2f`](http://github.com/eggjs/egg-onerror/commit/63cca2f3fb087583459e26e38c3874285b14aefd)] - feat: add replace template config (#28) (Harry Chen <>) 45 | 46 | 2.0.0 / 2017-11-13 47 | ================== 48 | 49 | **others** 50 | * [[`6861f8f`](http://github.com/eggjs/egg-onerror/commit/6861f8fb5df4a210afca2c7454dcca4ec1ccbae4)] - refactor: use async function and support egg@2 (#27) (Yiyu He <>) 51 | 52 | 1.6.0 / 2017-11-13 53 | ================== 54 | 55 | **features** 56 | * [[`4a1b770`](http://github.com/eggjs/egg-onerror/commit/4a1b7707b28d3cc1e8bd69f4cca606305c507248)] - feat: support customize error handler (#26) (Yiyu He <>) 57 | * [[`37c06ce`](http://github.com/eggjs/egg-onerror/commit/37c06ce45fb671a3087f4e74aafcef1ac122360d)] - feat: support jsonp (#25) (Yiyu He <>) 58 | 59 | **others** 60 | * [[`a0d4df2`](http://github.com/eggjs/egg-onerror/commit/a0d4df2830bf58903dd27e277f963e3d52d32587)] - chore: fix README & update deps & fix test (#23) (TZ | 天猪 <>) 61 | * [[`e49252e`](http://github.com/eggjs/egg-onerror/commit/e49252e3a648abbefc562635e163c4b9dd28e57d)] - docs: fix typo (#22) (TZ | 天猪 <>) 62 | 63 | 1.5.0 / 2017-07-20 64 | ================== 65 | 66 | * feat: errorPageUrl support function (#21) 67 | 68 | 1.4.6 / 2017-06-20 69 | ================== 70 | 71 | * fix: only output simple error html on unittest (#19) 72 | 73 | 1.4.5 / 2017-06-20 74 | ================== 75 | 76 | * fix: should show 4xx status error html (#18) 77 | 78 | 1.4.4 / 2017-06-12 79 | ================== 80 | 81 | * fix: make error style more good (#17) 82 | * fix: add error-title's line-height (#16) 83 | 84 | 1.4.3 / 2017-06-04 85 | ================== 86 | 87 | * docs: fix License url (#15) 88 | 89 | 1.4.2 / 2017-06-01 90 | ================== 91 | 92 | * fix: remove error detail on JSON response (#14) 93 | 94 | 1.4.1 / 2017-06-01 95 | ================== 96 | 97 | * fix: add missing files on package.json (#13) 98 | 99 | 1.4.0 / 2017-05-31 100 | ================== 101 | 102 | * feat: better error page on development (#12) 103 | 104 | 1.3.0 / 2017-01-22 105 | ================== 106 | 107 | * feat: use ctx.acceptJSON (#11) 108 | 109 | 1.2.2 / 2017-01-13 110 | ================== 111 | 112 | * fix: should keep support `*.json` ext to detect response type (#10) 113 | 114 | 1.2.1 / 2017-01-13 115 | ================== 116 | 117 | * fix: add agent.js to package.files (#9) 118 | 119 | 1.2.0 / 2017-01-13 120 | ================== 121 | 122 | * feat: support options.accepts 123 | * refactor: remove accpetJSON 124 | 125 | 1.1.0 / 2016-11-09 126 | ================== 127 | 128 | * feat: should watch error event on agent (#7) 129 | 130 | 1.0.0 / 2016-10-21 131 | ================== 132 | 133 | * deps: upgrade koa-onerror (#6) 134 | * fix: make sure ctx always exists (#3) 135 | 136 | 0.0.3 / 2016-07-16 137 | ================== 138 | 139 | * fix: fix this is undefined on arrow function (#1) 140 | 141 | 0.0.2 / 2016-07-13 142 | ================== 143 | * init code 144 | -------------------------------------------------------------------------------- /lib/error_view.js: -------------------------------------------------------------------------------- 1 | // modify from https://github.com/poppinss/youch/blob/develop/src/Youch/index.js 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const cookie = require('cookie'); 6 | const Mustache = require('mustache'); 7 | const stackTrace = require('stack-trace'); 8 | const util = require('util'); 9 | 10 | const { detectErrorMessage } = require('./utils'); 11 | const startingSlashRegex = /\\|\//; 12 | 13 | class ErrorView { 14 | constructor(ctx, error, template) { 15 | this.codeContext = 5; 16 | this._filterHeaders = [ 'cookie', 'connection' ]; 17 | 18 | this.ctx = ctx; 19 | this.error = error; 20 | this.request = ctx.request; 21 | this.app = ctx.app; 22 | this.assets = new Map(); 23 | this.viewTemplate = template; 24 | } 25 | 26 | /** 27 | * get html error page 28 | * 29 | * @return {String} html page 30 | */ 31 | toHTML() { 32 | const stack = this.parseError(); 33 | const data = this.serializeData(stack, (frame, index) => { 34 | const serializedFrame = this.serializeFrame(frame); 35 | serializedFrame.classes = this.getFrameClasses(frame, index); 36 | return serializedFrame; 37 | }); 38 | 39 | data.request = this.serializeRequest(); 40 | data.appInfo = this.serializeAppInfo(); 41 | 42 | return this.complieView(this.viewTemplate, data); 43 | } 44 | 45 | /** 46 | * compile view 47 | * 48 | * @param {String} tpl - template 49 | * @param {Object} locals - data used by template 50 | * 51 | * @return {String} html 52 | */ 53 | complieView(tpl, locals) { 54 | return Mustache.render(tpl, locals); 55 | } 56 | 57 | /** 58 | * check if the frame is node native file. 59 | * 60 | * @param {Frame} frame - current frame 61 | * @return {Boolean} bool 62 | */ 63 | isNode(frame) { 64 | if (frame.isNative()) { 65 | return true; 66 | } 67 | const filename = frame.getFileName() || ''; 68 | return !path.isAbsolute(filename) && filename[0] !== '.'; 69 | } 70 | 71 | /** 72 | * check if the frame is app modules. 73 | * 74 | * @param {Object} frame - current frame 75 | * @return {Boolean} bool 76 | */ 77 | isApp(frame) { 78 | if (this.isNode(frame)) { 79 | return false; 80 | } 81 | const filename = frame.getFileName() || ''; 82 | return !filename.includes('node_modules' + path.sep); 83 | } 84 | 85 | /** 86 | * cache file asserts 87 | * 88 | * @param {String} key - assert key 89 | * @param {String} value - assert content 90 | */ 91 | setAssets(key, value) { 92 | this.assets.set(key, value); 93 | } 94 | 95 | /** 96 | * get cache file asserts 97 | * 98 | * @param {String} key - assert key 99 | */ 100 | getAssets(key) { 101 | this.assets.get(key); 102 | } 103 | 104 | /** 105 | * get frame source 106 | * 107 | * @param {Object} frame - current frame 108 | * @return {Object} frame source 109 | */ 110 | getFrameSource(frame) { 111 | const filename = frame.getFileName(); 112 | const lineNumber = frame.getLineNumber(); 113 | let contents = this.getAssets(filename); 114 | if (!contents) { 115 | contents = fs.existsSync(filename) ? fs.readFileSync(filename, 'utf8') : ''; 116 | this.setAssets(filename, contents); 117 | } 118 | const lines = contents.split(/\r?\n/); 119 | 120 | return { 121 | pre: lines.slice(Math.max(0, lineNumber - (this.codeContext + 1)), lineNumber - 1), 122 | line: lines[lineNumber - 1], 123 | post: lines.slice(lineNumber, lineNumber + this.codeContext), 124 | }; 125 | } 126 | 127 | /** 128 | * parse error and return frame stack 129 | * 130 | * @return {Array} frame 131 | */ 132 | parseError() { 133 | const stack = stackTrace.parse(this.error); 134 | return stack.map(frame => { 135 | if (!this.isNode(frame)) { 136 | frame.context = this.getFrameSource(frame); 137 | } 138 | return frame; 139 | }); 140 | } 141 | 142 | /** 143 | * get stack context 144 | * 145 | * @param {Object} frame - current frame 146 | * @return {Object} context 147 | */ 148 | getContext(frame) { 149 | if (!frame.context) { 150 | return {}; 151 | } 152 | 153 | return { 154 | start: frame.getLineNumber() - (frame.context.pre || []).length, 155 | pre: frame.context.pre.join('\n'), 156 | line: frame.context.line, 157 | post: frame.context.post.join('\n'), 158 | }; 159 | } 160 | 161 | /** 162 | * get frame classes, let view identify the frame 163 | * 164 | * @param {any} frame - current frame 165 | * @param {any} index - current index 166 | * @return {String} classes 167 | */ 168 | getFrameClasses(frame, index) { 169 | const classes = []; 170 | if (index === 0) { 171 | classes.push('active'); 172 | } 173 | 174 | if (!this.isApp(frame)) { 175 | classes.push('native-frame'); 176 | } 177 | 178 | return classes.join(' '); 179 | } 180 | 181 | /** 182 | * serialize frame and return meaningful data 183 | * 184 | * @param {Object} frame - current frame 185 | * @return {Object} frame result 186 | */ 187 | serializeFrame(frame) { 188 | const filename = frame.getFileName(); 189 | const relativeFileName = filename.includes(process.cwd()) 190 | ? filename.replace(process.cwd(), '').replace(startingSlashRegex, '') 191 | : filename; 192 | const extname = path.extname(filename).replace('.', ''); 193 | 194 | return { 195 | extname, 196 | file: relativeFileName, 197 | method: frame.getFunctionName(), 198 | line: frame.getLineNumber(), 199 | column: frame.getColumnNumber(), 200 | context: this.getContext(frame), 201 | }; 202 | } 203 | 204 | /** 205 | * serialize base data 206 | * 207 | * @param {Object} stack - frame stack 208 | * @param {Function} frameFomatter - frame fomatter function 209 | * @return {Object} data 210 | */ 211 | serializeData(stack, frameFomatter) { 212 | const code = this.error.code || this.error.type; 213 | let message = detectErrorMessage(this.ctx, this.error); 214 | if (code) { 215 | message = `${message} (code: ${code})`; 216 | } 217 | return { 218 | code, 219 | message, 220 | name: this.error.name, 221 | status: this.error.status, 222 | frames: stack instanceof Array ? stack.filter(frame => frame.getFileName()).map(frameFomatter) : [], 223 | }; 224 | } 225 | 226 | /** 227 | * serialize request object 228 | * 229 | * @return {Object} request object 230 | */ 231 | serializeRequest() { 232 | const headers = []; 233 | 234 | Object.keys(this.request.headers).forEach(key => { 235 | if (this._filterHeaders.includes(key)) { 236 | return; 237 | } 238 | headers.push({ 239 | key, 240 | value: this.request.headers[key], 241 | }); 242 | }); 243 | 244 | const parsedCookies = cookie.parse(this.request.headers.cookie || ''); 245 | const cookies = Object.keys(parsedCookies).map(key => { 246 | return { key, value: parsedCookies[key] }; 247 | }); 248 | 249 | return { 250 | url: this.request.url, 251 | httpVersion: this.request.httpVersion, 252 | method: this.request.method, 253 | connection: this.request.headers.connection, 254 | headers, 255 | cookies, 256 | }; 257 | } 258 | 259 | /** 260 | * serialize app info object 261 | * 262 | * @return {Object} egg app info 263 | */ 264 | serializeAppInfo() { 265 | let config = this.app.config; 266 | if (typeof this.app.dumpConfigToObject === 'function') { 267 | config = this.app.dumpConfigToObject().config.config; 268 | } 269 | return { 270 | baseDir: this.app.config.baseDir, 271 | config: util.inspect(config), 272 | }; 273 | } 274 | } 275 | 276 | module.exports = ErrorView; 277 | -------------------------------------------------------------------------------- /test/onerror.test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const assert = require('assert'); 4 | const mm = require('egg-mock'); 5 | 6 | describe('test/onerror.test.js', () => { 7 | let app; 8 | before(() => { 9 | mm.env('local'); 10 | mm.consoleLevel('NONE'); 11 | app = mm.app({ baseDir: 'onerror' }); 12 | return app.ready(); 13 | }); 14 | after(() => app.close()); 15 | 16 | afterEach(mm.restore); 17 | 18 | it('should handle error not in the req/res cycle with no ctx', async () => { 19 | mm.consoleLevel('NONE'); 20 | const app = mm.app({ 21 | baseDir: 'mock-test-error', 22 | }); 23 | await app.ready(); 24 | const err = new Error('mock test error'); 25 | app.emit('error', err, null); 26 | err.status = 400; 27 | app.emit('error', err, null); 28 | app.close(); 29 | }); 30 | 31 | it('should handle status:-1 as status:500', () => { 32 | return app.httpRequest() 33 | .get('/?status=-1') 34 | .expect(/

Error in /\?status=-1<\/h1>/) 35 | .expect(500); 36 | }); 37 | 38 | it('should handle status:undefined as status:500', () => { 39 | return app.httpRequest() 40 | .get('/') 41 | .expect(/
test error<\/div>/) 42 | .expect(500); 43 | }); 44 | 45 | it('should handle not exists file in stack without error', () => { 46 | return app.httpRequest() 47 | .get('/unknownFile') 48 | .expect(/
test error<\/div>/) 49 | .expect(500); 50 | }); 51 | 52 | it('should handle escape xss', () => { 53 | return app.httpRequest() 54 | .get('/?message=') 55 | .expect(/<script></script>/) 56 | .expect(500); 57 | }); 58 | 59 | it('should handle status:1 as status:500', () => { 60 | return app.httpRequest() 61 | .get('/?status=1') 62 | .expect(/
test error<\/div>/) 63 | .expect(500); 64 | }); 65 | 66 | it('should handle status:400', () => { 67 | return app.httpRequest() 68 | .get('/?status=400') 69 | .expect(/
test error<\/div>/) 70 | .expect(400); 71 | }); 72 | 73 | it('should return error json format when Accept is json', () => { 74 | return app.httpRequest() 75 | .get('/user') 76 | .set('Accept', 'application/json') 77 | .expect(res => { 78 | assert(res.body); 79 | assert(res.body.message === 'test error'); 80 | assert(res.body.stack.includes('Error: test error')); 81 | // should not includes error detail 82 | assert(!res.body.frames); 83 | }) 84 | .expect(500); 85 | }); 86 | 87 | it('should return error json format when request path match *.json', () => { 88 | return app.httpRequest() 89 | .get('/user.json') 90 | .expect(res => { 91 | assert(res.body); 92 | assert(res.body.message === 'test error'); 93 | assert(res.body.stack.includes('Error: test error')); 94 | assert(res.body.status === 500); 95 | assert(res.body.name === 'Error'); 96 | }) 97 | .expect(500); 98 | }); 99 | 100 | it('should support custom accpets return err.stack', () => { 101 | mm(app.config.onerror, 'accepts', ctx => { 102 | if (ctx.get('x-requested-with') === 'XMLHttpRequest') return 'json'; 103 | return 'html'; 104 | }); 105 | return app.httpRequest() 106 | .get('/user.json') 107 | .set('x-requested-with', 'XMLHttpRequest') 108 | .expect(/"message":"test error"/) 109 | .expect(/"stack":/) 110 | .expect(500); 111 | }); 112 | 113 | it('should return err.stack when unittest', () => { 114 | mm(app.config, 'env', 'unittest'); 115 | return app.httpRequest() 116 | .get('/user.json') 117 | .set('Accept', 'application/json') 118 | .expect(/"message":"test error"/) 119 | .expect(/"stack":/) 120 | .expect(500); 121 | }); 122 | 123 | it('should return err status message', () => { 124 | mm(app.config, 'env', 'prod'); 125 | return app.httpRequest() 126 | .get('/user.json') 127 | .set('Accept', 'application/json') 128 | .expect({ message: 'Internal Server Error' }) 129 | .expect(500); 130 | }); 131 | 132 | it('should return err.errors', () => { 133 | return app.httpRequest() 134 | .get('/user.json?status=400&errors=test') 135 | .set('Accept', 'application/json') 136 | .expect(/test/) 137 | .expect(400); 138 | }); 139 | 140 | it('should return err json at prod env', () => { 141 | mm(app.config, 'env', 'prod'); 142 | return app.httpRequest() 143 | .get('/user.json?status=400&errors=test') 144 | .set('Accept', 'application/json') 145 | .expect(/test/) 146 | .expect({ 147 | errors: 'test', 148 | message: 'test error', 149 | }) 150 | .expect('Content-Type', 'application/json; charset=utf-8') 151 | .expect(400); 152 | }); 153 | 154 | it('should return 4xx html at prod env', () => { 155 | mm(app.config, 'env', 'prod'); 156 | return app.httpRequest() 157 | .post('/test?status=400&errors=test') 158 | .set('Accept', 'text/html') 159 | .expect('

400 Bad Request

') 160 | .expect('Content-Type', 'text/html; charset=utf-8') 161 | .expect(400); 162 | }); 163 | 164 | it('should return 500 html at prod env', () => { 165 | mm(app.config, 'env', 'prod'); 166 | mm(app.config.onerror, 'errorPageUrl', ''); 167 | return app.httpRequest() 168 | .post('/test?status=502&errors=test') 169 | .set('Accept', 'text/html') 170 | .expect('

Internal Server Error, real status: 502

') 171 | .expect('Content-Type', 'text/html; charset=utf-8') 172 | .expect(500); 173 | }); 174 | 175 | it('should return err json at non prod env', () => { 176 | return app.httpRequest() 177 | .get('/user.json?status=400&errors=test') 178 | .set('Accept', 'application/json') 179 | .expect(/test/) 180 | .expect({ 181 | errors: 'test', 182 | message: 'test error', 183 | }) 184 | .expect(400); 185 | }); 186 | 187 | it('should return parsing json error on html response', () => { 188 | return app.httpRequest() 189 | .post('/test?status=400') 190 | .send({ test: 1 }) 191 | .set('Content-Type', 'application/json') 192 | .expect(/Problems parsing JSON/) 193 | .expect('Content-Type', 'text/html; charset=utf-8') 194 | .expect(400); 195 | }); 196 | 197 | it('should ignore secure config on html response', () => { 198 | return app.httpRequest() 199 | .post('/test?status=400') 200 | .send({ test: 1 }) 201 | .set('Content-Type', 'application/json') 202 | .expect(/keys: '<String len: 7/) 203 | .expect('Content-Type', 'text/html; charset=utf-8') 204 | .expect(400); 205 | }); 206 | 207 | it('should return parsing json error on json response', () => { 208 | return app.httpRequest() 209 | .post('/test?status=400') 210 | .send({ test: 1 }) 211 | .set('Content-Type', 'application/json') 212 | .set('Accept', 'application/json') 213 | .expect({ 214 | message: 'Problems parsing JSON', 215 | }) 216 | .expect('Content-Type', 'application/json; charset=utf-8') 217 | .expect(400); 218 | }); 219 | 220 | it('should redirect to error page', () => { 221 | mm(app.config, 'env', 'test'); 222 | return app.httpRequest() 223 | .get('/?status=500') 224 | .expect('Location', 'https://eggjs.com/500.html?real_status=500') 225 | .expect(302); 226 | }); 227 | 228 | it('should handle 403 err', () => { 229 | mm(app.config, 'env', 'prod'); 230 | return app.httpRequest() 231 | .get('/?status=403&code=3') 232 | .expect('

403 Forbidden

') 233 | .expect(403); 234 | }); 235 | 236 | it('should return jsonp style', () => { 237 | mm(app.config, 'env', 'prod'); 238 | return app.httpRequest() 239 | .get('/jsonp?callback=fn') 240 | .expect('content-type', 'application/javascript; charset=utf-8') 241 | .expect('/**/ typeof fn === \'function\' && fn({"message":"Internal Server Error"});') 242 | .expect(500); 243 | }); 244 | 245 | describe('customize', () => { 246 | let app; 247 | before(() => { 248 | mm.consoleLevel('NONE'); 249 | app = mm.app({ 250 | baseDir: 'onerror-customize', 251 | }); 252 | return app.ready(); 253 | }); 254 | after(() => app.close()); 255 | 256 | it('should support customize json style', () => { 257 | mm(app.config, 'env', 'prod'); 258 | return app.httpRequest() 259 | .get('/user.json') 260 | .expect('content-type', 'application/json; charset=utf-8') 261 | .expect({ msg: 'error' }) 262 | .expect(500); 263 | }); 264 | 265 | it('should return jsonp style', () => { 266 | mm(app.config, 'env', 'prod'); 267 | return app.httpRequest() 268 | .get('/jsonp?callback=fn') 269 | .expect('content-type', 'application/javascript; charset=utf-8') 270 | .expect('/**/ typeof fn === \'function\' && fn({"msg":"error"});') 271 | .expect(500); 272 | }); 273 | 274 | it('should handle html by default', () => { 275 | mm(app.config, 'env', 'test'); 276 | return app.httpRequest() 277 | .get('/?status=500') 278 | .expect('Location', 'https://eggjs.com/500.html?real_status=500') 279 | .expect(302); 280 | }); 281 | }); 282 | 283 | if (process.platform === 'linux') { 284 | // ignore Error: write ECONNRESET on windows and macos 285 | it('should log warn 4xx', async () => { 286 | fs.rmSync(path.join(__dirname, 'fixtrues/onerror-4xx/logs'), { force: true, recursive: true }); 287 | const app = mm.app({ 288 | baseDir: 'onerror-4xx', 289 | }); 290 | await app.ready(); 291 | await app.httpRequest() 292 | .post('/body_parser') 293 | .set('Content-Type', 'application/x-www-form-urlencoded') 294 | .send({ foo: new Buffer(1024 * 1000).fill(1).toString() }) 295 | .expect(/request entity too large/) 296 | .expect(413); 297 | await app.close(); 298 | 299 | const warnLog = path.join(__dirname, 'fixtures/onerror-4xx/logs/onerror-4xx/onerror-4xx-web.log'); 300 | const content = fs.readFileSync(warnLog, 'utf8'); 301 | assert.match(content, /POST \/body_parser] nodejs\..*?Error: request entity too large/); 302 | }); 303 | } 304 | 305 | describe('no errorpage', () => { 306 | let app; 307 | before(() => { 308 | mm.consoleLevel('NONE'); 309 | app = app = mm.app({ 310 | baseDir: 'onerror-no-errorpage', 311 | }); 312 | return app.ready(); 313 | }); 314 | after(() => app.close()); 315 | 316 | it('should display 500 Internal Server Error', () => { 317 | mm(app.config, 'env', 'prod'); 318 | return app.httpRequest() 319 | .get('/?status=500') 320 | .expect(500) 321 | .expect(/Internal Server Error, real status: 500/); 322 | }); 323 | }); 324 | 325 | describe('app.errorpage.url=/500', () => { 326 | let app; 327 | before(() => { 328 | mm.consoleLevel('NONE'); 329 | app = app = mm.app({ 330 | baseDir: 'onerror-custom-500', 331 | }); 332 | return app.ready(); 333 | }); 334 | after(() => app.close()); 335 | 336 | it('should redirect to error page', async () => { 337 | mm(app.config, 'env', 'prod'); 338 | 339 | await app.httpRequest() 340 | .get('/mockerror') 341 | .expect('Location', '/500?real_status=500') 342 | .expect(302); 343 | 344 | await app.httpRequest() 345 | .get('/mock4xx') 346 | .expect('

400 Bad Request

') 347 | .expect(400); 348 | 349 | await app.httpRequest() 350 | .get('/500') 351 | .expect('hi, this custom 500 page') 352 | .expect(500); 353 | 354 | await app.httpRequest() 355 | .get('/special') 356 | .expect('Location', '/specialerror?real_status=500') 357 | .expect(302); 358 | }); 359 | }); 360 | 361 | describe('onerror.ctx.error env=local', () => { 362 | let app; 363 | before(() => { 364 | mm.env('local'); 365 | mm.consoleLevel('NONE'); 366 | app = mm.app({ 367 | baseDir: 'onerror-ctx-error', 368 | }); 369 | return app.ready(); 370 | }); 371 | after(() => app.close()); 372 | 373 | it('should 500 full html', () => { 374 | return app.httpRequest() 375 | .get('/error') 376 | .expect(500) 377 | .expect(/you can`t get userId\./); 378 | }); 379 | }); 380 | 381 | describe('onerror.ctx.error env=unittest', () => { 382 | let app; 383 | before(() => { 384 | mm.consoleLevel('NONE'); 385 | app = mm.app({ 386 | baseDir: 'onerror-ctx-error', 387 | }); 388 | return app.ready(); 389 | }); 390 | after(() => app.close()); 391 | 392 | it('should 500 simple html', () => { 393 | return app.httpRequest() 394 | .get('/error') 395 | .expect(500) 396 | .expect(/you can`t get userId\./); 397 | }); 398 | }); 399 | 400 | describe('appErrorFilter', () => { 401 | let app; 402 | before(() => { 403 | mm.consoleLevel('NONE'); 404 | app = mm.app({ 405 | baseDir: 'custom-listener-onerror', 406 | }); 407 | return app.ready(); 408 | }); 409 | after(() => app.close()); 410 | 411 | it('should ignore error log', () => { 412 | mm(app.logger, 'log', () => { 413 | throw new Error('should not excute'); 414 | }); 415 | 416 | return app.httpRequest() 417 | .get('/?name=IgnoreError') 418 | .expect(500); 419 | }); 420 | 421 | it('should custom log error log', async () => { 422 | let lastMessage; 423 | mm(app.logger, 'error', msg => { 424 | lastMessage = msg; 425 | }); 426 | await app.httpRequest() 427 | .get('/?name=CustomError') 428 | .expect(500); 429 | assert(lastMessage === 'error happened'); 430 | }); 431 | 432 | it('should default log error', async () => { 433 | let lastError; 434 | mm(app.logger, 'log', (LEVEL, args) => { 435 | lastError = args[0]; 436 | }); 437 | 438 | await app.httpRequest() 439 | .get('/?name=OtherError') 440 | .expect(500); 441 | assert(lastError); 442 | assert(lastError.name === 'OtherError'); 443 | }); 444 | }); 445 | 446 | describe('agent emit error', () => { 447 | let app; 448 | before(() => { 449 | app = mm.cluster({ 450 | baseDir: 'agent-error', 451 | }); 452 | app.debug(); 453 | return app.ready(); 454 | }); 455 | after(() => app.close()); 456 | 457 | it('should log error', async () => { 458 | // console.log('app.stderr: %s', app.stderr); 459 | // console.log(app.stdout); 460 | await app.close(); 461 | assert.match(app.stderr, /TypeError/); 462 | }); 463 | }); 464 | 465 | describe('replace onerror default template', () => { 466 | 467 | let app = null; 468 | before(() => { 469 | mm.consoleLevel('NONE'); 470 | app = mm.app({ 471 | baseDir: 'onerror-custom-template', 472 | }); 473 | return app.ready(); 474 | }); 475 | 476 | after(() => app.close()); 477 | 478 | afterEach(mm.restore); 479 | 480 | it('should use custom template', () => { 481 | mm(app.config, 'env', 'local'); 482 | return app.httpRequest() 483 | .get('/') 484 | .expect(/custom template/) 485 | .expect(500); 486 | }); 487 | 488 | }); 489 | }); 490 | -------------------------------------------------------------------------------- /test/fixtures/onerror-custom-template/template.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | custom template 6 | 7 | 8 |
9 |
10 |

{{ status }}

11 |
12 |

{{ name }} in {{ request.url }}

13 |
{{ message }}
14 |
15 | 16 | 17 | 18 |
19 | 24 | 25 |
26 |
27 | 28 | 29 |
30 | 31 |
32 | {{#frames}} 33 | {{index}} 34 |
35 |
36 | {{ file }}:{{ line }}:{{ column }} 37 |
38 |
39 | {{ method }} 40 |
41 |
{{ context.pre }} 49 | {{ context.line }} 50 | {{ context.post }} 51 |
52 |
53 | {{/frames}} 54 |
55 |
56 |
57 |
58 | 59 |
60 |

Request Details

61 |
62 |
63 |
URI
64 |
{{ request.url }}
65 |
66 | 67 |
68 |
Request Method
69 |
{{ request.method }}
70 |
71 | 72 |
73 |
HTTP Version
74 |
{{ request.httpVersion }}
75 |
76 | 77 |
78 |
Connection
79 |
{{ request.connection }}
80 |
81 |
82 | 83 |

Headers

84 |
85 | {{#request.headers}} 86 |
87 |
{{ key }}
88 |
{{ value }}
89 |
90 | {{/request.headers}} 91 |
92 | 93 |

Cookies

94 |
95 | {{#request.cookies}} 96 |
97 |
{{ key }}
98 |
{{ value }}
99 |
100 | {{/request.cookies}} 101 |
102 |

AppInfo

103 |
104 |
105 |
baseDir
106 |
{{ appInfo.baseDir }}
107 |
108 |
109 |
config
110 |
111 |
{{ appInfo.config }}
112 |
113 |
114 | 115 |
116 | 117 | 128 | 187 |
188 | 189 | -------------------------------------------------------------------------------- /lib/onerror_page.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 249 | 250 | 348 | 349 | 571 | 572 | 573 |
574 |
575 |

{{ status }}

576 |
577 |

{{ name }} in {{ request.url }}

578 |
{{ message }}
579 |
580 | 581 | 582 | 583 |
584 | 589 | 590 |
591 |
592 | 593 | 594 |
595 | 596 |
597 | {{#frames}} 598 | {{index}} 599 |
600 |
601 | {{ file }}:{{ line }}:{{ column }} 602 |
603 |
604 | {{ method }} 605 |
606 |
{{ context.pre }} 614 | {{ context.line }} 615 | {{ context.post }} 616 |
617 |
618 | {{/frames}} 619 |
620 |
621 |
622 |
623 | 624 |
625 |

Request Details

626 |
627 |
628 |
URI
629 |
{{ request.url }}
630 |
631 | 632 |
633 |
Request Method
634 |
{{ request.method }}
635 |
636 | 637 |
638 |
HTTP Version
639 |
{{ request.httpVersion }}
640 |
641 | 642 |
643 |
Connection
644 |
{{ request.connection }}
645 |
646 |
647 | 648 |

Headers

649 |
650 | {{#request.headers}} 651 |
652 |
{{ key }}
653 |
{{ value }}
654 |
655 | {{/request.headers}} 656 |
657 | 658 |

Cookies

659 |
660 | {{#request.cookies}} 661 |
662 |
{{ key }}
663 |
{{ value }}
664 |
665 | {{/request.cookies}} 666 |
667 |

AppInfo

668 |
669 |
670 |
baseDir
671 |
{{ appInfo.baseDir }}
672 |
673 |
674 |
config
675 |
676 |
{{ appInfo.config }}
677 |
678 |
679 | 680 |
681 | 682 | 693 | 759 |
760 | 761 | 762 | --------------------------------------------------------------------------------