
后端前端金融科技数据可视化【免费下载链接】ghostfolioOpen Source Wealth Management Software. Angular NestJS Prisma Nx TypeScript 项目地址https://gitcode.com/GitHub_Trending/gh/ghostfolio点击查看免费下载导读本文围绕 Ghostfolio 仓库内.agents/skills/nestjs-best-practices/rules/error-use-exception-filters.md这条高阶错误处理规则展开系统讲解如何在 NestJS 应用中用 Exception Filters异常过滤器取代控制器里的手写 try/catch 响应实现全应用一致的错误处理。文章先讲清为什么不该在控制器里手动拼 JSON再给出内置异常、自定义领域异常、局部过滤器、全局过滤器的完整写法最后以 Ghostfolio 真实代码组合快照计算过滤器、MCP 工具过滤器、CallerFacingError 体系印证这套模式在生产项目中的落地方式。读完你将掌握一套可直接复制的异常处理骨架并理解 Ghostfolio 如何在 REST 与 MCP 两条链路上分别收敛错误。一、反模式控制器里手写错误响应许多 NestJS 项目最初会把错误处理写成在每个控制器里捕获、再手动构造 JSON。下面这种写法虽然能工作却是规则明确反对的典型反模式// Manual error handling in controllers Controller(users) export class UsersController { Get(:id) async findOne(Param(id) id: string, Res() res: Response) { try { const user await this.usersService.findById(id); if (!user) { return res.status(404).json({ statusCode: 404, message: User not found, }); } return res.json(user); } catch (error) { console.error(error); return res.status(500).json({ statusCode: 500, message: Internal server error, }); } } }它的弊端显而易见响应格式不统一每个控制器各写各的{ statusCode, message }字段名、错误码、时间戳都可能不一致控制器职责膨胀业务逻辑里混入 HTTP 状态码决策与响应序列化控制器无法保持薄重复代码爆炸404/500 的处理逻辑在几十个控制器里复制粘贴改一处格式就要全局搜索替换遗漏风险并非所有路径都被 try/catch 覆盖异步错误、管道校验错误、守卫抛出的错误都可能绕过手工处理直接落到框架默认行为上。规则给出的结论是永远不要在控制器中 catch 异常并手工格式化错误响应而应使用 NestJS 的 Exception Filters 统一处理。二、正确做法抛异常让过滤器接管NestJS 本身自带异常层只要抛出HttpException或其子类框架默认就会把它转换为 JSON 响应。因此控制器只需要抛不需要接// Use built-in and custom exceptions Controller(users) export class UsersController { Get(:id) async findOne(Param(id) id: string): PromiseUser { const user await this.usersService.findById(id); if (!user) { throw new NotFoundException(User #${id} not found); } return user; } }与之配套的规则.agents/skills/nestjs-best-practices/rules/error-throw-http-exceptions.md进一步强调在 HTTP 应用中服务层直接抛HttpException子类是被允许且更优的——它让控制器保持薄、让服务层直接表达错误状态而对于需要跨层复用的服务则应定义领域异常domain exception再在过滤器里映射到 HTTP 状态码。这正是抛异常 过滤器映射两条腿走路的核心思想。自定义领域异常给错误附上业务语义内置的NotFoundException只能表达没找到但业务上往往需要携带错误码code、上下文等结构化信息。为此可以派生领域异常// Custom domain exception export class UserNotFoundException extends NotFoundException { constructor(userId: string) { super({ statusCode: 404, error: Not Found, message: User with ID ${userId} not found, code: USER_NOT_FOUND, }); } }这样调用方只需要throw new UserNotFoundException(id)状态码、错误码、人类可读信息都在一处定义语义集中、可复用、可测试。三、自定义 Exception Filter接管领域异常的序列化Catch(DomainException)可以把特定异常类型从框架默认行为中接管过来让你完全控制响应体结构加入timestamp、path等字段// Custom exception filter for domain errors Catch(DomainException) export class DomainExceptionFilter implements ExceptionFilter { catch(exception: DomainException, host: ArgumentsHost) { const ctx host.switchToHttp(); const response ctx.getResponseResponse(); const request ctx.getRequestRequest(); const status exception.getStatus?.() || 400; response.status(status).json({ statusCode: status, code: exception.code, message: exception.message, timestamp: new Date().toISOString(), path: request.url, }); } }要点过滤器必须实现ExceptionFilter接口即catch(exception, host)方法通过host.switchToHttp()拿到 Express 的Request/Response从而写入状态码、响应体并读取当前请求路径响应体中同时携带timestamp与path便于调用方与日志侧对齐排查业务错误码code让客户端可以做程序化处理而不是解析人类语言消息。四、全局过滤器兜底一切未处理异常仅有针对领域异常的过滤器还不够——未预期的Error、数据库约束冲突、未知异常仍会穿透。此时需要一个Catch()的全量兜底过滤器// Global exception filter for unhandled errors Catch() export class AllExceptionsFilter implements ExceptionFilter { constructor(private readonly logger: Logger) {} catch(exception: unknown, host: ArgumentsHost) { const ctx host.switchToHttp(); const response ctx.getResponseResponse(); const request ctx.getRequestRequest(); const status exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR; const message exception instanceof HttpException ? exception.message : Internal server error; this.logger.error( ${request.method} ${request.url}, exception instanceof Error ? exception.stack : exception, ); response.status(status).json({ statusCode: status, message, timestamp: new Date().toISOString(), path: request.url, }); } }这个兜底过滤器做了三件事分类HttpException取其状态码其他一切异常统一映射为500 Internal Server Error记录把METHOD URL与堆栈写入 Logger保证线上可观测收敛对外只暴露statusCode / message / timestamp / path不把堆栈、内部字段名泄露给调用方。注册方式一useGlobalFiltersmain.ts// Register globally in main.ts app.useGlobalFilters( new AllExceptionsFilter(app.get(Logger)), new DomainExceptionFilter(), );注意useGlobalFilters在NestFactory.create()之后调用即可生效适用于在引导阶段一次性挂载所有全局过滤器。注册方式二APP_FILTER 依赖注入模块级更符合 NestJS 依赖注入习惯、且能被测试框架感知的是APP_FILTER令牌// Or via module Module({ providers: [ { provide: APP_FILTER, useClass: AllExceptionsFilter, }, ], }) export class AppModule {}APP_FILTER注册的过滤器会作为全局过滤器挂载同时仍可注入Logger等依赖是生产项目中最常用的方式。规则还提醒对特定控制器/模块只起作用的异常用UseFilters(...)在控制器或方法上局部挂载即可避免全局过滤器过度集中。五、Ghostfolio 源码印证过滤器如何落地到生产仓库Ghostfolio 的apps/apiNestJS把这条规则落成了两套真实实现分别覆盖 REST 与 MCP 两条错误链路是理解领域异常 局部过滤器 全局过滤器组合拳的最佳案例。5.1 领域异常定义分层清晰的错误类型仓库在apps/api/src/errors/caller-facing.error.ts定义了面向调用方的基础错误/** * An error whose message is written for the caller. A filter passes such a * message on, while it hides the message of every other error, because that * message can carry internals of the application. */ export class CallerFacingError extends Error { public constructor(message: string) { super(message); this.name CallerFacingError; } }而 apps/api/src/app/import/errors/import-validation.error.ts 继承它表达导入校验失败apps/api/src/app/portfolio/errors/portfolio-snapshot-computation.error.ts 则是一个独立的纯领域异常用于表达组合快照多次尝试仍无法计算。这套命名约定本身就是过滤器策略的一部分CallerFacingError的消息写给调用方看可以原样透传而其他异常的消息可能携带 DTO 字段名、数据库约束名等内部信息必须被隐藏。5.2 全局过滤器PortfolioSnapshotComputationExceptionFilter组合快照计算是 Ghostfolio 的 CPU 密集核心路径apps/api/src/app/portfolio/calculator/portfolio-calculator.ts 在超过MAX_INITIALIZATION_ATTEMPTS次尝试后抛出PortfolioSnapshotComputationError。针对它仓库实现了 apps/api/src/filters/portfolio-snapshot-computation-exception.filter.tsCatch(PortfolioSnapshotComputationError) export class PortfolioSnapshotComputationExceptionFilter implements ExceptionFilter { private readonly logger new Logger( PortfolioSnapshotComputationExceptionFilter.name ); public catch( exception: PortfolioSnapshotComputationError, host: ArgumentsHost ) { this.logger.error(exception.message); const response host.switchToHttp().getResponseResponse(); response.status(StatusCodes.SERVICE_UNAVAILABLE).json({ message: getReasonPhrase(StatusCodes.SERVICE_UNAVAILABLE), statusCode: StatusCodes.SERVICE_UNAVAILABLE }); } }实现要点用Catch(PortfolioSnapshotComputationError)精确绑定领域异常其他异常不受影响语义映射计算失败并非 500 而是503 Service Unavailable上游数据未就绪导致的暂时性失败并通过http-status-codes的getReasonPhrase生成标准原因短语而不是暴露内部 message通过APP_FILTER注册为全局过滤器见 apps/api/src/app/app.module.tsproviders: [ I18nService, { provide: APP_FILTER, useClass: PortfolioSnapshotComputationExceptionFilter }, { provide: APP_GUARD, useClass: ImpersonationWriteGuard } ]5.3 局部过滤器McpToolExceptionFilterGhostfolio 通过rekog/mcp-nest暴露 Model Context ProtocolMCP工具端点控制器 apps/api/src/app/endpoints/mcp/mcp.controller.ts 用UseFilters(McpToolExceptionFilter)局部挂载过滤器McpController() UseFilters(McpToolExceptionFilter) export class GhostfolioMcpController { public constructor(private readonly mcpService: McpService) {} }过滤器本身实现了RpcExceptionFilter而非 HTTP 的ExceptionFilter返回Observablenever把异常转换成 MCP 协议规定的{ message, status: error }结构见 apps/api/src/filters/mcp-tool-exception.filter.tsCatch() export class McpToolExceptionFilter implements RpcExceptionFilter { private readonly logger new Logger(McpToolExceptionFilter.name); public catch(exception: unknown): Observablenever { // The message of this exception is written for the caller, hence it is // passed on and is not written to the log if (exception instanceof CallerFacingError) { return throwError(() { return { message: exception.message, status: error }; }); } const statusCode this.getStatus(exception); // An exception which the caller causes, for example a refused call, is // expected, hence only an exception of the application is written to the // log if (statusCode StatusCodes.INTERNAL_SERVER_ERROR) { this.logger.error(exception); } // The message of an exception can carry internals, for example the // property names of a data transfer object of a failed validation, hence // the reason phrase of the status is passed on instead return throwError(() { return { message: this.getReasonPhraseOfStatus(statusCode), status: error }; }); } private getReasonPhraseOfStatus(statusCode: number) { try { return getReasonPhrase(statusCode); } catch { return getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR); } } private getStatus(exception: unknown) { if (exception instanceof PortfolioSnapshotComputationError) { return StatusCodes.SERVICE_UNAVAILABLE; } if (exception instanceof HttpException) { return exception.getStatus(); } return StatusCodes.INTERNAL_SERVER_ERROR; } }它把前面所有原则浓缩进了一个过滤器白名单透传只有CallerFacingError的消息原样返回给调用方如导入校验的activities.0.symbol (X) is not valid黑名单隐藏其余异常一律不泄露原始 message只返回状态码对应的标准原因短语避免 DTO 字段名、Prisma 约束信息等内部细节外泄日志分级只有服务端内部错误 500才写 error 日志调用方引起的 4xx如无权限的 Forbidden不刷日志防止日志被垃圾请求灌满异常分类PortfolioSnapshotComputationError→ 503HttpException→ 其自带状态码未知异常 → 500。5.4 测试用例把错误映射固化为契约规则强调过滤器应有可验证性apps/api/src/filters/mcp-tool-exception.filter.spec.ts 用 4 个用例把行为钉死用例输入异常期望输出是否写日志透传调用方面向消息ImportValidationError(activities.0.symbol (X) is not valid){ message: 原消息, status: error }否隐藏意外错误new Error(Unique constraint failed...){ message: Internal Server Error, status: error }是4xx 不刷日志new ForbiddenException(){ message: Forbidden, status: error }否快照不可计算new PortfolioSnapshotComputationError(...){ message: Service Unavailable, status: error }是测试通过firstValueFrom(filter.catch(exception))直接驱动过滤器无需起 HTTP 服务验证了消息白名单、日志分级、状态码映射三条核心契约apps/api/src/app/endpoints/mcp/mcp.controller.spec.ts 则通过反射检查EXCEPTION_FILTERS_METADATA确认过滤器确实挂载在控制器上。六、实践清单把规则变成可执行的工程约束综合规则文档与 Ghostfolio 源码落地 Exception Filters 时应遵循以下检查清单控制器零 try/catch控制器只负责取参 → 调服务 → 返回任何错误状态用throw表达需要Res()手动拼 JSON 的地方一律视为反模式。优先内置异常NotFoundException、BadRequestException、ConflictException等能覆盖 90% 场景需要错误码/上下文时再派生领域异常子类。领域异常与 HTTP 解耦纯业务层如计算、导入抛自定义Error子类由Catch(DomainException)过滤器负责映射状态码与响应体参考PortfolioSnapshotComputationError→ 503。全局兜底 局部精确用APP_FILTER注册一个Catch()兜底过滤器处理所有未预期异常对特定链路如 MCP 工具用UseFilters局部挂载专用过滤器避免污染全局响应格式。对外隐藏内部信息异常 message 可能携带 DTO 字段名、数据库约束名、堆栈细节一律用getReasonPhrase等标准短语替代面向调用方的消息通过显式标记如CallerFacingError白名单透传。日志分级服务端内部错误 500记录完整异常与堆栈可预期的 4xx 不刷 error 日志防止日志噪声。测试固化契约为每个过滤器编写单元测试验证状态码映射、消息透传/隐藏、日志行为让错误响应成为可回归的 API 契约。继承微服务配置Ghostfolio 在 apps/api/src/main.ts 中通过connectMicroservice(..., { inheritAppConfig: true })让全局过滤器/管道同样作用于 MCP 微服务提示我们在接入微服务或协议端点时不要忘记错误处理的一致性继承。结语Exception Filters 不是NestJS 的一个小特性而是一套把错误处理从控制器里的散装代码提升为集中式、可测试、可观测的横切关注点的架构手段。Ghostfolio 仓库给出了一个教科书级的组合领域异常定义在errors目录、APP_FILTER注册全局兜底、UseFilters局部挂载协议专用过滤器、spec 测试钉死契约。照着这套骨架迁移你的控制器就能获得统一的响应格式、更薄的服务边界以及不会把内部细节泄露给调用方的安全防线。赞分享后端前端金融科技数据可视化【免费下载链接】ghostfolioOpen Source Wealth Management Software. Angular NestJS Prisma Nx TypeScript 项目地址https://gitcode.com/GitHub_Trending/gh/ghostfolio点击查看免费下载相关推荐rsschool-app NestJS 错误处理规范使用 Exception Filters 实现统一异常处理rsschool app NestJS 错误处理规范使用 Exception Filters 实现统一异常处理 异常处理是后端 API 质量的分水岭散落在控教育后端前端Comp AI CRM 中的 NestJS Exception Filters用异常过滤器统一全局错误处理Comp AI CRM 中的 NestJS Exception Filters用异常过滤器统一全局错误处理 导读 本文围绕 Comp AI CRM 仓库中的一后端前端CRM人工智能AI AgentPaddleSpeech 服务端异常处理体系详解ServerBaseException 与统一错误响应机制PaddleSpeech 服务端异常处理体系详解ServerBaseException 与统一错误响应机制 导读 PaddleSpeech 在 paddles人工智能语音音频上一篇深岩银河存档修改器DRG Save Editor免费避坑指南从装到改一次讲清下一篇Win11Debloat速成指南三步让Windows 11告别卡顿与弹窗创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考