@eggjs/onerror 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +67 -0
- package/dist/commonjs/agent.d.ts +6 -0
- package/dist/commonjs/agent.js +16 -0
- package/dist/commonjs/app.d.ts +12 -0
- package/dist/commonjs/app.js +150 -0
- package/dist/commonjs/config/config.default.d.ts +27 -0
- package/dist/commonjs/config/config.default.js +15 -0
- package/dist/commonjs/index.d.ts +1 -0
- package/dist/commonjs/index.js +4 -0
- package/dist/commonjs/lib/error_view.d.ts +154 -0
- package/dist/commonjs/lib/error_view.js +248 -0
- package/dist/commonjs/lib/onerror_page.mustache.html +761 -0
- package/dist/commonjs/lib/utils.d.ts +10 -0
- package/dist/commonjs/lib/utils.js +53 -0
- package/dist/commonjs/package.json +3 -0
- package/dist/commonjs/types.d.ts +7 -0
- package/dist/commonjs/types.js +3 -0
- package/dist/esm/agent.d.ts +6 -0
- package/dist/esm/agent.js +13 -0
- package/dist/esm/app.d.ts +12 -0
- package/dist/esm/app.js +144 -0
- package/dist/esm/config/config.default.d.ts +27 -0
- package/dist/esm/config/config.default.js +10 -0
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/lib/error_view.d.ts +154 -0
- package/dist/esm/lib/error_view.js +241 -0
- package/dist/esm/lib/onerror_page.mustache.html +761 -0
- package/dist/esm/lib/utils.d.ts +10 -0
- package/dist/esm/lib/utils.js +43 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/types.d.ts +7 -0
- package/dist/esm/types.js +2 -0
- package/dist/package.json +4 -0
- package/package.json +93 -0
- package/src/agent.ts +12 -0
- package/src/app.ts +160 -0
- package/src/config/config.default.ts +34 -0
- package/src/index.ts +1 -0
- package/src/lib/error_view.ts +281 -0
- package/src/lib/onerror_page.mustache.html +761 -0
- package/src/lib/utils.ts +47 -0
- package/src/types.ts +12 -0
- package/src/typings/index.d.ts +4 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Context, EggCore } from '@eggjs/core';
|
|
2
|
+
import type { OnerrorError } from 'koa-onerror';
|
|
3
|
+
export declare function detectErrorMessage(ctx: Context, err: OnerrorError): string;
|
|
4
|
+
export declare function detectStatus(err: OnerrorError): number;
|
|
5
|
+
export declare function accepts(ctx: Context): "json" | "js" | "html";
|
|
6
|
+
export declare function isProd(app: EggCore): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Get the source directory name
|
|
9
|
+
*/
|
|
10
|
+
export declare function getSourceDirname(): string;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
export function detectErrorMessage(ctx, err) {
|
|
4
|
+
// detect json parse error
|
|
5
|
+
if (err.status === 400 &&
|
|
6
|
+
err.name === 'SyntaxError' &&
|
|
7
|
+
ctx.request.is('application/json', 'application/vnd.api+json', 'application/csp-report')) {
|
|
8
|
+
return 'Problems parsing JSON';
|
|
9
|
+
}
|
|
10
|
+
return err.message;
|
|
11
|
+
}
|
|
12
|
+
export function detectStatus(err) {
|
|
13
|
+
// detect status
|
|
14
|
+
let status = err.status || 500;
|
|
15
|
+
if (status < 200) {
|
|
16
|
+
// invalid status consider as 500, like urllib will return -1 status
|
|
17
|
+
status = 500;
|
|
18
|
+
}
|
|
19
|
+
return status;
|
|
20
|
+
}
|
|
21
|
+
export function accepts(ctx) {
|
|
22
|
+
if (ctx.acceptJSON)
|
|
23
|
+
return 'json';
|
|
24
|
+
if (ctx.acceptJSONP)
|
|
25
|
+
return 'js';
|
|
26
|
+
return 'html';
|
|
27
|
+
}
|
|
28
|
+
export function isProd(app) {
|
|
29
|
+
return app.config.env !== 'local' && app.config.env !== 'unittest';
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Get the source directory name
|
|
33
|
+
*/
|
|
34
|
+
export function getSourceDirname() {
|
|
35
|
+
if (typeof __dirname === 'string') {
|
|
36
|
+
return path.dirname(__dirname);
|
|
37
|
+
}
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
39
|
+
// @ts-ignore
|
|
40
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
41
|
+
return path.dirname(path.dirname(__filename));
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidXRpbHMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvbGliL3V0aWxzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sSUFBSSxNQUFNLFdBQVcsQ0FBQztBQUM3QixPQUFPLEVBQUUsYUFBYSxFQUFFLE1BQU0sVUFBVSxDQUFDO0FBSXpDLE1BQU0sVUFBVSxrQkFBa0IsQ0FBQyxHQUFZLEVBQUUsR0FBaUI7SUFDaEUsMEJBQTBCO0lBQzFCLElBQUksR0FBRyxDQUFDLE1BQU0sS0FBSyxHQUFHO1FBQ2xCLEdBQUcsQ0FBQyxJQUFJLEtBQUssYUFBYTtRQUMxQixHQUFHLENBQUMsT0FBTyxDQUFDLEVBQUUsQ0FBQyxrQkFBa0IsRUFBRSwwQkFBMEIsRUFBRSx3QkFBd0IsQ0FBQyxFQUFFLENBQUM7UUFDN0YsT0FBTyx1QkFBdUIsQ0FBQztJQUNqQyxDQUFDO0lBQ0QsT0FBTyxHQUFHLENBQUMsT0FBTyxDQUFDO0FBQ3JCLENBQUM7QUFFRCxNQUFNLFVBQVUsWUFBWSxDQUFDLEdBQWlCO0lBQzVDLGdCQUFnQjtJQUNoQixJQUFJLE1BQU0sR0FBRyxHQUFHLENBQUMsTUFBTSxJQUFJLEdBQUcsQ0FBQztJQUMvQixJQUFJLE1BQU0sR0FBRyxHQUFHLEVBQUUsQ0FBQztRQUNqQixvRUFBb0U7UUFDcEUsTUFBTSxHQUFHLEdBQUcsQ0FBQztJQUNmLENBQUM7SUFDRCxPQUFPLE1BQU0sQ0FBQztBQUNoQixDQUFDO0FBRUQsTUFBTSxVQUFVLE9BQU8sQ0FBQyxHQUFZO0lBQ2xDLElBQUksR0FBRyxDQUFDLFVBQVU7UUFBRSxPQUFPLE1BQU0sQ0FBQztJQUNsQyxJQUFJLEdBQUcsQ0FBQyxXQUFXO1FBQUUsT0FBTyxJQUFJLENBQUM7SUFDakMsT0FBTyxNQUFNLENBQUM7QUFDaEIsQ0FBQztBQUVELE1BQU0sVUFBVSxNQUFNLENBQUMsR0FBWTtJQUNqQyxPQUFPLEdBQUcsQ0FBQyxNQUFNLENBQUMsR0FBRyxLQUFLLE9BQU8sSUFBSSxHQUFHLENBQUMsTUFBTSxDQUFDLEdBQUcsS0FBSyxVQUFVLENBQUM7QUFDckUsQ0FBQztBQUVEOztHQUVHO0FBQ0gsTUFBTSxVQUFVLGdCQUFnQjtJQUM5QixJQUFJLE9BQU8sU0FBUyxLQUFLLFFBQVEsRUFBRSxDQUFDO1FBQ2xDLE9BQU8sSUFBSSxDQUFDLE9BQU8sQ0FBQyxTQUFTLENBQUMsQ0FBQztJQUNqQyxDQUFDO0lBQ0QsNkRBQTZEO0lBQzdELGFBQWE7SUFDYixNQUFNLFVBQVUsR0FBRyxhQUFhLENBQUMsTUFBTSxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUMsQ0FBQztJQUNsRCxPQUFPLElBQUksQ0FBQyxPQUFPLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxVQUFVLENBQUMsQ0FBQyxDQUFDO0FBQ2hELENBQUMifQ==
|
package/package.json
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@eggjs/onerror",
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"description": "error handler for egg",
|
|
8
|
+
"eggPlugin": {
|
|
9
|
+
"name": "onerror",
|
|
10
|
+
"optionalDependencies": [
|
|
11
|
+
"jsonp"
|
|
12
|
+
],
|
|
13
|
+
"exports": {
|
|
14
|
+
"import": "./dist/esm",
|
|
15
|
+
"require": "./dist/commonjs",
|
|
16
|
+
"typescript": "./src"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/eggjs/onerror.git"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"egg",
|
|
25
|
+
"egg-plugin",
|
|
26
|
+
"onerror"
|
|
27
|
+
],
|
|
28
|
+
"author": "dead_horse",
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">= 18.19.0"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@eggjs/core": "^6.3.1",
|
|
34
|
+
"cookie": "^1.0.2",
|
|
35
|
+
"koa-onerror": "^5.0.1",
|
|
36
|
+
"mustache": "^4.2.0",
|
|
37
|
+
"stack-trace": "^0.0.10"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@arethetypeswrong/cli": "^0.17.3",
|
|
41
|
+
"@eggjs/bin": "7",
|
|
42
|
+
"@eggjs/mock": "^6.0.5",
|
|
43
|
+
"@eggjs/tsconfig": "1",
|
|
44
|
+
"@types/mocha": "10",
|
|
45
|
+
"@types/mustache": "^4.2.5",
|
|
46
|
+
"@types/node": "22",
|
|
47
|
+
"@types/stack-trace": "^0.0.33",
|
|
48
|
+
"egg": "^4.0.4",
|
|
49
|
+
"eslint": "8",
|
|
50
|
+
"eslint-config-egg": "14",
|
|
51
|
+
"rimraf": "6",
|
|
52
|
+
"tshy": "3",
|
|
53
|
+
"tshy-after": "1",
|
|
54
|
+
"typescript": "5"
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"lint": "eslint --cache src test --ext .ts",
|
|
58
|
+
"pretest": "npm run clean && npm run lint -- --fix",
|
|
59
|
+
"test": "egg-bin test",
|
|
60
|
+
"preci": "npm run clean && npm run lint",
|
|
61
|
+
"ci": "egg-bin cov",
|
|
62
|
+
"postci": "npm run prepublishOnly && npm run clean",
|
|
63
|
+
"clean": "rimraf dist",
|
|
64
|
+
"prepublishOnly": "tshy && tshy-after && attw --pack"
|
|
65
|
+
},
|
|
66
|
+
"type": "module",
|
|
67
|
+
"tshy": {
|
|
68
|
+
"exports": {
|
|
69
|
+
".": "./src/index.ts",
|
|
70
|
+
"./package.json": "./package.json"
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"exports": {
|
|
74
|
+
".": {
|
|
75
|
+
"import": {
|
|
76
|
+
"types": "./dist/esm/index.d.ts",
|
|
77
|
+
"default": "./dist/esm/index.js"
|
|
78
|
+
},
|
|
79
|
+
"require": {
|
|
80
|
+
"types": "./dist/commonjs/index.d.ts",
|
|
81
|
+
"default": "./dist/commonjs/index.js"
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
"./package.json": "./package.json"
|
|
85
|
+
},
|
|
86
|
+
"files": [
|
|
87
|
+
"dist",
|
|
88
|
+
"src"
|
|
89
|
+
],
|
|
90
|
+
"types": "./dist/commonjs/index.d.ts",
|
|
91
|
+
"main": "./dist/commonjs/index.js",
|
|
92
|
+
"module": "./dist/esm/index.js"
|
|
93
|
+
}
|
package/src/agent.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ILifecycleBoot, EggCore } from '@eggjs/core';
|
|
2
|
+
|
|
3
|
+
export default class Boot implements ILifecycleBoot {
|
|
4
|
+
constructor(private agent: EggCore) {}
|
|
5
|
+
|
|
6
|
+
async didLoad() {
|
|
7
|
+
// should watch error event
|
|
8
|
+
this.agent.on('error', err => {
|
|
9
|
+
this.agent.coreLogger.error(err);
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
}
|
package/src/app.ts
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { onerror, type OnerrorOptions, type OnerrorError } from 'koa-onerror';
|
|
4
|
+
import type { ILifecycleBoot, EggCore, Context } from '@eggjs/core';
|
|
5
|
+
import { ErrorView } from './lib/error_view.js';
|
|
6
|
+
import { isProd, detectStatus, detectErrorMessage, accepts } from './lib/utils.js';
|
|
7
|
+
import type { OnerrorConfig } from './config/config.default.js';
|
|
8
|
+
|
|
9
|
+
export interface OnerrorErrorWithCode extends OnerrorError {
|
|
10
|
+
code?: string;
|
|
11
|
+
type?: string;
|
|
12
|
+
errors?: any[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default class Boot implements ILifecycleBoot {
|
|
16
|
+
constructor(private app: EggCore) {}
|
|
17
|
+
|
|
18
|
+
async didLoad() {
|
|
19
|
+
// logging error
|
|
20
|
+
const config = this.app.config.onerror;
|
|
21
|
+
const viewTemplate = fs.readFileSync(config.templatePath, 'utf8');
|
|
22
|
+
const app = this.app;
|
|
23
|
+
app.on('error', (err, ctx) => {
|
|
24
|
+
if (!ctx) {
|
|
25
|
+
ctx = app.currentContext || app.createAnonymousContext();
|
|
26
|
+
}
|
|
27
|
+
if (config.appErrorFilter && !config.appErrorFilter(err, ctx)) return;
|
|
28
|
+
|
|
29
|
+
const status = detectStatus(err);
|
|
30
|
+
// 5xx
|
|
31
|
+
if (status >= 500) {
|
|
32
|
+
try {
|
|
33
|
+
ctx.logger.error(err);
|
|
34
|
+
} catch (ex) {
|
|
35
|
+
app.logger.error(err);
|
|
36
|
+
app.logger.error(ex);
|
|
37
|
+
}
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 4xx
|
|
42
|
+
try {
|
|
43
|
+
ctx.logger.warn(err);
|
|
44
|
+
} catch (ex) {
|
|
45
|
+
app.logger.warn(err);
|
|
46
|
+
app.logger.error(ex);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const errorOptions: OnerrorOptions = {
|
|
51
|
+
// support customize accepts function
|
|
52
|
+
accepts(this: Context) {
|
|
53
|
+
const fn = config.accepts || accepts;
|
|
54
|
+
return fn(this as any);
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
html(err, ctx: Context) {
|
|
58
|
+
const status = detectStatus(err);
|
|
59
|
+
const errorPageUrl = typeof config.errorPageUrl === 'function'
|
|
60
|
+
? config.errorPageUrl(err, ctx)
|
|
61
|
+
: config.errorPageUrl;
|
|
62
|
+
|
|
63
|
+
// keep the real response status
|
|
64
|
+
ctx.realStatus = status;
|
|
65
|
+
// don't respond any error message in production env
|
|
66
|
+
if (isProd(app)) {
|
|
67
|
+
// 5xx
|
|
68
|
+
if (status >= 500) {
|
|
69
|
+
if (errorPageUrl) {
|
|
70
|
+
const statusQuery =
|
|
71
|
+
(errorPageUrl.indexOf('?') > 0 ? '&' : '?') +
|
|
72
|
+
`real_status=${status}`;
|
|
73
|
+
return ctx.redirect(errorPageUrl + statusQuery);
|
|
74
|
+
}
|
|
75
|
+
ctx.status = 500;
|
|
76
|
+
ctx.body = `<h2>Internal Server Error, real status: ${status}</h2>`;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
// 4xx
|
|
80
|
+
ctx.status = status;
|
|
81
|
+
ctx.body = `<h2>${status} ${http.STATUS_CODES[status]}</h2>`;
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
// show simple error format for unittest
|
|
85
|
+
if (app.config.env === 'unittest') {
|
|
86
|
+
ctx.status = status;
|
|
87
|
+
ctx.body = `${err.name}: ${err.message}\n${err.stack}`;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const errorView = new ErrorView(ctx, err, viewTemplate);
|
|
92
|
+
ctx.body = errorView.toHTML();
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
json(err: OnerrorErrorWithCode, ctx: Context) {
|
|
96
|
+
const status = detectStatus(err);
|
|
97
|
+
let errorJson: Record<string, any> = {};
|
|
98
|
+
|
|
99
|
+
ctx.status = status;
|
|
100
|
+
const code = err.code ?? err.type;
|
|
101
|
+
const message = detectErrorMessage(ctx, err);
|
|
102
|
+
|
|
103
|
+
if (isProd(app)) {
|
|
104
|
+
// 5xx server side error
|
|
105
|
+
if (status >= 500) {
|
|
106
|
+
errorJson = {
|
|
107
|
+
code,
|
|
108
|
+
// don't respond any error message in production env
|
|
109
|
+
message: http.STATUS_CODES[status],
|
|
110
|
+
};
|
|
111
|
+
} else {
|
|
112
|
+
// 4xx client side error
|
|
113
|
+
// addition `errors`
|
|
114
|
+
errorJson = {
|
|
115
|
+
code,
|
|
116
|
+
message,
|
|
117
|
+
errors: err.errors,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
errorJson = {
|
|
122
|
+
code,
|
|
123
|
+
message,
|
|
124
|
+
errors: err.errors,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
if (status >= 500) {
|
|
128
|
+
// provide detail error stack in local env
|
|
129
|
+
errorJson.stack = err.stack;
|
|
130
|
+
errorJson.name = err.name;
|
|
131
|
+
for (const key in err) {
|
|
132
|
+
if (!errorJson[key]) {
|
|
133
|
+
errorJson[key] = (err as any)[key];
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
ctx.body = errorJson;
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
js(err, ctx: Context) {
|
|
143
|
+
errorOptions.json!.call(ctx, err, ctx);
|
|
144
|
+
|
|
145
|
+
if (ctx.createJsonpBody) {
|
|
146
|
+
ctx.createJsonpBody(ctx.body);
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// support customize error response
|
|
152
|
+
const keys: (keyof OnerrorConfig)[] = [ 'all', 'html', 'json', 'text', 'js' ];
|
|
153
|
+
for (const type of keys) {
|
|
154
|
+
if (config[type]) {
|
|
155
|
+
Reflect.set(errorOptions, type, config[type]);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
onerror(app, errorOptions);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import type { Context } from '@eggjs/core';
|
|
3
|
+
import type { OnerrorError, OnerrorOptions } from 'koa-onerror';
|
|
4
|
+
import { getSourceDirname } from '../lib/utils.js';
|
|
5
|
+
|
|
6
|
+
export interface OnerrorConfig extends OnerrorOptions {
|
|
7
|
+
/**
|
|
8
|
+
* 5xx error will redirect to ${errorPageUrl}
|
|
9
|
+
* won't redirect in local env
|
|
10
|
+
*
|
|
11
|
+
* Default: `''`
|
|
12
|
+
*/
|
|
13
|
+
errorPageUrl: string | ((err: OnerrorError, ctx: Context) => string);
|
|
14
|
+
/**
|
|
15
|
+
* will execute `appErrorFilter` when emit an error in `app`
|
|
16
|
+
* If `appErrorFilter` return false, egg-onerror won't log this error.
|
|
17
|
+
* You can logging in `appErrorFilter` and return false to override the default error logging.
|
|
18
|
+
*
|
|
19
|
+
* Default: `undefined`
|
|
20
|
+
*/
|
|
21
|
+
appErrorFilter?: (err: OnerrorError, ctx: Context) => boolean;
|
|
22
|
+
/**
|
|
23
|
+
* default template path
|
|
24
|
+
*/
|
|
25
|
+
templatePath: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default {
|
|
29
|
+
onerror: {
|
|
30
|
+
errorPageUrl: '',
|
|
31
|
+
appErrorFilter: undefined,
|
|
32
|
+
templatePath: path.join(getSourceDirname(), 'lib/onerror_page.mustache.html'),
|
|
33
|
+
} as OnerrorConfig,
|
|
34
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import './types.js';
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
// modify from https://github.com/poppinss/youch/blob/develop/src/Youch/index.js
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import util from 'node:util';
|
|
6
|
+
import { parse } from 'cookie';
|
|
7
|
+
import Mustache from 'mustache';
|
|
8
|
+
import stackTrace, { type StackFrame } from 'stack-trace';
|
|
9
|
+
import { detectErrorMessage } from './utils.js';
|
|
10
|
+
import type { OnerrorError } from 'koa-onerror';
|
|
11
|
+
import type { Context } from '@eggjs/core';
|
|
12
|
+
|
|
13
|
+
const startingSlashRegex = /\\|\//;
|
|
14
|
+
|
|
15
|
+
export interface FrameSource {
|
|
16
|
+
pre: string[];
|
|
17
|
+
line: string;
|
|
18
|
+
post: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface Frame extends StackFrame {
|
|
22
|
+
context?: FrameSource;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class ErrorView {
|
|
26
|
+
ctx: Context;
|
|
27
|
+
error: OnerrorError;
|
|
28
|
+
request: Context['request'];
|
|
29
|
+
app: Context['app'];
|
|
30
|
+
assets: Map<string, string>;
|
|
31
|
+
viewTemplate: string;
|
|
32
|
+
|
|
33
|
+
codeContext = 5;
|
|
34
|
+
_filterHeaders = [ 'cookie', 'connection' ];
|
|
35
|
+
|
|
36
|
+
constructor(ctx: Context, error: OnerrorError, template: string) {
|
|
37
|
+
this.ctx = ctx;
|
|
38
|
+
this.error = error;
|
|
39
|
+
this.request = ctx.request;
|
|
40
|
+
this.app = ctx.app;
|
|
41
|
+
this.assets = new Map();
|
|
42
|
+
this.viewTemplate = template;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* get html error page
|
|
47
|
+
*
|
|
48
|
+
* @return {String} html page
|
|
49
|
+
*/
|
|
50
|
+
toHTML(): string {
|
|
51
|
+
const stack = this.parseError();
|
|
52
|
+
const data = this.serializeData(stack, (frame, index) => {
|
|
53
|
+
const serializedFrame = this.serializeFrame(frame);
|
|
54
|
+
serializedFrame.classes = this.getFrameClasses(frame, index);
|
|
55
|
+
return serializedFrame;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return this.compileView(this.viewTemplate, {
|
|
59
|
+
...data,
|
|
60
|
+
appInfo: this.serializeAppInfo(),
|
|
61
|
+
request: this.serializeRequest(),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* compile view
|
|
67
|
+
*
|
|
68
|
+
* @param {String} tpl - template
|
|
69
|
+
* @param {Object} locals - data used by template
|
|
70
|
+
*/
|
|
71
|
+
compileView(tpl: string, locals: Record<string, unknown>) {
|
|
72
|
+
return Mustache.render(tpl, locals);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* check if the frame is node native file.
|
|
77
|
+
*
|
|
78
|
+
* @param {Frame} frame - current frame
|
|
79
|
+
*/
|
|
80
|
+
isNode(frame: Frame) {
|
|
81
|
+
if (frame.isNative()) {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
const filename = frame.getFileName() || '';
|
|
85
|
+
return !path.isAbsolute(filename) && filename[0] !== '.';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* check if the frame is app modules.
|
|
90
|
+
*
|
|
91
|
+
* @param {Object} frame - current frame
|
|
92
|
+
*/
|
|
93
|
+
isApp(frame: Frame) {
|
|
94
|
+
if (this.isNode(frame)) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
const filename = frame.getFileName() || '';
|
|
98
|
+
return !filename.includes('node_modules' + path.sep);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* cache file asserts
|
|
103
|
+
*
|
|
104
|
+
* @param {String} key - assert key
|
|
105
|
+
* @param {String} value - assert content
|
|
106
|
+
*/
|
|
107
|
+
setAssets(key: string, value: string) {
|
|
108
|
+
this.assets.set(key, value);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* get cache file asserts
|
|
113
|
+
*
|
|
114
|
+
* @param {String} key - assert key
|
|
115
|
+
*/
|
|
116
|
+
getAssets(key: string) {
|
|
117
|
+
return this.assets.get(key);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* get frame source
|
|
122
|
+
*
|
|
123
|
+
* @param {Object} frame - current frame
|
|
124
|
+
*/
|
|
125
|
+
getFrameSource(frame: StackFrame): FrameSource {
|
|
126
|
+
const filename = frame.getFileName();
|
|
127
|
+
const lineNumber = frame.getLineNumber();
|
|
128
|
+
let contents = this.getAssets(filename);
|
|
129
|
+
if (!contents) {
|
|
130
|
+
contents = fs.existsSync(filename) ? fs.readFileSync(filename, 'utf8') : '';
|
|
131
|
+
this.setAssets(filename, contents);
|
|
132
|
+
}
|
|
133
|
+
const lines = contents.split(/\r?\n/);
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
pre: lines.slice(Math.max(0, lineNumber - (this.codeContext + 1)), lineNumber - 1),
|
|
137
|
+
line: lines[lineNumber - 1],
|
|
138
|
+
post: lines.slice(lineNumber, lineNumber + this.codeContext),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* parse error and return frame stack
|
|
144
|
+
*/
|
|
145
|
+
parseError() {
|
|
146
|
+
const stack = stackTrace.parse(this.error);
|
|
147
|
+
return stack.map((frame: Frame) => {
|
|
148
|
+
if (!this.isNode(frame)) {
|
|
149
|
+
frame.context = this.getFrameSource(frame);
|
|
150
|
+
}
|
|
151
|
+
return frame;
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* get stack context
|
|
157
|
+
*
|
|
158
|
+
* @param {Object} frame - current frame
|
|
159
|
+
*/
|
|
160
|
+
getContext(frame: Frame) {
|
|
161
|
+
if (!frame.context) {
|
|
162
|
+
return {};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
start: frame.getLineNumber() - (frame.context.pre || []).length,
|
|
167
|
+
pre: frame.context.pre.join('\n'),
|
|
168
|
+
line: frame.context.line,
|
|
169
|
+
post: frame.context.post.join('\n'),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* get frame classes, let view identify the frame
|
|
175
|
+
*
|
|
176
|
+
* @param {any} frame - current frame
|
|
177
|
+
* @param {any} index - current index
|
|
178
|
+
*/
|
|
179
|
+
getFrameClasses(frame: Frame, index: number) {
|
|
180
|
+
const classes: string[] = [];
|
|
181
|
+
if (index === 0) {
|
|
182
|
+
classes.push('active');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!this.isApp(frame)) {
|
|
186
|
+
classes.push('native-frame');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return classes.join(' ');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* serialize frame and return meaningful data
|
|
194
|
+
*
|
|
195
|
+
* @param {Object} frame - current frame
|
|
196
|
+
*/
|
|
197
|
+
serializeFrame(frame: Frame) {
|
|
198
|
+
const filename = frame.getFileName();
|
|
199
|
+
const relativeFileName = filename.includes(process.cwd())
|
|
200
|
+
? filename.replace(process.cwd(), '').replace(startingSlashRegex, '')
|
|
201
|
+
: filename;
|
|
202
|
+
const extname = path.extname(filename).replace('.', '');
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
extname,
|
|
206
|
+
file: relativeFileName,
|
|
207
|
+
method: frame.getFunctionName(),
|
|
208
|
+
line: frame.getLineNumber(),
|
|
209
|
+
column: frame.getColumnNumber(),
|
|
210
|
+
context: this.getContext(frame),
|
|
211
|
+
classes: '',
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* serialize base data
|
|
217
|
+
*
|
|
218
|
+
* @param {Object} stack - frame stack
|
|
219
|
+
* @param {Function} frameFormatter - frame formatter function
|
|
220
|
+
*/
|
|
221
|
+
serializeData(stack: Frame[], frameFormatter: (frame: Frame, index: number) => any) {
|
|
222
|
+
const code = Reflect.get(this.error, 'code') ?? Reflect.get(this.error, 'type');
|
|
223
|
+
let message = detectErrorMessage(this.ctx, this.error);
|
|
224
|
+
if (code) {
|
|
225
|
+
message = `${message} (code: ${code})`;
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
code,
|
|
229
|
+
message,
|
|
230
|
+
name: this.error.name,
|
|
231
|
+
status: this.error.status,
|
|
232
|
+
frames: stack instanceof Array ? stack.filter(frame => frame.getFileName()).map(frameFormatter) : [],
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* serialize request object
|
|
238
|
+
*/
|
|
239
|
+
serializeRequest() {
|
|
240
|
+
const headers: { key: string; value: string | string[] | undefined }[] = [];
|
|
241
|
+
|
|
242
|
+
Object.keys(this.request.headers).forEach(key => {
|
|
243
|
+
if (this._filterHeaders.includes(key)) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
headers.push({
|
|
247
|
+
key,
|
|
248
|
+
value: this.request.headers[key],
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const parsedCookies = parse(this.request.headers.cookie || '');
|
|
253
|
+
const cookies = Object.keys(parsedCookies).map(key => {
|
|
254
|
+
return { key, value: parsedCookies[key] };
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
url: this.request.url,
|
|
259
|
+
httpVersion: this.request.req.httpVersion,
|
|
260
|
+
method: this.request.method,
|
|
261
|
+
connection: this.request.headers.connection,
|
|
262
|
+
headers,
|
|
263
|
+
cookies,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* serialize app info object
|
|
269
|
+
*/
|
|
270
|
+
serializeAppInfo() {
|
|
271
|
+
let config = this.app.config;
|
|
272
|
+
if ('dumpConfigToObject' in this.app && typeof this.app.dumpConfigToObject === 'function') {
|
|
273
|
+
config = this.app.dumpConfigToObject().config.config;
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
baseDir: this.app.config.baseDir as string,
|
|
277
|
+
config: util.inspect(config),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|