返回首页

未命名 1

分类:现代化开发
发布于:
阅读时间:28 分钟

// 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");