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
3 changes: 2 additions & 1 deletion packages/cluster/test/app_worker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ describe.skipIf(process.version.startsWith('v24') || process.platform === 'win32
app
// .debug()
.expect('code', 1)
.expect('stdout', /\[app_worker] beforeExit success/)
.expect('stderr', /Error: mock error/)
.expect('stderr', /app_worker#1:\d+ start fail/)
.end()
);
});
Expand Down
32 changes: 20 additions & 12 deletions packages/egg/test/cluster1/app_worker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ describe('test/cluster1/app_worker.test.ts', () => {
});

it('should response 400 bad request when HTTP request packet broken', async () => {
// Node.js will emit a clientError when the raw URI in the HTTP request
// packet contains spaces. Send raw packets because modern clients reject
// unescaped paths before they reach the server.
const responses = await Promise.all([rawRequest(app.port, '/foo bar'), rawRequest(app.port, '/foo baz')]);

for (const response of responses) {
Expand Down Expand Up @@ -139,9 +142,11 @@ function connect(port: number) {

function rawRequest(port: number, path: string) {
return new Promise<string>((resolve, reject) => {
const socket = net.createConnection(port, '127.0.0.1');
let response = '';
let settled = false;
const socket = net.createConnection(port, '127.0.0.1', () => {
socket.write(`GET ${path} HTTP/1.1\r\nHost: 127.0.0.1:${port}\r\nConnection: close\r\n\r\n`);
});

function resolveOnce() {
if (!settled) {
Expand All @@ -150,24 +155,27 @@ function rawRequest(port: number, path: string) {
}
}

function rejectOnce(err: Error) {
if (!settled) {
settled = true;
reject(err);
}
}

socket.setEncoding('utf8');
socket.on('connect', () => {
socket.write(`GET ${path} HTTP/1.1\r\nHost: 127.0.0.1:${port}\r\nConnection: close\r\n\r\n`);
});
socket.setTimeout(5000);
socket.on('data', (chunk) => {
response += chunk;
});
socket.on('timeout', () => {
socket.destroy(new Error('Timed out waiting for raw HTTP response'));
});
socket.on('end', resolveOnce);
socket.on('close', (hasError) => {
if (!hasError) {
socket.on('error', rejectOnce);
socket.on('close', (hadError) => {
if (!hadError) {
resolveOnce();
}
});
socket.on('error', (err) => {
if (!settled) {
settled = true;
reject(err);
}
});
});
}
2 changes: 2 additions & 0 deletions plugins/onerror/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"./app": "./src/app.ts",
"./config/config.default": "./src/config/config.default.ts",
"./lib/error_view": "./src/lib/error_view.ts",
"./lib/onerror_page": "./src/lib/onerror_page.ts",
"./lib/utils": "./src/lib/utils.ts",
"./types": "./src/types.ts",
"./package.json": "./package.json"
Expand All @@ -40,6 +41,7 @@
"./app": "./dist/app.js",
"./config/config.default": "./dist/config/config.default.js",
"./lib/error_view": "./dist/lib/error_view.js",
"./lib/onerror_page": "./dist/lib/onerror_page.js",
"./lib/utils": "./dist/lib/utils.js",
"./types": "./dist/types.js",
"./package.json": "./package.json"
Expand Down
4 changes: 3 additions & 1 deletion plugins/onerror/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ export default class Boot implements ILifecycleBoot {
async didLoad(): Promise<void> {
// logging error
const config = this.app.config.onerror;
const viewTemplate = fs.readFileSync(config.templatePath, 'utf8');
const viewTemplate = config.templatePath
? fs.readFileSync(config.templatePath, 'utf8')
: (await import('./lib/onerror_page.ts')).ONERROR_PAGE_TEMPLATE;
const app = this.app;
app.on('error', (err, ctx) => {
if (!ctx) {
Expand Down
8 changes: 4 additions & 4 deletions plugins/onerror/src/config/config.default.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import path from 'node:path';

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

Expand All @@ -20,7 +18,9 @@ export interface OnerrorConfig extends OnerrorOptions {
*/
appErrorFilter?: (err: OnerrorError, ctx: Context) => boolean;
/**
* default template path
* Custom template path. If empty, uses the built-in error page template.
*
* Default: `''`
*/
templatePath: string;
}
Expand All @@ -29,6 +29,6 @@ export default {
onerror: {
errorPageUrl: '',
appErrorFilter: undefined,
templatePath: path.join(import.meta.dirname, '../lib/onerror_page.mustache.html'),
Comment thread
killagu marked this conversation as resolved.
templatePath: '',
} as OnerrorConfig,
};
76 changes: 72 additions & 4 deletions plugins/onerror/src/lib/error_view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,18 @@ import stackTrace, { type StackFrame } from 'stack-trace';
import { detectErrorMessage } from './utils.ts';

const startingSlashRegex = /\\|\//;
const defaultConfigIgnoreList: (string | RegExp)[] = [
'pass',
'pwd',
'passd',
'passwd',
'password',
'keys',
'masterKey',
'accessKey',
/secret/i,
];
const redactedValue = '<Redacted>';

export interface FrameSource {
pre: string[];
Expand Down Expand Up @@ -302,13 +314,69 @@ export class ErrorView {
baseDir: string;
config: string;
} {
let config = this.app.config;
if ('dumpConfigToObject' in this.app && typeof this.app.dumpConfigToObject === 'function') {
config = this.app.dumpConfigToObject().config.config;
}
const config = this.serializeConfig();
return {
baseDir: this.app.config.baseDir as string,
config: util.inspect(config) satisfies string as string,
};
}

serializeConfig(): unknown {
if ('dumpConfigToObject' in this.app && typeof this.app.dumpConfigToObject === 'function') {
return this.app.dumpConfigToObject().config.config;
}

return this.redactConfig(this.app.config, this.getConfigIgnoreList());
}
Comment thread
killagu marked this conversation as resolved.

getConfigIgnoreList(): (string | RegExp)[] {
try {
return Array.from(this.app.config.dump.ignore);
} catch {
return defaultConfigIgnoreList;
}
}

redactConfig(
value: unknown,
ignoreList: (string | RegExp)[],
ancestors: WeakSet<object> = new WeakSet<object>(),
): unknown {
if (!value || typeof value !== 'object') {
return value;
}

if (value instanceof Date || value instanceof RegExp || value instanceof URL) {
return value.toString();
}

if (Buffer.isBuffer(value)) {
Comment on lines +349 to +353
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the fallback redaction path, Set/Map values (e.g. Egg’s default config.dump.ignore is a Set) will currently fall through to the generic object handling and end up serialized as {} because Object.keys(new Set()) is empty. Consider adding explicit handling for Set/Map (e.g. convert to arrays/entries and recursively redact) before the generic object branch so AppInfo stays informative.

Copilot uses AI. Check for mistakes.
return value;
}

if (ancestors.has(value)) {
return '[Circular]';
}
ancestors.add(value);

try {
if (Array.isArray(value)) {
return value.map((item) => this.redactConfig(item, ignoreList, ancestors));
}

const result: Record<string, unknown> = {};
for (const key of Object.keys(value)) {
result[key] = this.shouldRedactConfigKey(key, ignoreList)
? redactedValue
: this.redactConfig((value as Record<string, unknown>)[key], ignoreList, ancestors);
}
return result;
} finally {
ancestors.delete(value);
}
}

shouldRedactConfigKey(key: string, ignoreList: (string | RegExp)[]): boolean {
return ignoreList.some((item) => (typeof item === 'string' ? item === key : item.test(key)));
}
}
Loading
Loading