未命名 1
// src/plugins/errorHandler.ts
// 只抛不return
import chalk from "chalk";
import { Elysia } from "elysia";
import { HttpError } from "elysia-http-problem-json";
import { createLogger } from "logixlysia";
// 导入统一的日志实例
import * as Errors from "./index";
// 格式化堆栈信息,突出显示函数名、文件路径和行列号
function formatStack(stack?: string): string[] {
if (!stack) return [];
return stack.split("\n").map((line) => {
// 匹配类似: at fnName (filePath:line:col)
const match = line.match(/at\s+(.+?)\s+((.+?)(?::(\d+):(\d+))?)/);
if (match) {
const [, fnName, filepath, lineNum, colNum] = match;
const formattedFn = fnName ? chalk.cyan(fnName) : "";
const formattedFile = chalk.bold(filepath);
const formattedLocation =
lineNum && colNum ? chalk.yellow(:${lineNum}:${colNum}) : "";
return at ${formattedFn} (${formattedFile}${formattedLocation});
}
// 简单的 at 行(例如原生错误)
if (line.trim().startsWith("at ")) {
return chalk.gray(line);
}
return line;
});
}
// 创建带标题的分隔线
function createSeparator(title: string, width = 80): string {
const padding = Math.max(0, width - title.length - 4);
const leftPad = Math.floor(padding / 2);
const rightPad = padding - leftPad;
return (
chalk.red("═".repeat(leftPad)) +
" " +
chalk.bold(title) +
" " +
chalk.red("═".repeat(rightPad))
);
}
export const errorPlugin = new Elysia()
.onError(({ code, error, path, request }) => {
const timestamp = new Date().toISOString();
const method = request?.method || "UNKNOWN";
const url = request?.url || path;
// ========<mark> 第一步:错误识别与转换 </mark>========
let processedError: any = error;
let errorSource: "database" | "http" | "unknown" = "unknown";
// ========<mark> 第二步:统一日志记录 (使用 unifiedLogger) </mark>========
// 记录原始错误信息,包含请求上下文和原始错误对象
log.error(
{
// pino 结构化数据
request: {
method: method,
url: url,
},
originalError: error,
errorCode: code,
errorSource: errorSource,
},
Request Error: ${processedError.status} - ${processedError.message} // 记录消息
);
// 1. 数据库错误 → 转为 HttpError
if (Errors.isDatabaseError(error)) {
errorSource = "database";
const dbError = error as {
code: string;
detail?: string;
message?: string;
};
processedError = Errors.mapDatabaseError(dbError);
if (process.env.NODE_ENV =<mark> "development") {
console.error(\n${createSeparator("🗄️ DATABASE ERROR DETECTED")});
console.error(
chalk.red(🚨 DB Error Code: ${chalk.yellow(dbError.code)})
);
console.error(
chalk.red(📝 DB Detail: ${chalk.white(dbError.detail || "N/A")})
);
console.error(
chalk.red(
💡 Converted to HTTP ${chalk.yellow(processedError.status)}: ${chalk.white(processedError.message)}
)
);
console.error(${chalk.red("═".repeat(80))}\n);
} else {
console.log(
[DBError ${dbError.code}] → [HTTP ${processedError.status}] ${path}: ${processedError.message}
);
}
}
// 2. 已是 HttpError
else if (
error &&
typeof error </mark>= "object" &&
"status" in error &&
"message" in error
) {
errorSource = "http";
}
// 3. 未知错误 → 包装为 500
else {
errorSource = "unknown";
processedError = new HttpError.InternalServerError(
(error as any)?.message || "服务器内部错误"
);
}
// ========<mark> 第二步:统一日志记录 </mark>======<mark>
if (process.env.NODE_ENV </mark>= "development") {
let separator: string;
switch (errorSource) {
case "database":
separator = createSeparator("🗄️ DATABASE → HTTP ERROR");
break;
case "http":
separator = createSeparator("🔥 HTTP ERROR");
break;
default:
separator = createSeparator("💥 UNKNOWN → HTTP ERROR");
}
console.error(\n${separator});
console.error(
chalk.red(🚨 Status Code: ${chalk.yellow(processedError.status)})
);
console.error(
chalk.red(💬 Message: ${chalk.white(processedError.message)})
);
console.error(chalk.red(📍 Path: ${chalk.cyan(path)}));
console.error(
chalk.red(🏷️ Source: ${chalk.cyan(errorSource.toUpperCase())})
);
if (errorSource === "unknown" && error instanceof Error) {
console.error(
chalk.red(
🔍 Original Error: ${chalk.white(${error.name}: ${error.message})}
)
);
}
if (processedError.stack) {
console.error(chalk.red("📚 Stack Trace:"));
formatStack(processedError.stack).forEach((line) =>
console.error(line)
);
}
console.error(${chalk.red("═".repeat(80))}\n);
} else {
// 生产环境简洁日志
console.error(
[${errorSource.toUpperCase()}][${processedError.status}] ${path}: ${processedError.message}
);
}
// ========<mark> 第三步:返回处理 </mark>======<mark>
if (errorSource </mark>= "database") {
throw processedError; // 重新抛出以触发 httpProblemJsonPlugin
}
// 其他情况由插件自动处理,无需返回
})
.as("global");