今天调试前端接口,又被后端同事甩过来的报错恶心到了。点开 network 一看,一个 HTTP 500 直接把完整的 PostgreSQL 错误调用栈 org.postgresql.util.PSQLException: ERROR: duplicate key value violates unique constraint... 暴露在 JSON 响应里。不仅前端直接懵逼,连用户看一眼都能知道我们后台表结构,这简直是裸奔的安全大漏洞。
这种直接把底层 DB 异常原封不动甩给客户端的垃圾接口,在日常排查中简直是灾难。前端根本无法根据这个报错做优雅的 UI 降级提示,只能生硬地展示“系统繁忙”或者把一串英文报错怼给用户。而且,一旦线上出错,由于没有统一的排查 ID(Trace ID),我们要去 ELK 日志库里检索对应的堆栈信息就像大海捞针一样痛苦。
要解决这个问题,首先需要达成共识:API 接口的异常响应应该是一个高内聚的黑盒。底层技术细节(如具体的 SQL 语句、数据库连接错误、代码行数)绝对不能溢出到外网。
我们需要一套统一的 JSON 错误结构,它至少应该包含以下几个核心属性:
- HTTP Status Code:遵循标准 HTTP 规范(如 400 Bad Request, 401 Unauthorized, 422 Unprocessable Entity, 500 Internal Error)。
- Code (业务状态码):细粒度的字符串式业务错误编码,方便前端直接做分支判断(例如
AUTH_TOKEN_EXPIRED远比401更好用)。 - Message:对用户友好的多语言提示语。
- TraceId:全局唯一的请求标识,方便开发人员直接根据该 ID 去日志系统精准定位堆栈。
- Details (可选):具体字段的校验错误信息。
下面是我们目前在 Node.js (TypeScript) 项目中落地并推行的一套统一 API 错误处理和 Express 中间件的具体实现:
// 1. 定义标准的统一错误响应结构
export interface ApiErrorResponse {
code: string; // 业务状态码,例如 'USER_NOT_FOUND', 'INVALID_PARAMETER'
message: string; // 用户友好的多语言错误描述
traceId: string; // 全局唯一请求 ID,便于定位问题
details?: Array<{
field: string;
message: string;
}>;
}
// 2. 自定义基类异常类
export class AppError extends Error {
public readonly statusCode: number;
public readonly code: string;
public readonly details: any;
constructor(statusCode: number, code: string, message: string, details: any = null) {
super(message);
this.statusCode = statusCode;
this.code = code;
this.details = details;
Object.setPrototypeOf(this, new.target.prototype);
}
}
// 3. 统一全局异常处理中间件
import { Request, Response, NextFunction } from 'express';
import { v4 as uuidv4 } from 'uuid';
export function errorHandler(
err: any,
req: Request,
res: Response,
next: NextFunction
): void {
// 生产环境务必生成一个全局排查 ID,并绑定到响应和日志中
const traceId = (req.headers['x-trace-id'] as string) || uuidv4();
let statusCode = 500;
let errorCode = 'INTERNAL_SERVER_ERROR';
let message = '服务器开小差了,请稍后再试';
let details = undefined;
if (err instanceof AppError) {
statusCode = err.statusCode;
errorCode = err.code;
message = err.message;
details = err.details;
} else {
// 未知系统异常,在这里记录堆栈到后台日志系统,但绝对不能输出给客户端
console.error(`[CRITICAL ERROR] TraceId: ${traceId} - Details:`, err);
}
const errorResponseBody: ApiErrorResponse = {
code: errorCode,
message,
traceId,
...(details && { details }),
};
res.status(statusCode).json(errorResponseBody);
}
有了这套设计,前端拿到报错后,可以直接提取 traceId 贴给客服或提工单,后端复制这个 ID 到 Kibana 里一敲,两秒钟就能从几十亿条日志里拽出完整的 DB 异常堆栈,排查效率直线上升。同时,对于表单校验失败(HTTP 422),前端可以非常方便地拿到 details 数组,动态绑定到对应的表单输入框下方进行爆红提示。
不要把接口当作甩锅的工具,给客户端多一点温柔,也就是在拯救我们自己深夜值班的睡眠。
你们公司的 API 错误码是采用纯数字的编码(比如 100021)还是类似我们这种语义化的字符串?在做多语言国际化错误提示时,你们是把字典库放在前端还是后端动态翻译?