Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion plugins/onerror/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@
},
"dependencies": {
"cookie": "catalog:",
"koa-onerror": "catalog:",
"mustache": "catalog:",
"stack-trace": "catalog:"
},
Expand Down
2 changes: 1 addition & 1 deletion plugins/onerror/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import fs from 'node:fs';
import http from 'node:http';

import type { ILifecycleBoot, Application, Context } from 'egg';
import { onerror, type OnerrorOptions, type OnerrorError } from 'koa-onerror';

import type { OnerrorConfig } from './config/config.default.ts';
import { ErrorView } from './lib/error_view.ts';
import { onerror, type OnerrorOptions, type OnerrorError } from './lib/onerror.ts';
import { isProd, detectStatus, detectErrorMessage, accepts } from './lib/utils.ts';

export interface OnerrorErrorWithCode extends OnerrorError {
Expand Down
3 changes: 2 additions & 1 deletion plugins/onerror/src/config/config.default.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Context } from 'egg';
import type { OnerrorError, OnerrorOptions } from 'koa-onerror';

import type { OnerrorError, OnerrorOptions } from '../lib/onerror.ts';

export interface OnerrorConfig extends OnerrorOptions {
/**
Expand Down
2 changes: 1 addition & 1 deletion plugins/onerror/src/lib/error_view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import util from 'node:util';

import { parse } from 'cookie';
import type { Context } from 'egg';
import type { OnerrorError } from 'koa-onerror';
import Mustache from 'mustache';
import stackTrace, { type StackFrame } from 'stack-trace';

import type { OnerrorError } from './onerror.ts';
import { detectErrorMessage } from './utils.ts';

const startingSlashRegex = /\\|\//;
Expand Down
163 changes: 163 additions & 0 deletions plugins/onerror/src/lib/onerror.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import http from 'node:http';
import { debuglog, inspect } from 'node:util';

const debug = debuglog('egg-onerror');

export type OnerrorError = Error & {
status: number;
code?: string;
headers?: Record<string, string>;
expose?: boolean;
};

export type OnerrorHandler = (err: OnerrorError, ctx: any) => void;

export interface OnerrorOptions {
text?: OnerrorHandler;
json?: OnerrorHandler;
html?: OnerrorHandler;
all?: OnerrorHandler;
js?: OnerrorHandler;
redirect?: string | null;
accepts?: (...args: string[]) => string;
}

const defaultOptions: OnerrorOptions = {
text,
json,
html,
};

export function onerror(app: any, options?: OnerrorOptions): any {
options = { ...defaultOptions, ...options };

app.context.onerror = function (err: any) {
debug('onerror: %s', err);
if (err == null) return;

if (typeof this.req?.resume === 'function') {
this.req.resume();
debug('resume the req stream');
}

if (!(err instanceof Error)) {
debug('err is not an instance of Error');
let errMsg = err;
if (typeof err === 'object') {
try {
errMsg = JSON.stringify(err);
} catch (e) {
debug('stringify error: %s', e);
errMsg = inspect(err);
}
}
const newError = new Error('non-error thrown: ' + errMsg);
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) Reflect.set(newError, 'status', err.status);
if (err.headers) Reflect.set(newError, 'headers', err.headers);
}
err = newError;
debug('wrap err: %s', err);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const headerSent = this.headerSent || !this.writable;
if (headerSent) {
debug('headerSent is true');
err.headerSent = true;
}

this.app.emit('error', err, this);
if (headerSent) return;

if (err.code === 'ENOENT') {
err.status = 404;
}
if (typeof err.status !== 'number' || !http.STATUS_CODES[err.status]) {
err.status = 500;
}
this.status = err.status;

Comment thread
killagu marked this conversation as resolved.
clearResponseHeaders(this);
if (err.headers) {
this.set(err.headers);
}
let type: string;
if (options.accepts) {
type = options.accepts.call(this, 'html', 'text', 'json', 'js');
} else {
type = this.accepts('html', 'text', 'json', 'js');
}
debug('accepts type: %s', type);
type = type || 'text';
if (options.all) {
options.all.call(this, err, this);
} else if (options.redirect && type !== 'json') {
this.redirect(options.redirect);
} else {
const handler = getHandler(options, type);
handler?.call(this, err, this);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
this.type = type;
}

if (type === 'json' && typeof this.body !== 'string') {
this.body = JSON.stringify(this.body);
}
debug('end the response, body: %s', this.body);
this.res.end(this.body);
};

return app;
}

function getHandler(options: OnerrorOptions, type: string): OnerrorHandler | undefined {
if (type === 'html' || type === 'text' || type === 'json' || type === 'js') {
return options[type];
}
}

function isDev(): boolean {
return !process.env.NODE_ENV || process.env.NODE_ENV === 'development';
}

function text(err: OnerrorError, ctx: any): void {
ctx.body = (isDev() || err.expose) && err.message ? err.message : http.STATUS_CODES[ctx.status];
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

function json(err: OnerrorError, ctx: any): void {
const message = (isDev() || err.expose) && err.message ? err.message : http.STATUS_CODES[ctx.status];
ctx.body = { error: message };
}

function html(err: OnerrorError, ctx: any): void {
const message = (isDev() || err.expose) && err.message ? err.message : http.STATUS_CODES[ctx.status];
ctx.body = `<h2>${escapeHtml(String(err.status))} ${escapeHtml(String(message))}</h2>`;
ctx.type = 'html';
}

function escapeHtml(value: string): string {
return value.replace(/[&<>"']/g, (char) => {
switch (char) {
case '&':
return '&amp;';
case '<':
return '&lt;';
case '>':
return '&gt;';
case '"':
return '&quot;';
default:
return '&#39;';
}
});
}

function clearResponseHeaders(ctx: any): void {
const headers = ctx.response?.header ?? ctx.response?.headers ?? ctx.res.getHeaders?.() ?? {};
for (const name of Object.keys(headers)) {
if (name.toLowerCase() === 'set-cookie') continue;
ctx.res.removeHeader(name);
}
}
3 changes: 2 additions & 1 deletion plugins/onerror/src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Context, Application } from 'egg';
import type { OnerrorError } from 'koa-onerror';

import type { OnerrorError } from './onerror.ts';

export function detectErrorMessage(ctx: Context, err: OnerrorError): string {
// detect json parse error
Expand Down
21 changes: 19 additions & 2 deletions plugins/onerror/test/onerror.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { strict as assert } from 'node:assert';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { fileURLToPath, pathToFileURL } from 'node:url';

import { mm, type MockApplication } from '@eggjs/mock';
import { type Context } from 'egg';
Expand Down Expand Up @@ -42,7 +42,7 @@ describe('test/onerror.test.ts', () => {
app.emit('error', err, null);
(err as any).status = 400;
app.emit('error', err, null);
app.close();
await app.close();
});

it('should handle status:-1 as status:500', async () => {
Expand Down Expand Up @@ -585,4 +585,21 @@ describe('test/onerror.test.ts', () => {
.expect(500);
});
});

it('should not read koa-onerror package templates when importing the plugin app boot hook', async () => {
const originalReadFileSync = fs.readFileSync;
const blockedReads: string[] = [];
mm(fs, 'readFileSync', ((file: fs.PathOrFileDescriptor, ...args: any[]) => {
const filename = file instanceof URL ? file.href : String(file);
if (filename.includes('koa-onerror') && filename.includes('templates')) {
blockedReads.push(filename);
throw new Error(`unexpected koa-onerror template read: ${filename}`);
}
return originalReadFileSync.call(fs, file as any, ...args);
}) as typeof fs.readFileSync);

const appBootHookUrl = pathToFileURL(path.join(__dirname, '../src/app.ts')).href;
await import(/* @vite-ignore */ `${appBootHookUrl}?bundle-static-resource=${Date.now()}`);
assert.deepEqual(blockedReads, []);
});
});
Loading
Loading