Commit ee382c07 by luoqi

fix(错误处理): 响应 schema 漂了不再是一句没有日志的「Internal Server Error」

ZodSerializationException 继承的是 InternalServerErrorException, 不是
ZodValidationException —— 名字像,血缘不同。于是它掉进 filter 的通用
HttpException 分支:code=90000、HTTP 保持 200、**一条日志都不写**
(那条分支不打日志,"5xx 兜底日志"也因为 status 是 200 而不触发)。

实测代价:workload 接口挂了,接口只回「Internal Server Error」,
服务日志从头到尾干干净净,查了很久才定位到是响应少了一个字段。

单独拦下并排在通用分支之前,打印 zod issues(哪个字段/期望什么/收到什么),
非生产环境连 details 一起回;单独打 Sentry tag(这是 PAC 自己的 bug)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
parent 0dce436d
...@@ -13,7 +13,7 @@ import { ...@@ -13,7 +13,7 @@ import {
} from '@nestjs/common'; } from '@nestjs/common';
import * as Sentry from '@sentry/nestjs'; import * as Sentry from '@sentry/nestjs';
import { ApiCode, describeApiCode } from '@pac/types'; import { ApiCode, describeApiCode } from '@pac/types';
import { ZodValidationException } from 'nestjs-zod'; import { ZodSerializationException, ZodValidationException } from 'nestjs-zod';
import { ZodError } from 'zod'; import { ZodError } from 'zod';
import type { Request, Response } from 'express'; import type { Request, Response } from 'express';
import { ContractDriftError } from '../../modules/sync/assembler/field-mapper'; import { ContractDriftError } from '../../modules/sync/assembler/field-mapper';
...@@ -28,6 +28,9 @@ import { BizError } from '../errors/biz-error'; ...@@ -28,6 +28,9 @@ import { BizError } from '../errors/biz-error';
* - Truly uncaught Errors → HTTP 500 (so probes / LB still see dead instance). * - Truly uncaught Errors → HTTP 500 (so probes / LB still see dead instance).
* *
* Code mapping policy (priority highest → lowest): * Code mapping policy (priority highest → lowest):
* 0. ZodSerializationException → INTERNAL_ERROR (90000) + **打日志**(响应漂了是 PAC 自己的锅)
* ⚠️ 必须排最前:它继承 InternalServerErrorException,
* 被第 5 条吃掉的话就成了一句没有日志的「Internal Server Error」
* 1. BizError → use its explicit 5-digit code * 1. BizError → use its explicit 5-digit code
* 2. ZodValidationException → CLIENT_VALIDATION_FAILED (10002) * 2. ZodValidationException → CLIENT_VALIDATION_FAILED (10002)
* 3. PayloadTooLargeError → CLIENT_VALIDATION_FAILED (10002), HTTP 200 —— 裸 Error, * 3. PayloadTooLargeError → CLIENT_VALIDATION_FAILED (10002), HTTP 200 —— 裸 Error,
...@@ -50,7 +53,39 @@ export class AllExceptionsFilter implements ExceptionFilter { ...@@ -50,7 +53,39 @@ export class AllExceptionsFilter implements ExceptionFilter {
let msg = describeApiCode(ApiCode.INTERNAL_ERROR); let msg = describeApiCode(ApiCode.INTERNAL_ERROR);
let details: unknown; let details: unknown;
if (exception instanceof BizError) { if (exception instanceof ZodSerializationException) {
/**
* 🔴 **响应**没通过自己的 schema —— 这是 PAC 自己的 bug,不是调用方的。
*
* ⚠️ 必须排在下面的 `HttpException` 之前:`ZodSerializationException`
* 继承的是 `InternalServerErrorException`(⛔ **不是** `ZodValidationException`,
* 名字像但血缘不同)。不单独拦的话它掉进通用 HttpException 分支,
* 对外只剩一句「Internal Server Error / 90000」,
* **日志里一个字都没有**(那条分支不写日志、HTTP 也保持 200,
* 连"5xx"这条兜底日志都不会触发)——
* 2026-08-08 就因为这个,一处响应字段漂了,查了很久才定位。
* ⇒ 这里**必须把 issues 打出来**:哪个字段、期望什么、实际收到什么。
*/
code = ApiCode.INTERNAL_ERROR;
msg = describeApiCode(code);
const err = exception.getZodError();
const issues =
err instanceof ZodError
? err.issues.map((i) => ({
path: i.path.join('.'),
code: i.code,
message: i.message,
}))
: undefined;
this.logger.error(
`${req.method} ${req.url} → 响应不符合 schema(PAC 自身 bug):${JSON.stringify(issues)}`,
);
// ⚠️ details 只在非生产给出:字段路径属于内部结构,⛔ 别漏给宿主
if (process.env.NODE_ENV !== 'production') details = issues;
Sentry.captureException(exception, {
tags: { path: `${req.method} ${req.url}`, kind: 'zod-serialization' },
});
} else if (exception instanceof BizError) {
code = exception.code; code = exception.code;
msg = exception.msg; msg = exception.msg;
details = exception.details; details = exception.details;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment